守护进程实现步骤

本文通过将一个演示服务改造成守护守护进程,以此介绍 实现守护进程的必要步骤 。演示服务每隔 10 秒打印一条 syslog 日志:

  1. #include <syslog.h>
  2. #include <unistd.h>
  3. void echo_forever(char *ident, char *msg)
  4. {
  5. openlog(ident, (LOG_CONS | LOG_PID), LOG_USER);
  6. setlogmask(LOG_UPTO(LOG_INFO));
  7. for(;;) {
  8. syslog(LOG_ERR, "%s\n", msg);
  9. sleep(10);
  10. }
  11. }
  12. int main(int argc, const char *argv[])
  13. {
  14. echo_forever("echo-forever", "Hello, world!");
  15. }

为了将服务改造成守护进程,需要实现一个函数 daemonize

  1. int daemonize();

该函数负责初始化守护进程,通过返回值可以判断执行是否成功:

返回值
返回值状态
0成功,且处于守护进程(子进程)上下文
-1失败,错误信息见error

这样一来,只需在服务入口先调用 daemonize 函数即可完成守护进程初始化:

  1. int main(int argc, const char *argv[])
  2. {
  3. if (daemonize() == 0) {
  4. echo_forever("echo-forever", "Hello, world!");
  5. }
  6. }

编译并启动守护服务:

  1. $ make all
  2. $ ./echod
  3. $

注意到,运行 echod 后,程序立马返回了。通过 ps 命令,可以确定 echod 已经作为守护进程在后台运行了:

  1. $ ps aux | grep echod
  2. fasion 30807 1.0 0.0 4352 80 ? S 20:17 0:00 ./echod
  3. fasion 30838 0.0 0.0 14224 932 pts/11 S+ 20:17 0:00 grep --color=auto echod

syslog 日志也可以看到服务不断打印出来的信息:

  1. $ tail -f /var/log/syslog
  2. Jan 9 20:19:10 ant echo-forever[30807]: Hello, world!
  3. Jan 9 20:19:40 ant echo-forever[30807]: message repeated 3 times: [ Hello, world! ]
  4. Jan 9 20:19:50 ant echo-forever[30807]: Hello, world!

实现步骤

回头来看看 daemonize 函数的实现,这是本文的重点——初始化守护进程的关键步骤:

  1. #include <fcntl.h>
  2. #include <signal.h>
  3. #include <stdlib.h>
  4. #include <sys/resource.h>
  5. #include <sys/stat.h>
  6. #include <unistd.h>
  7. int
  8. daemonize()
  9. {
  10. // fork to run in background
  11. int pid = fork();
  12. if (pid == -1) {
  13. return -1;
  14. }
  15. else if (pid != 0) {
  16. exit(0);
  17. }
  18. // become session leader to lose controlling TTY
  19. setsid();
  20. // fork again in order to forbid reallocating new controlling TTY
  21. pid = fork();
  22. if (pid == -1) {
  23. return -1;
  24. }
  25. else if (pid != 0) {
  26. exit(0);
  27. }
  28. // get maximun number of file descriptors
  29. struct rlimit rl;
  30. if (getrlimit(RLIMIT_NOFILE, &rl) < 0) {
  31. return -1;
  32. }
  33. if (rl.rlim_max == RLIM_INFINITY) {
  34. rl.rlim_max = 1024;
  35. }
  36. // close all open file descriptors
  37. for (int i=0; i<rl.rlim_max; i++) {
  38. close(i);
  39. }
  40. // reopen stdin, stdout and stderr
  41. int stdin = open("/dev/null", O_RDWR);
  42. int stdout = dup(stdin);
  43. int stderr = dup(stdin);
  44. if (stdin != 0 || stdout != 1 || stderr != 2) {
  45. return -1;
  46. }
  47. // change the current working directory to root
  48. // in order not to prevent file system from being umounted
  49. if (chdir("/") < 0) {
  50. return -1;
  51. }
  52. // clear file creation mask
  53. umask(0);
  54. // set up signal handler
  55. struct sigaction sa;
  56. sa.sa_handler = SIG_IGN;
  57. sigemptyset(&sa.sa_mask);
  58. sa.sa_flags = 0;
  59. if (sigaction(SIGHUP, &sa, NULL) == -1) {
  60. return -1;
  61. }
  62. return 0;
  63. }

  • 12-18 行,调用 fork 系统调用派生子进程,以便实现:①如果守护进程由 shell 启动,父进程退出会让 shell 认为命令执行完毕;②子进程虽继承了父进程的进程组 ID ,但获得新的进程 ID ,保证子进程不是组长进程,为调用 setsid 做准备。
  • 21 行,调用 setsid 创建新会话,使调用进程:①成为新会话首进程;②新进程组组长;③没有控制终端(脱离控制终端)。
  • 再次调用 fork 并终止父进程,保证守护进程(子进程)不是会话首进程,防止它再次取得控制终端。
  • 42-44 行,关闭所有文件描述符,请注意进程文件描述符范围是如何确定的。
  • 47-52* 行,重新打开 标准输入 ( 0 )、 标准输出 ( 1 )以及 标准错误 ( 2 ),并将其重定向到 /dev/null 上。这样一来,任何试图访问标准输入输出的程序代码均不会产生任何效果。
  • 56-58 行,将当前工作目录更改为根目录,避免有文件系统因守护进程而无法卸载。
  • 61 行,调用 umask 将文件模式屏蔽位设置为一个已知值,一般为 0 。因为继承而来的屏蔽位,可能设置为屏蔽某些权限。
  • 64-70 行,设置信号处理函数,忽略信号 SIGHUP

下一步

订阅更新,获取更多学习资料,请关注我们的 微信公众号

../../_images/wechat-mp-qrcode.png小菜学编程

微信打赏