[关闭]
@FunC 2018-05-20T10:42:03.000000Z 字数 2767 阅读 1999

UNP CH13 守护进程与 inetd 超级服务器

unix


守护进程(daemon)是在后台运行且不与任何控制终端关联的进程。

守护进程有多种启动方法:
* 在系统启动阶段,由系统初始化脚本启动
* 许多网络服务器由 inetd 超级服务器启动
* cron 守护进程按照规则定期执行一些程序,由它启动执行的程序同样作为守护进程运行
* at 命令用于指定将来某个时刻的程序执行
* 从用户终端或在前台或在后台启动(这种做法通常用于测试)

因为守护进程没有控制终端,当有事件发生时需要由输出消息的某种方法可用。 syslog 函数是输出这些消息的标准方法,它把这些消息发送给 syslogd 守护进程

syslogd 守护进程

Unix 系统中的 syslogd 守护进程通常由某个系统初始化脚本启动,而且在系统工作期间一直运行,用于接受并处理日志信息。

启动时执行以下步骤:
1. 读取配置文件
2. 创建一个 Unix 域数据报套接字,给他捆绑路径名 /var/run/log
3. 创建一个 UDP 套接字, 给它捆绑端口 514 (syslog 服务使用的端口)
4. 打开路径名 /dev/klog , 来自内核中的任何出错信息看着像是这个设备的输入

之后 syslogd 守护进程在一个无限循环中运行: 调用 select 以等待它的三个描述符之一变为可读,并按照配置文件进行处理。如果收到 SIGHUP 信号,则重新读取配置文件。

syslog 函数

因为守护进程没有控制终端,所以它们不能把消息 fprintf 到 stderr 上。从守护进程中登记消息的常用技巧就是调用 syslog 函数

  1. #include <syslog.h>
  2. void syslog(int priority, const char *message, ...);

其中 priority 参数是 级别(level)和 设施(facility)两者的结合。message 参数类似 printf 的格式串,增设了%m规范,它将被替换成与当前 errno 值对应的出错信息:
syslog(LOG_INFO | LOG_LOCAL2, "rename(%s, %s): %m", file1, fil2)

当 syslog 被应用进程首次调用时,它创建一个 Unix 域数据报套接字,然后调用 connect 连接到由 syslogd 守护进程创建的 Unix 域数据报套接字的众所周知路径名。这个套接字一直保持打开,直到进程终止为止。作为替换,进程也可以调用 openlog 和 closelog:

  1. #include <syslog.h>
  2. void openlog(const char *ident, int options, int facility);
  3. void closelog(void);

openlog 可以在首次调用 syslog 前调用, closelog 可以在应用进程不再需要发送日志消息时调用。

daemon_init 函数

讨论完守护进程输出消息的方法,我们是时候来看看怎么把一个普通进程变为守护进程了。

如果只是让前台任务变为后台任务,那我们只需要在命令的尾部加上符号& 即可:
node server.js &

但这样做只是变为后台任务,并不是变成守护进程。它继承了当前 session 的标准输出和标准错误,并且不继承标准输入。同时还会在用户退出 session 时因接受到 SIGHUP 信号而退出。这样看来,将普通进程转变为守护进程还是有很多细节要考虑的。

我们来看一下 daemon_init 函数是怎么完成这个任务的:

  1. #include "unp.h"
  2. #include <syslog.h>
  3. #define MAXFD 64
  4. extern int daemon_proc; /* defined in error.c */
  5. int daemon_init(const char *pname, int facility)
  6. {
  7. int i;
  8. pid_t pid;
  9. if ( (pid = Fork()) < 0)
  10. return (-1);
  11. else if (pid)
  12. _exit(0); /* 终止父进程 */
  13. /* 下面由子进程 1 执行 */
  14. // 因为子进程继承了父进程的进程组ID,故不是当前进程组的头进程,可以调用setsid
  15. if (setsid() < 0) /* 成为会话的头进程 */
  16. return (-1);
  17. // 会话头进程终止时,会话中的所有进程都会收到 SIGHUP 信号然后终止
  18. Signal(SIGHUP, SIG_IGN); /* 屏蔽SIGHUP信号 */
  19. if ( (pid = Fork()) < 0) /* 再次fork,避免头进程自动获得控制终端的问题 */
  20. return (-1);
  21. else if (pid)
  22. _exit(0); /* 终止子进程 1 */
  23. /* 下面由子进程 2 执行 */
  24. daemon_proc = 1; /* for err_XXX() functions */
  25. chdir("/"); /* 修改工作目录,避免无法 unmount */
  26. // 因为fork会继承fd, 关闭所有的文件描述符,包括stdin,stdout,stderr
  27. for (i = 0; i < MAXFD; i++)
  28. close(i);
  29. // 将stdin, stdout, stderr 重定向到 /dev/null
  30. open("/dev/null", O_RDONLY); /* fd依次为0,1,2 */
  31. open("/dev/null", O_RDWR);
  32. open("/dev/null", O_RDWR);
  33. openlog(pname, LOG_PID, facility); /* 调用openlog,将日志发送给syslogd */
  34. return (0); /* success */
  35. }

大致步骤为:调用 fork 以转到后台运行,调用 setsid 建立一个新的 POSIX 会话并成为会话头进程,再次 fork 以避免无意中获得新的控制终端,改变工作目录和文件创建模式掩码,最后关闭所有非必要的描述符。

inetd 守护进程

Inter 守护进程作为一个超级服务器,用单个进程为多个服务等待外来的客户请求,以此取代每个服务一个进程的做法,减少了系统中的进程总数。

下面是 inetd 守护进程的工作流程:
核心是使用 select 等待多个套接字描述符,之后使用 exec 执行服务器程序

需要注意的是,fork 出子进程处理服务请求时,子进程调用 dup2 三次,把这个待处理套接字的描述符复制到描述符0、1和2,然后关闭原套接字描述符。

因为替一个 TCP 服务器调用 accept 的进程是 inetd,由 inetd 启动的真正服务器通常通过调用 getpeername 获取客户的 IP 地址和端口号。

inetd 通常不适用于服务密集型的服务器(如Web 服务器),相比于标准的并发服务器只有一个 fork 的开销,inetd 启动的服务器开销为一个 fork + exec

Node 相关

对于 Node 应用,有不少优秀的 npm 包可供使用。
如简单的用于文件变化时自动重启进程的 nodemon
保证进程退出时能自动重启,同时能作为服务进程启动的 forever
以及功能强大,甚至能实时收集日志和监控的 pm2

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注