1. 守护进程的概念
在我们之前写的大多数程序中,如果你关闭控制终端,就很可能会导致在该控制终端中运行的进程也退出。
控制终端关闭时,该控制终端符合下面条件的进程会收到 SIGHUP 信号:
- 前台进程组的所有进程
- 某个后台进程组中存在停止的进程(ps ajx 命令中,进程状态显示为 T)。控制终端关闭,导致该后台进程组成为孤儿进程组,则该孤儿进程组中的所有进程都收到 SIGHUP 信号。
所以,并不是所有情况下控制终端中的进程都会收到 SIGHUP 信号。
有时候,我们希望进程不和任何控制终端挂钩,即使你关闭终端对它也没有任何影响。另外我们不希望这种进程向终端读写数据(需要关闭标准输入、标准输出和标准错误),它会一直在后台运行着,像这种进程,称之为守护进程,或者叫精灵进程(daemon process).
图 1 中展示的都是守护进程。COMMAND 一栏中,带方括号的是内核级守护进程。我们看到 1 号 init 进程也是守护进程。守护进程都没有控制终端,比如 TTY 一栏全部是问号,既然没有控制终端,那也就没有前台进程组和后台进程组的概念了,所以 TPGID 一栏都是 -1.
图1 守护进程
2. 创建守护进程
本篇先不打算讲解守护进程的编写步骤,我们先使用系统提供的函数 daemon
编写一个守护进程,它在后台工作,并不断向test.log
文件写入数据。
daemon
的原型如下:
int daemon(int nochdir, int noclose);
参数 nochdir 如果为 0,表示将当前工作目录切换到根目录 “/”;否则当前目录不变。
参数 noclose 如果为 0,表示重定向标准输入、标准输出和标准错误到文件 /dev/null;否则这些文件描述符不变。
2.1 程序清单
- 代码
// daemon.c
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main() {
// 先打印进程的 pid 等信息
printf("pid: %d, ppid: %d, sid: %d\n", getpid(), getppid(), getsid(getpid()));
// 将进程设置为守护进程
daemon(0, 0);
int fd;
char buf[256];
// 守护进程向标准输出打印信息,并向 test.log 写信息
while(1) {
// 实际上,这一行永远不会打印到屏幕,因为守护进程没有控制终端,标准输出也被重定向到了 /dev/null
printf("pid: %d, ppid: %d, sid: %d\n", getpid(), getppid(), getsid(getpid()));
// 因为 daemon 函数改变了当前工作目录,所以这里使用全路径定位到当前目录。
fd = open("/home/allen/apue/relationship/daemon/test.log", O_WRONLY | O_APPEND | O_CREAT, 0664);
if (fd > 0) {
sprintf(buf, "pid: %d, ppid: %d, sid: %d\n", getpid(), getppid(), getsid(getpid()));
write(fd, buf, strlen(buf));
}
sleep(3);
}
return 0;
}
- 编译和运行
$ gcc daemon.c -o daemon
$ ./daemon
2.2 结果分析
当我们运行 ./daemon 后,发现屏幕只输出一行:
图2
使用 ls
命令查看当前文件夹下,发现生成了 test.log 文件。
查看 test.log 文件:
图3 test.log 文件
使用 ps ajx
查看进程信息:
图4 ps ajx 查看进程信息
值得注意的是,我们发现图 2 和图 3 中的信息是一致的,只是图 1 中的结果,貌似不怎么对?
图 1 中打印的进程 pid 明明是 6169,父进程是 3993,而且它也在会话 3993 中。可到了图 2 和图 3 中,情况貌似有变化,它的进程 id 号“变成”了 6170,父进程是 1,另外,它在会话 6170 中!
其实,这没有什么不对,如果你能结合我们前面学过的有关进程间关系所有的知识,你能想明白。
daemon 函数里头肯定是动了手脚,我们应该能想到,图 2 中的结果,实际上已经不是你启动的那个进程了,而是你启动的那个进程的子进程!而你启动的那个进程,已经运行结束,子进程被 1 号进程收养,成为了孤儿进程。另外,该子进程还创建了一个新会话!
到这里,我想即便你不使用系统提供的 daemon 函数,你也能够自己手工写一个类似这样的函数了。
只不过,除了 fork 子进程,创建新会话,你还应该关注一些其它的细节,这将在下一篇博文介绍。
3. 总结
- 理解什么是守护进程
- 学会使用 daemon 函数
- 理解 daemon 函数做了什么工作
练习 1 :完成本文中的例子。
练习 2:不查阅资料,尝试自己编写一个 daemon 函数。
思考:守护进程一定不能向标准输入写数据吗?