必须 fork 两次:第一次让子进程调用 setsid() 脱离终端并成为会话首进程,第二次确保该进程不再是会话首进程,从而永久失去获取控制终端的能力。

为什么 fork 一次不够,必须 fork 两次?
直接 fork() 出子进程后,进程仍可能继承父进程的会话(session)和控制终端(controlling terminal),一旦父进程退出或终端关闭,子进程会被发送 SIGHUP 信号终止——这和“守护进程要长期运行”的目标冲突。
关键在于:守护进程必须不是会话首进程(session leader),否则后续调用 setsid() 会失败;而 setsid() 又是脱离终端的必要步骤。所以标准做法是:
- 第一次
fork():让子进程脱离父进程的上下文,然后父进程退出 - 子进程调用
setsid():成为新会话首进程、脱离控制终端、清空进程组 ID - 第二次
fork():让这个新会话的首进程退出,确保它**永远无法重新获得控制终端**(POSIX 规定:只有会话首进程才能打开终端设备,而这次 fork 后的新子进程不再是会话首进程)
如何正确调用 setsid() 并处理工作目录与文件描述符?
setsid() 必须在第一次 fork() 的子进程中立即调用,且不能在父进程中调用(会失败并返回 -1)。但它只是第一步,还必须配合其他清理动作,否则守护进程可能意外占用资源或阻塞系统。
常见遗漏点:
立即学习“C++免费学习笔记(深入)”;
- 不重定向
stdin/stdout/stderr:默认仍连着终端,日志写入失败或引发 SIGPIPE - 不更改工作目录(
chdir("/")):导致当前挂载点无法卸载(如守护进程启动时在 /mnt/usb 下) - 不设置文件掩码(
umask(0)):子进程创建的文件权限受父进程 umask 影响,不可控 - 不关闭继承的文件描述符:父进程打开的 socket、log 文件等可能被意外读写
建议在第二次 fork() 后的最终子进程中执行这些操作:
umask(0);
chdir("/");
for (int i = 0; i < sysconf(_SC_OPEN_MAX); ++i) {
close(i);
}
open("/dev/null", O_RDONLY);
open("/dev/null", O_WRONLY);
open("/dev/null", O_WRONLY); // 保证 0,1,2 被重定向到 /dev/null
Linux 上如何防止守护进程被 systemd 拦截或误杀?
现代 Linux 发行版普遍使用 systemd 管理服务,直接用传统 fork + setsid 启动的进程容易被归入用户 session,随登录登出被 kill,或被 systemd --user 收集为 transient unit 限制资源。
若你坚持手写 C++ 守护进程(而非写 .service 文件),需主动规避 systemd 的自动管理:
- 避免从交互式 shell 直接启动(比如不要在 ssh 会话里 ./daemon 运行)
- 启动前显式脱离 cgroup:调用
prctl(PR_SET_CHILD_SUBREAPER, 0)(可选,降低被父 cgroup 接管概率) - 检查
/proc/self/cgroup内容,若路径含user.slice或session-,说明仍在用户会话中,应改用systemctl --system enable && systemctl start - 更稳妥的做法:放弃纯 fork 方案,改用
systemd托管,C++ 程序只做核心逻辑,不自己 fork
std::this_thread::sleep_for 在守护进程中为什么不能替代信号等待?
很多初学者用一个无限循环加 std::this_thread::sleep_for(std::chrono::seconds(1)) 模拟守护行为,但这只是“假装”在运行,实际无法响应外部事件(如配置重载、平滑重启、优雅退出)。
真正健壮的守护进程必须基于异步事件驱动,例如:
- 用
signalfd()(Linux)或sigwait()等待SIGHUP、SIGTERM - 用
epoll_wait()或poll()监听配置文件 inotify fd、socket、定时器 fd - 避免阻塞在 sleep 上——它让进程对信号不敏感,且无法实现“收到信号立刻处理”
哪怕只是最简版本,也建议至少处理 SIGTERM:
volatile sig_atomic_t keep_running = 1;
void signal_handler(int sig) { if (sig == SIGTERM) keep_running = 0; }
signal(SIGTERM, signal_handler);
while (keep_running) {
// do work
sleep(1);
}
守护进程的难点不在 fork 次数,而在“彻底断连”——断开终端、会话、父进程、文件描述符、信号屏蔽、cgroup 和 systemd 的隐式绑定。漏掉任意一环,都可能在某个环境里突然失效。











