Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3. 守护进程

Linux 系统启动时会启动很多系统服务进程,例如第 1.3 节 “网络登录过程”讲的 inetd ,这些系统服务进程没有控制终端,不能直接和用户交互。其它进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程不受用户登录注销的影响,它们一直在运行着。这种进程有一个名称叫守护进程(Daemon)。

下面我们用 ps axj 命令查看系统中的进程。参数 a 表示不仅列当前用户的进程,也列出所有其他用户的进程,参数 x 表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数 j 表示列出与作业控制相关的信息。

$ ps axj
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    0     1     1     1 ?           -1 Ss       0   0:01 /sbin/init
    0     2     0     0 ?           -1 S<       0   0:00 [kthreadd]
    2     3     0     0 ?           -1 S<       0   0:00 [migration/0]
    2     4     0     0 ?           -1 S<       0   0:00 [ksoftirqd/0]
...
    1  2373  2373  2373 ?           -1 S<s      0   0:00 /sbin/udevd --daemon
...
    1  4680  4680  4680 ?           -1 Ss       0   0:00 /usr/sbin/acpid -c /etc
...
    1  4808  4808  4808 ?           -1 Ss     102   0:00 /sbin/syslogd -u syslog
...

凡是 TPGID 一栏写着 -1 的都是没有控制终端的进程,也就是守护进程。在 COMMAND 一列用 [] 括起来的名字表示内核线程,这些线程在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用以 k 开头的名字,表示 Kernel。 init 进程我们已经很熟悉了, udevd 负责维护 /dev 目录下的设备文件, acpid 负责电源管理, syslogd 负责维护 /var/log 下的日志文件,可以看出,守护进程通常采用以 d 结尾的名字,表示 Daemon。

创建守护进程最关键的一步是调用 setsid 函数创建一个新的 Session,并成为 Session Leader。

#include <unistd.h>

pid_t setsid(void);

该函数调用成功时返回新创建的 Session 的 id(其实也就是当前进程的 id),出错返回-1。注意,调用这个函数之前,当前进程不允许是进程组的 Leader,否则该函数返回-1。要保证当前进程不是进程组的 Leader 也很容易,只要先 fork 再调用 setsid 就行了。 fork 创建的子进程和父进程在同一个进程组中,进程组的 Leader 必然是该组的第一个进程,所以子进程不可能是该组的第一个进程,在子进程中调用 setsid 就不会有问题了。

成功调用该函数的结果是:

  • 创建一个新的 Session,当前进程成为 Session Leader,当前进程的 id 就是 Session 的 id。

  • 创建一个新的进程组,当前进程成为进程组的 Leader,当前进程的 id 就是进程组的 id。

  • 如果当前进程原本有一个控制终端,则它失去这个控制终端,成为一个没有控制终端的进程。所谓失去控制终端是指,原来的控制终端仍然是打开的,仍然可以读写,但只是一个普通的打开文件而不是控制终端了。

例 34.2. 创建守护进程

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>

void daemonize(void)
{
	pid_t  pid;

	/*
	 * Become a session leader to lose controlling TTY.
	 */
	if ((pid = fork()) < 0) {
		perror("fork");
		exit(1);
	} else if (pid != 0) /* parent */
		exit(0);
	setsid();

	/*
	 * Change the current working directory to the root.
	 */
	if (chdir("/") < 0) {
		perror("chdir");
		exit(1);
	}

	/*
	 * Attach file descriptors 0, 1, and 2 to /dev/null.
	 */
	close(0);
	open("/dev/null", O_RDWR);
	dup2(0, 1);
	dup2(0, 2);
}

int main(void)
{
	daemonize();
	while(1);
}

为了确保调用 setsid 的进程不是进程组的 Leader,首先 fork 出一个子进程,父进程退出,然后子进程调用 setsid 创建新的 Session,成为守护进程。按照守护进程的惯例,通常将当前工作目录切换到根目录,将文件描述符 0、1、2 重定向到 /dev/null 。Linux 也提供了一个库函数 daemon(3) 实现我们的 daemonize 函数的功能,它带两个参数指示要不要切换工作目录到根目录,以及要不要把文件描述符 0、1、2 重定向到 /dev/null

$ ./a.out
$ ps
  PID TTY          TIME CMD
11494 pts/0    00:00:00 bash
13271 pts/0    00:00:00 ps
$ ps xj | grep a.out
    1 13270 13270 13270 ?           -1 Rs    1000   0:05 ./a.out
11494 13273 13272 11494 pts/0    13272 S+    1000   0:00 grep a.out
(关闭终端窗口重新打开,或者注销重新登录)
$ ps xj | grep a.out
    1 13270 13270 13270 ?           -1 Rs    1000   0:21 ./a.out
13282 13338 13337 13282 pts/1    13337 S+    1000   0:00 grep a.out
$ kill 13270

运行这个程序,它变成一个守护进程,不再和当前终端关联。用 ps 命令看不到,必须运行带 x 参数的 ps 命令才能看到。另外还可以看到,用户关闭终端窗口或注销也不会影响守护进程的运行。