必须两次fork:第一次为脱离终端和成为孤儿进程以满足setsid前提,第二次为防止重获控制终端;setsid创建新会话并脱离终端,但要求调用者非进程组组长;chdir("/")避免阻塞文件系统卸载,需在两次fork和setsid后执行。

要让一个普通程序变成真正可靠的守护进程,关键不是加 & 或用 nohup,而是按固定顺序调用 fork、setsid 和 chdir 等系统调用。这个序列不是随意安排的,每一步都解决一个特定依赖问题:终端控制、会话归属、文件系统挂载点锁定。
为什么必须两次 fork?
第一次 fork 是为了摆脱父进程和终端的强绑定关系。父进程退出后,子进程成为孤儿进程,被 init(或 systemd)收养,不再受 Shell 生命周期影响。更重要的是,它不再是进程组组长——这是调用 setsid() 的硬性前提。
第二次 fork 发生在 setsid() 之后,目的是防止进程意外重新获取控制终端。因为会话首进程在某些条件下(比如打开 /dev/tty)可能再次获得终端控制权;再 fork 一次,让最终的服务进程变成非会话首进程,彻底杜绝这种可能。
- 第一次 fork 后,父进程必须立即 exit,不能做其他事
- 第二次 fork 后,原子进程(即 setsid 的调用者)也要 exit,只留最内层子进程运行服务逻辑
- 两次 fork 都要检查返回值,
pid == 0表示当前是子进程,pid > 0表示父进程需退出
setsid 的作用不只是“新建会话”
setsid() 实际完成三件事:创建新会话、成为新会话首进程、脱离控制终端。但它有个隐藏前提——调用者不能是进程组组长。所以它必须放在第一次 fork 的子进程中执行。
执行成功后,进程的 session ID(sid)和进程组 ID(pgid)都会变成新值,且 getsid(0) 返回值不再等于原终端的 sid。此时它已无法接收 SIGHUP、SIGINT 等与终端相关的信号,也不会因 SSH 断开而终止。
- 如果在未 fork 的主进程中直接调用 setsid,会失败并返回 -1
- setsid 后不要立即 reopen 标准流,应等第二次 fork 完成后再处理
- setsid 不会自动关闭文件描述符,这步要手动做
chdir("/") 不只是为了“切换目录”
把工作目录切到根目录,表面看是避免路径残留,实际核心目的是防止守护进程阻塞文件系统卸载。比如程序启动时工作目录在 /mnt/usb,后来你拔掉 U 盘,系统就无法卸载该设备——因为有进程仍持有该挂载点的引用。
这步必须在两次 fork 和 setsid 之后做,否则若在早期就 chdir,后续 fork 出的子进程仍可能继承旧路径。同时要配合 umask(0) 和关闭所有 fd,否则新建文件权限不可控,或日志写入失败。
- 不能用
chdir("/tmp")或其他非根路径替代,只有 "/" 是安全的卸载锚点 - chdir 成功后,建议验证返回值,失败时应记录错误并退出,而不是静默忽略
- 如果服务本身需要访问特定路径(如配置文件),应在 chdir 后用绝对路径打开,而非相对路径
整个流程环环相扣,漏掉任何一环都可能导致进程在后台异常退出、资源泄漏,或在系统重启/卸载设备时出问题。不复杂但容易忽略。











