守护进程需两次 fork:第一次脱离父进程组,第二次在 setsid() 后确保不成为会话首进程,彻底避免获取控制终端;随后重定向标准流至 /dev/null、chdir("/")、umask(0)、安全创建 pidfile 并清理资源。

为什么 fork 两次是常见做法
守护进程需要脱离终端控制,避免被 SIGHUP 终止,同时不能成为会话首进程(否则可能意外获得控制终端)。第一次 fork() 让子进程脱离父进程的进程组;第二次 fork() 是关键——子进程调用 setsid() 后成为会话首进程,但此时它仍可能在某些系统上重新获取控制终端(比如打开 /dev/tty 时)。再 fork 一次,让最终的子进程不再是会话首进程,彻底失去获取终端的能力。
常见错误是只 fork 一次后就 setsid(),结果在某些 Linux 发行版(如 systemd 管理的环境)中,进程仍可能被分配到默认 tty 或被 cgroup 误判为交互式进程。
- 第一次 fork 后,父进程应直接
exit(),避免残留 - 第二次 fork 前必须先
setsid(),否则setsid()会失败(只有进程组组长才能调用) - 两次 fork 后,最终子进程的 PPID 变为 1(init 或 systemd),这是守护进程的典型特征
标准重定向:/dev/null 还是 /dev/console?
守护进程的标准输入、输出、错误必须重定向,否则它们可能继承自父进程(比如 shell 终端),导致日志写入失败或意外阻塞。正确做法是全部重定向到 /dev/null,而不是 /dev/console ——后者在现代 Linux 中通常不可写,且仅限特权进程访问,普通守护进程写入会触发权限拒绝或静默失败。
更稳妥的方式是显式关闭 fd 0/1/2,再用 open("/dev/null", O_RDWR) 三次,确保 fd 0、1、2 被复用。这样即使原 fd 被意外关闭或未初始化,也能保证标准流可用。
- 不要依赖
freopen("/dev/null", "r", stdin),它不保证 fd 复用顺序 - systemd 管理的守护进程可跳过重定向,由 journald 接管日志,但传统 SysV 风格必须手动处理
- 若需调试,临时将 stderr 重定向到文件(如
/var/log/mydaemon.log),但上线前务必改回/dev/null,避免日志无限增长
chdir("/") 和 umask(0) 的真实作用
chdir("/") 不是为了“进入根目录”这个动作本身,而是防止当前工作目录锁住某个挂载点——如果守护进程的工作目录位于一个待卸载的文件系统上(如 USB 设备),该设备将无法被卸载。而 umask(0) 是为了消除父进程传入的掩码影响,确保守护进程创建文件时权限可控(比如日志轮转、pidfile 写入等操作能按预期设置权限)。
集企业自助建站、网络营销、商品推广于一体的系统 功能说明: 1、系统采用Microsoft SQL Server大型数据库支持,查询数据库用的全是存储过程,速度和性能极好。开发环境是vs.net,采用4层结构,具有很好的可维护性和可扩冲性。 2、用户注册和登陆 未注册用户只具备浏览商品、新闻和留言功能;要采购商品,需接受服务协议并填写相关注册信息成为正式用户后方可进行,以尽可能减少和避免无效
这两个调用常被误认为“形式主义”,但实际线上故障中,因未 chdir("/") 导致 NFS 挂载点卡死、因 umask 遗留导致 pidfile 权限为 0600(其他用户无法读取,影响监控脚本)的情况并不少见。
- 不要用
chdir("/tmp")替代chdir("/"),/tmp 可能是独立挂载且带 noexec/nodev 选项,反而引发问题 -
umask(0)后应立即对关键文件(如 pidfile)显式指定权限,例如open(..., O_CREAT, 0644) - 若进程需访问特定路径(如配置目录),应在完成初始化后再
chdir(),而非省略初始chdir("/")
pidfile 的创建与竞态处理
pidfile 不是可选装饰,而是进程生命周期管理的关键锚点。但直接 fopen("mydaemon.pid", "w") 写入是危险的:多个实例可能同时判断文件不存在,然后都写入自己的 PID,造成覆盖和误杀。
正确方式是用 open("mydaemon.pid", O_CREAT|O_RDWR|O_EXCL) —— O_EXCL 保证原子性,仅当文件不存在时才成功。失败即说明已有实例在运行,应退出。写入后建议调用 fsync() 确保内容落盘,避免断电导致 pidfile 内容为空或损坏。
- 不要用
kill -0 $(cat pidfile)判断进程存活,PID 可能已被复用;应配合文件锁(flock())或检查 /proc/PID/exe 符号链接 - 进程退出前必须 unlink pidfile,否则下次启动会被误判为已存在
- pidfile 路径应使用绝对路径(如
/var/run/mydaemon.pid),避免相对路径在 chdir 后失效
真正棘手的不是 fork 或重定向这些步骤,而是它们之间的时序和状态清理是否彻底。比如忘记在第二次 fork 后关闭所有继承的 fd,可能导致日志文件句柄泄露;又或者 umask 设置后没重设,影响后续 dlopen 加载的共享库行为。这些细节不会报错,但会在高负载或长时间运行后暴露出来。









