守护进程(daemon),简单来说就是可以脱离终端在后台运行的进程,脱离终端后成为内核初始进程的子进程。常见的服务如 web 服务都以守护进程的方式运行,它们的特点就是需要长时间或者永久运行,持续对外提供服务。
进程包括以下几个 ID:
- PID:进程 ID,进程的唯一标识。
- PPID:父进程 ID。
- PGID:进程组 ID,每个进程都会有进程组 ID,表示该进程所属的进程组。默认情况下新创建的进程会继承父进程的进程组 ID。
- SID:会话 ID,每个进程都有会话 ID。默认情况下,新创建的进程会继承父进程的会话 ID。
在 Linux 中可以通过ps ajx
查看这几个 ID。一般 PPID 为 0 的,都是内核态进程;一般 PPID 为 1 且 PID == PGID == SID 的,都是守护进程,当然还有一些特殊情况,PID 跟 PGID、SID 不一致。
守护进程的创建流程如下:
- fork 一个子进程,退出主进程,目的是让子进程继承进程组 ID,并获取一个新的进程 ID,这保证了子进程一定不是进程组组长,因为进程组组长不能创建新会话。
- setsid 创建新会话以及设置进程组 ID,成为会话组长和进程组组长,脱离原会话、进程组、终端控制,这样该进程就不会被原终端的控制信号中断。使得进程完全独立出来,摆脱其它进程的控制。
- 再次 fork 出子进程,退出父进程(前一个子进程)。通过再次创建子进程结束当前进程,使进程不再是会话首进程来禁止进程重新打开控制终端。这个步骤并不是必须的,只是在基于 System-V 的系统上,有人建议再 fork 一次,避免打开终端设备,使程序的通用性更强。
- 设置文件创建掩码,在子进程中调用 umask(0) 重设文件权限,这是因为子进程继承了父进程的文件权限掩码,带来一定麻烦。
这个并不是必要的,需要操作文件才需要
。
补充 APUE 中的守护进程编程规则
以上守护进程的创建流程可能不是太规范。以下是 APUE 中给出的在编写守护进程程序时需遵循的基本规则。
- 首先要做的是调用 umask 将文件模式创建屏蔽字设置为一个已知值(通常是 0)。由继承得来的文件模式创建屏蔽字可能会被设置为拒绝某些权限。如果守护进程要创建文件,那么它可能要设置特定的权限。
- 调用 fork,然后使父进程 exit。这样做实现了下面几点。第一,如果该守护进程是作为一条简单的 shell 命令启动的,那么父进程终止会让 shell 认为这条命令已经执行完毕。第二,虽然子进程继承了父进程的进程组 ID,但获得了一个新的进程 ID,这就保证了子进程不是一个进程组的组长进程。这是下面将要进行的 setsid 调用的先决条件。
- 调用 setsid 创建一个会话。使调用进程:(a)成为新会话的首进程,(b)成为一个新进程组的组长进程,(c)没有控制终端。在基于 System V 的系统中,有些人建议在此时再次调用 fork,终止父进程,继续使用子进程中的守护进程。这就保证了该守护进程不是会话首进程,于是按照 System V 规则,可以防止它取得控制终端。
- 将当前工作目录更改为根目录。从父进程处继承过来的当前工作目录可能在一个挂载的文件系统中。因为守护进程通常在系统再引导之前是一直存在的,所以如果守护进程的当前工作目录在一个挂载文件系统中,那么该文件系统就不能被卸载。或者,某些守护进程还可能会把当前工作目录更改到某个指定位置,并在此位置进行它们的全部工作。
- 关闭不再需要的文件描述符。这使守护进程不再持有从其父进程继承来的任何文件描述符。
- 某些守护进程打开
/dev/null
使其具有文件描述符 0、1 和 2,这样,任何一个试图读标准输入、写标准输出或标准错误的库例程都不会产生任何效果。因为守护进程并不与终端设备相关联,所以其输出无处显示,也无处从交互式用户那里接收输入。即使守护进程是从交互式会话启动的,但是守护进程是在后台运行的,所以登录会话的终止并不影响守护进程。如果其他用户在同一终端设备上登录,我们不希望在该终端上见到守护进程的输出,用户也不期望他们在终端上的输入被守护进程读取。
因此,以下程序也并不太规范,缺少了第 4-6 步。
守护进程示例
以下是 C 语言的示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
umask(0);
pid_t pid;
pid = fork();
if (pid < 0) {
printf("fork error\n");
exit(1);
} else if (pid > 0) {
sleep(10);
printf("first process exit\n"); // 2
return 0;
}
printf("before setsid\n"); // 1
sleep(20);
setsid();
printf("after setsid\n"); // 3
pid = fork();
if (pid < 0) {
printf("fork error\n");
exit(2);
} else if (pid > 0) {
sleep(10);
printf("second process exit\n"); // 4
return 0;
}
sleep(20);
printf("Hello world\n"); // 5
return 0;
}
附加个 PHP 版本的,跟 C 语言的大同小异,需要安装pcntl
和posix
扩展。后面的说明以 C 语言的为准,当然 PHP 版本的试过也没有问题,效果是一致的。
<?php
umask(0);
$pid = pcntl_fork();
if ($pid < 0) {
printf("fork error\n");
exit(1);
} else if ($pid > 0) {
sleep(10);
printf("first process exit\n"); // 2
exit(0);
}
printf("before setsid\n"); // 1
sleep(20);
posix_setsid();
printf("after setsid\n"); // 3
$pid = pcntl_fork();
if ($pid < 0) {
printf("fork error\n");
exit(2);
} else if ($pid > 0) {
sleep(10);
printf("second process exit\n"); // 4
exit(0);
}
sleep(20);
printf("Hello world\n"); // 5
exit(0);
接着,根据以上程序的运行情况进行分析。
不同时间点的进程状态
需要额外说明一下,由于运行环境处于 docker 中,所以守护进程的 PPID 不是 1,而是 0。每个时间点顺序位于以上程序的注释处。以下内容以 C 语言实现的版本为准。
before setsid
root@4b3b2feeac63:/var/www/html# ps ajx | sort -k 10
16 145 145 16 pts/1 145 S+ 0 0:00 ./a.out
145 146 145 16 pts/1 145 S+ 0 0:00 ./a.out
0 16 16 16 pts/1 145 Ss 0 0:00 /bin/bash
0 134 134 134 pts/0 147 Ss 0 0:00 /bin/bash
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
这时,第一次创建完子进程,子进程的 PPID、PID、PGID、SID 分别为 145、146、145、16。可以看到进程 16 和 145 的 PGID 都是 145,PGID 跟进程 145 的 PID 相同,两个进程位于同一个进程组;而 SID 则是为 16,跟 PID 为 16 的终端一致,进程 145、146、16 共享一个会话。
first process exit
root@4b3b2feeac63:/var/www/html# ps ajx | sort -k 10
0 146 145 16 pts/1 16 S 0 0:00 ./a.out
0 16 16 16 pts/1 16 Ss+ 0 0:00 /bin/bash
0 134 134 134 pts/0 149 Ss 0 0:00 /bin/bash
134 150 149 134 pts/0 149 R+ 0 0:00 /bin/bash
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
第一次父进程退出后,子进程 PPID 变成了 0,这是因为子进程变成孤儿进程
之后被内核初始进程收养了,docker 容器的内核初始进程 PID 就是 0;其余的 ID 并没有变化。
after setsid
root@4b3b2feeac63:/var/www/html# ps ajx | sort -k 10
0 146 146 146 ? -1 Ss 0 0:00 ./a.out
146 153 146 146 ? -1 S 0 0:00 ./a.out
0 16 16 16 pts/1 16 Ss+ 0 0:00 /bin/bash
0 134 134 134 pts/0 156 Ss 0 0:00 /bin/bash
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
调用 setsid 之后,同时第二次创建了子进程,也就是进程 153。这时,第一次创建的子进程(也就是 PPID 为 0 的进程)PGID 和 SID 都变成 146,跟其 PID 相同,实现了自己的进程组和会话,此时进程已经跟进程 16 (终端)脱离了;同时,进程 153 的 PGID 和 SID 跟进程 146 相同,也就是它们在同一个进程组中,并共享会话。
second process exit
root@4b3b2feeac63:/var/www/html# ps ajx | sort -k 10
0 153 146 146 ? -1 S 0 0:00 ./a.out
0 16 16 16 pts/1 16 Ss+ 0 0:00 /bin/bash
0 134 134 134 pts/0 158 Ss 0 0:00 /bin/bash
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
第一次创建的子进程退出,它的子进程保留原有的 PGID 和 SID,但是 PID 跟 PGID、SID 不相同的,也就是本文开头所说的特殊情况。
Hello world
root@4b3b2feeac63:/var/www/html# ps ajx | sort -k 10
0 16 16 16 pts/1 16 Ss+ 0 0:00 /bin/bash
0 134 134 134 pts/0 162 Ss 0 0:00 /bin/bash
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
守护进程完成使命,光荣退出。
当然常见的守护进程并不会自行退出。由于守护进程已经脱离终端了,因此不能直接使用 Ctrl+C 退出。Ctrl+C 本质上就是给进程发送一个 SIGINT 信号,当然我们还可以通过 kill 命令给进程发送信号,不过这方法不优雅。既然知道了退出进程的方法就是给进程发送信号,我们就可以通过命令参数传入命令,并写对应的信号处理器,在处理器中进行资源回收等操作并退出,本文就不多说了。