C++如何实现守护进程?(Linux daemon编写指南)

1次阅读

守护进程必须 fork 两次:第一次使子进程成为孤儿并由 init/systemd 接管,第二次确保无法获取控制终端;再调用 setsid() 创建新会话、chdir(“/”)、umask(0),并显式重定向 stdin/stdout/stderr 至 /dev/NULL

C++如何实现守护进程?(Linux daemon编写指南)

fork 两次是必须的,不是为了炫技

守护进程必须脱离终端控制,否则 SIGHUP 会随终端关闭直接杀死进程。一次 fork 不够——子进程仍属于原会话,可能继承控制终端;第二次 fork 后,孙子进程调用 setsid() 创建新会话,彻底断开与终端、进程组、会话的关联。

常见错误:只 fork 一次就 setsid(),结果进程仍可能被 SIGHUP 终止,或在 systemd 下被标记为 “not a daemon”。

  • 第一次 fork():父进程退出,子进程成为孤儿,由 init(或 systemd)接管
  • 子进程调用 setsid():脱离原会话,获得新会话 leader 身份,且失去控制终端
  • 第二次 fork():确保该进程无法再次获取控制终端(POSIX 要求)
  • 子进程再调用 chdir("/")umask(0):避免阻塞卸载文件系统、统一权限掩码

标准输入/输出/错误必须重定向到 /dev/null

不重定向会导致守护进程意外持有终端 fd,systemd 或 supervisord 可能拒绝启动,或日志混乱。尤其 stdoutstderr 若未关闭,某些库(如 glibc 日志)会尝试写入已失效的终端,触发 SIGPIPE

使用场景:你希望日志走 syslog 或文件,而不是终端回显。

立即学习C++免费学习笔记(深入)”;

  • close(STDIN_FILENO) 关闭三个标准 fd
  • open("/dev/null", O_RDWR) 三次,确保 fd 0/1/2 被复用为 /dev/null
  • 不要只 dup2 一次然后假设其他 fd 自动就位——dup2 不保证重用最小可用 fd,必须显式覆盖 0、1、2

systemd 下不要自己 fork,否则会冲突

如果你的服务由 systemd 管理(绝大多数现代 linux 发行版默认如此),手动 fork + setsid 是错的。Type=forking 需要程序自己 daemonize,但更推荐 Type=simple —— systemd 期望主进程不 fork,它自己负责后台化和生命周期管理。

错误现象:systemctl status mydaemon 显示 “inactive (dead)”,日志里有 Failed to start daemon: No such file or Directory,其实是 systemd 找不到它认为的“主进程”。

  • Type=simple,程序入口直接运行业务逻辑,不 fork
  • 删掉所有 fork/setsid/重定向代码,让 systemd 处理后台化
  • 若必须兼容传统 init(如 SysV),可加编译宏区分:定义 DAEMONIZE 时才执行 fork 流程
  • 注意:systemd 的 StandardInput=nullStandardOutput=journal 已隐式处理了 fd 重定向

信号处理要谨慎,尤其是 SIGCHLD 和 SIGHUP

守护进程常忽略 SIGHUP,但若没显式设置,它仍按默认行为终止——这点容易被忽略。而 SIGCHLD 若未处理,子进程变成僵尸;若错误地 waitpid(-1, &status, WNOHANG)循环里狂轮询,又可能饿死 CPU。

性能影响:信号 handler 里做复杂操作(如打开文件、分配内存)是危险的,因为信号可能中断任意系统调用。

  • sigemptyset(&sa.sa_mask) + sa.sa_flags = SA_RESTART 设置 handler,避免被中断的系统调用失败
  • SIGHUP 通常用于重载配置,不是必须忽略;若真要忽略,用 signal(SIGHUP, SIG_IGN) 显式声明
  • SIGCHLD handler 内只调用 waitpid(-1, &status, WNOHANG) 一次,不循环,避免漏掉多个子进程退出事件
  • 避免在 signal handler 中调用 printfmallocopen 等非 async-signal-safe 函数

真正麻烦的从来不是 fork 或 setsid,而是信号语义和 systemd 模式切换之间的隐含契约——写的时候以为在跑裸机,上线才发现跑在容器或 systemd 里,所有“标准做法”都成了反模式。

text=ZqhQzanResources