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

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 可能拒绝启动,或日志混乱。尤其 stdout 和 stderr 若未关闭,某些库(如 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=null和StandardOutput=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)显式声明 -
SIGCHLDhandler 内只调用waitpid(-1, &status, WNOHANG)一次,不循环,避免漏掉多个子进程退出事件 - 避免在 signal handler 中调用
printf、malloc、open等非 async-signal-safe 函数
真正麻烦的从来不是 fork 或 setsid,而是信号语义和 systemd 模式切换之间的隐含契约——写的时候以为在跑裸机,上线才发现跑在容器或 systemd 里,所有“标准做法”都成了反模式。