一、什么是守护进程?
守护进程 (Daemon,通常以字母 d 结尾,如 sshd、httpd、crond)是满足以下特征的进程:
- 在后台运行;
- 不与任何终端关联;
- 长期驻留内存;
- 通常提供某种系统服务。
与之相对的是交互式进程 。在 Shell 中直接执行 ./my_program 启动的程序属于交互式进程,它受当前终端控制:按下 Ctrl+C 会向前台进程组发送 SIGINT,关闭终端会发送 SIGHUP,这些信号的默认行为都是终止进程。
守护进程需要解决的核心问题是:
如何让一个进程脱离终端的控制,成为独立于终端、稳定运行的后台服务进程。
要理解实现方法,必须先明确 Linux 中进程、终端、进程组和会话之间的关系。
二、进程与终端的关系
2.1 控制终端
用户登录 Linux 系统后,系统会分配一个控制终端(Controlling Terminal)。在该终端中启动的进程,都直接或间接受到这个终端的约束。具体表现为:
- 终端按下
Ctrl+C会向前台进程组发送SIGINT,默认终止进程; - 终端关闭或网络断开时,系统向与控制终端关联的进程发送
SIGHUP,默认终止进程; - 进程的标准输入、标准输出、标准错误默认绑定到该终端设备。
因此,要让进程不随终端关闭而终止,必须切断它与控制终端的关联。
2.2 进程组
进程组(Process Group) 是一组进程的集合,用于统一接收信号和管理。例如管道命令:
bash
grep "error" log.txt | wc -l
会创建多个进程,它们被放入同一个进程组。
每个进程组有一个组长(Leader):
- 组长的 PID 等于该进程组的 PGID;
- 进程组 ID(PGID)就是组长进程的 PID。
组长身份具有特殊性:它代表进程组的标识,不能随意脱离自己所在的进程组。如果组长退出,该进程组可能成为"孤儿进程组",需要内核特殊处理。
2.3 会话
一个或多个进程组组成一个会话(Session)。
- 每次登录终端,系统会创建一个新的会话;
- 会话有一个首进程(Session Leader) ,通常是登录 Shell(如
bash); - 一个会话最多只能有一个控制终端,该终端通常由会话首进程在登录时建立。
2.4 setsid() 的前提条件
要切断与控制终端的关联,关键系统调用是 setsid(),其作用是:
- 创建一个全新的会话;
- 使调用者成为该会话的首进程;
- 使调用者失去原有的控制终端。
但 setsid() 有一个硬性限制:
调用者不能是进程组组长。
原因是:组长是进程组的标识,如果组长调用 setsid() 离开原进程组,原进程组会失去组长,可能变成孤儿进程组,内核难以管理。因此 POSIX 标准直接禁止组长调用 setsid()。
然而,在 Shell 中启动一个程序时,该程序通常就是某个进程组的组长。因此,要合法调用 setsid(),必须先让自己不再是组长。解决办法是第一次 fork()。

