

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