3. 守护进程

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

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

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

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

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

  1. #include <unistd.h>
  2.  
  3. 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. 创建守护进程

  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <fcntl.h>
  4.  
  5. void daemonize(void)
  6. {
  7. pid_t pid;
  8.  
  9. /*
  10. * Become a session leader to lose controlling TTY.
  11. */
  12. if ((pid = fork()) < 0) {
  13. perror("fork");
  14. exit(1);
  15. } else if (pid != 0) /* parent */
  16. exit(0);
  17. setsid();
  18.  
  19. /*
  20. * Change the current working directory to the root.
  21. */
  22. if (chdir("/") < 0) {
  23. perror("chdir");
  24. exit(1);
  25. }
  26.  
  27. /*
  28. * Attach file descriptors 0, 1, and 2 to /dev/null.
  29. */
  30. close(0);
  31. open("/dev/null", O_RDWR);
  32. dup2(0, 1);
  33. dup2(0, 2);
  34. }
  35.  
  36. int main(void)
  37. {
  38. daemonize();
  39. while(1);
  40. }

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

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

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