三、创建守护进程的核心步骤:两次 fork() 与 setsid()
3.1 第一次 fork():获得调用 setsid() 的资格
当前程序是进程组组长,不能直接调用 setsid()。通过 fork() 创建子进程:
- 子进程继承父进程的进程组,但其 PID 是新分配的;
- 因此子进程的 PID 不等于 PGID,不是进程组组长;
- 父进程立即退出,子进程继续运行。
父进程退出后,Shell 认为该命令已结束,会重新显示提示符;子进程则在后台继续执行。
此时子进程虽然不是组长,但仍处于原会话中,不过已经具备调用 setsid() 的资格。
3.2 setsid():创建新会话并脱离控制终端
子进程调用 setsid() 后,发生以下变化:
- 子进程成为一个全新会话的首进程,会话 ID(SID)等于它的 PID;
- 子进程成为一个全新进程组的组长;
- 子进程失去原有的控制终端。
从此,终端关闭时产生的 SIGHUP 信号不会再发送给该进程。
到这一步,进程已经基本脱离终端控制,但还存在一个潜在问题。
3.3 第二次 fork():防止重新获得控制终端
setsid() 之后的进程是会话首进程。会话首进程具有一个潜在能力:
如果它打开一个尚未被控制的终端设备文件,并且没有使用
O_NOCTTY标志,内核会自动将该终端分配为它的控制终端。
虽然守护进程一般不会主动打开终端设备,但程序中可能存在 bug 或特殊逻辑打开了 /dev/tty、串口等设备,导致进程意外重新获得控制终端,再次受到终端信号影响。
为了彻底避免这种情况,标准做法是再执行一次 fork():
- 会话首进程(记为 P2)调用
fork()产生孙进程 P3; - P2 立即退出,P3 继续运行。
此时,P3 的 PID 不等于 SID,因此不是会话首进程 ;同时 P3 的 PID 也不等于 PGID,因此也不是进程组组长。
根据 POSIX 标准:只有会话首进程才有资格申请控制终端。因此 P3 永久失去了获得控制终端的能力,成为严格意义上的后台守护进程。
四、环境清理
成为守护进程后,需要清理从父进程继承的环境,避免对系统或自身造成隐患。
4.1 切换工作目录
c
chdir("/");
守护进程继承了父进程的当前工作目录,例如 /home/user。如果它一直占用该目录,管理员可能无法卸载对应的文件系统。切换到根目录 / 后,守护进程不会阻碍任何文件系统的卸载操作。
4.2 重置文件权限掩码
c
umask(0);
父进程可能设置了非默认的 umask(如 022),这会导致守护进程创建文件时权限被隐式修改。重置为 0 后,文件权限完全由 open() 或 creat() 的权限参数决定,行为更可控。
4.3 处理标准输入输出
守护进程不再拥有终端,因此继承自终端的 stdin、stdout、stderr 文件描述符已经失效。如果不处理,可能导致:
- 后续打开的文件占用 0、1、2 这三个文件描述符,造成混乱;
- 向无效的标准输出或标准错误写入数据可能引发错误。
标准做法是将这三个文件描述符重定向到 /dev/null:
c
int fd = open("/dev/null", O_RDWR);
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
if (fd > 2) close(fd);
也可以直接关闭它们,但重定向到 /dev/null 更安全,能避免某些库函数因文件描述符无效而出现问题。
五、信号处理
5.1 忽略 SIGCHLD
守护进程通常会创建子进程处理具体任务。如果子进程退出后父进程不调用 wait(),子进程会变成僵尸进程。对于长期运行的守护进程,显式等待所有子进程可能很繁琐。简单做法是忽略 SIGCHLD:
c
signal(SIGCHLD, SIG_IGN);
这样内核会自动回收已退出的子进程,避免僵尸进程累积。
注意:如果守护进程需要获取子进程的退出状态,则不能简单忽略
SIGCHLD,必须使用waitpid()等方式显式处理。
5.2 忽略 SIGPIPE
网络服务型守护进程经常读写 socket。如果客户端突然断开,进程继续向已关闭的 socket 写数据,内核会发送 SIGPIPE 信号,默认行为是终止进程。对于服务端,客户端断开是常见情况,不能因此导致整个服务崩溃。因此通常忽略 SIGPIPE:
c
signal(SIGPIPE, SIG_IGN);
忽略后,write() 会返回 EPIPE 错误,由业务代码决定如何处理。
六、总结
创建守护进程的步骤可以归纳为以下逻辑链:
-
脱离终端控制
→ 需要调用
setsid()创建新会话并切断控制终端。 -
满足
setsid()的调用条件→ 调用者不能是进程组组长,因此先
fork()一次,使子进程成为非组长。 -
防止会话首进程重新获得控制终端
→ 再
fork()一次,使最终进程既不是会话首进程,也不是进程组组长。 -
环境清理
→ 切换工作目录到
/,重置umask,重定向标准 I/O 到/dev/null。 -
信号处理
→ 忽略
SIGCHLD防止僵尸进程,忽略SIGPIPE防止因客户端断开而崩溃。
这些步骤不是随意堆砌,而是基于 Linux 进程模型逐步推导得出的必要操作。理解背后的原理后,就能正确编写守护进程的初始化代码。