Unix网络编程(6):守护进程

        守护进程 ( $daemon$ ) 是在后台运行且不与任何控制终端关联的进程。Unix系统通常有很多守护进程在后台运行,执行不同的管理任务。守护进程没有控制终端通常源于它们由系统初始化脚本启动,然而守护进程也可能从某个终端由用户在shell提示符下输入命令行启动,这样的守护进程必须亲自脱离与控制终端的关联。

1. syslogd守护进程

        Unix系统中的 $syslogd$ 守护进程通常由某个系统初始化脚本启动,而且在系统工作期间一直运行。Linux系统的不同发行版会使用各自的实现,比如Ubuntu会使用 $rsyslog$ ,Arch使用 $journal$ 。源自 $Berkeley$ 的 $syslogd$ 实现在启动时执行以下步骤:

  1. 读取配置文件。通常为 $/etc/syslog.conf$ 的配置文件指定本守护进程可能收取的各种日志消息应该如何处理。这些消息可能被添加到一个文件,或被写到指定用户的登录窗口,或被转发给另一个主机上的 $syslog$ 进程;
  2. 创建一个Unix域数据报套接字,给它捆绑路径名 $/var/run/log$ ;
  3. 创建一个UDP套接字,给他捆绑端口 $514$ ( $syslog$ 服务使用的端口 );
  4. 打开路径名 $/dev/klog$ ,来自内核中的任何出错消息看着像是这个设备的输入。

        此后 $syslog$ 一直在一个无限循环中运行:调用 $select$ 等待 $3$ 个描述符 ( 上述第 $2$ 、$3$ 和 $4$ 步 ),读入日志消息,按照配置文件进行处理。如果接收到 $SIGHUP$ 信号,则重新读取配置文件。
        通过创建一个Unix域数据报套接字,我们就可以从自己的守护进程中通过往 $syslogd$ 绑定的路径名发送我们的消息,从而达到发送日志消息的目的。另外,我们也可以创建一个UDP套接字 ( 较新的实现为防止遭受攻击,会禁止套接字的创建 ),通过往环回地址和端口 $514$ 发送我们的消息达到发送日志消息的目的。

2. syslog函数

#include <syslog.h>

void syslog(int priority, const char *message, ...);

        从守护进程中登记消息的常用技巧就是调用 $syslog$ 函数。本函数最初是为BSD系统开发的,不过如今几乎所有Unix厂商都有提供。$priority$ 是级别 ( $level$ ) 和设施 ( $facility$ ) 两者的组合。$message$ 参数类似于 $printf$ 的格式串,不过增加了 $\%m$ 规范,将 $errno$ 值替换为对应的错误消息,可以在 $message$ 的末尾添加换行符,不过并非必需。

$level$ 说明
$LOG_-EMERG$ $0$ 系统不可用 ( 最高优先级 )
$LOG_-ALERT$ $1$ 必须立即采取行动
$LOG_-CRIT$ $2$ 临界条件
$LOG_-ERR$ $3$ 出错条件
$LOG_-WARNING$ $4$ 警告条件
$LOG_-NOTICE$ $5$ 正常然而重要的条件 ( 默认 )
$LOG_-INFO$ $6$ 通告消息
$LOG_-DEBUG$ $7$ 调试级消息 ( 最低优先级 )

$facility$ 说明
$LOG_-AUTH$ 安全/授权消息
$LOG_-AUTHPRIV$ 安全/授权消息 ( 私用 )
$LOG_-CRON$ $cron$ 守护进程
$LOG_-DAEMON$ 系统守护进程
$LOG_-FTP$ FTP守护进程
$LOG_-KERN$ 内核消息
$LOG_-LOCAL0$ 本地使用
$LOG_-LOCAL1$ 本地使用
$LOG_-LOCAL2$ 本地使用
$LOG_-LOCAL3$ 本地使用
$LOG_-LOCAL4$ 本地使用
$LOG_-LOCAL5$ 本地使用
$LOG_-LOCAL6$ 本地使用
$LOG_-LOCAL7$ 本地使用
$LOG_-LPR$ 行式打印机系统
$LOG_-MAIL$ 邮件系统
$LOG_-NEWS$ 网络新闻系统
$LOG_-SYSLOG$ 由 $syslogd$ 内部产生的消息
$LOG_-USER$ 任意的用户级消息 ( 默认 )
$LOG_-UUCP$ UUCP系统

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

#include <syslog.h>

void openlog(const char *ident, int options, int facility);

void closelog(void);

        $openlog$ 可以在首次调用 $syslog$ 前调用,$closelog$ 可以在应用程序不再需要发送日志消息时调用。$ident$ 参数是一个由 $syslog$ 冠于每个日志消息之前的字符串,值通常是程序名。$options$ 参数由一个或多个常值的逻辑或构成。

$options$ 说明
$LOG_-CONS$ 若无法发送到 $syslogd$ 守护进程则登记到控制台
$LOG_-NDELAY$ 不延迟打开,立即创建套接字
$LOG_-PERROR$ 既发送到 $syslogd$ 守护进程,由登记到标准错误输出
$LOG_-PID$ 随日志消息登记进程PID

        $openlog$ 被调用时,通常不立即创建Unix域套接字,而是直到首次调用 $syslog$ 时才打开。$openlog$ 的 $facility$ 参数为后续 $syslog$ 调用指定一个默认值,通过该参数,守护进程在后续的调用中只需要指定级别即可。
        日志消息也可以由 $logger$ 命令产生,例如在shell脚本中通过 $logger$ 命令向 $syslogd$ 发送消息。

3. daemon_init函数

        通过调用 $daemon_-init$ 函数,我们能够把一个普通进程转换为守护进程。有些Unix变体提供了一个名为 $daemon$ 的C库函数,实现类似的功能,BSDLinux均提供了 $daemon$ 函数。

#include "unp.h"
#include <syslog.h>

#define MAXFD 64

extern int daemon_proc;  // defined in error.c

int daemon_init(const char *pname, int facility) { 
  int i;
  pid_t pid;

  if ((pid = Fork()) < 0)
    return -1;
  else if (pid)
    _exit(0);  // parent terminates

  // child 1 continues

  if (setsid() < 0)
    return -1;

  Signal(SIGHUP, SIG_IGN);
  if ((pid = Fork()) < 0)
    return -1;
  else if (pid)
    _exit(0);  // child 1 terminates

  // child 2 continues

  daemon_proc = 1;  // for err_XXX() functions

  chdir("/");

  // close off file descriptors
  for (i = 0; i < MAXFD; i++)
    close(i);

  // redirect stdin, stdout and stderr to /dev/null
  open("/dev/null", O_RDONLY);
  open("/dev/null", O_RDWR);
  open("/dev/null", O_RDWR);

  openlog(pname, LOG_PID, facility);

  return 0;
}

        首先调用 $fork$ ,然后终止父进程。如果本进程是从前台作为一个shell命令启动的,那么当父进程终止时,shell就认为该命令执行完毕,从而子进程自动在后台运行。而且,子进程继承了父进程的进程组ID,从而保证它不是一个进程组的头进程,于是可以调用 $setsid$ ,创建一个新会话,当前进程变为新会话的会话头进程以及新进程组的进程组头进程,从而不再有控制终端。之后忽略 $SIGHUP$ 信号并再次 $fork$ ,目的是确保守护进程将来即使打开了一个终端设备,也不会自动获得控制终端,因为它不是会话头进程。之后会关闭本守护进程从执行它的进程 ( 通常是shell ) 继承而来的所有打开着的描述符。问题是怎样检测正在使用的最大描述符:没有现成的Unix函数提供该值。我们的解决办法是直接关闭前 $64$ 个描述符,即使其中大部分没有打开。最后打开 $/dev/null$ 作为本守护进程的标准输入、标准输出和标准错误输出。这保证了这些描述符处于打开状态,而且针对它们的 $read$ 会返回EOF,$write$ 数据则由内核丢弃,从而防止对守护进程的这些调用的失败。

4. inetd守护进程

        典型的Unix系统可能存在很多服务器,只是等待客户请求的到达,例如FTPTelnet等。4.3BSD面世之前的系统,所有这些服务都有一个进程与之关联。这些进程都是在系统自举阶段从 $/etc/rc$ 文件中启动,而且每个进程执行几乎相同的启动任务:创建一个套接字,把本服务器众所周知的端口捆绑到该套接字,等待一个连接或一个数据报,然后派生子进程。子进程为客户提供服务,父进程则继续等待下一个客户请求。这个模型存在两个问题:

  1. 所有这些守护进程含有几乎相同的启动代码,既表现在创建套接字上,也表现在转变为守护进程上;
  2. 每个守护进程在进程表中占据一个表项,然而它们大部分时候处于休眠状态。

        4.3BSD通过提供一个因特网超级服务器 ( 即inetd守护进程 ) 简化了上述问题:

  1. 通过由inetd处理普通守护进程的大部分启动细节以简化守护程序的编写;
  2. 单个进程 ( inetd ) 就能为多个服务等待外来的客户请求,从而取代每个服务一个进程的做法,减少系统中的进程总数。

        inetd进程首先将自己变为一个守护进程,接着会读入并处理自己的配置文件,通常是 $/etc/inetd.conf$ 的配置文件,指定本超级服务器处理哪些服务以及当一个服务请求到达时该怎么做。

字段 说明
$service-name$ 必须在 $/etc/services$ 文件中定义
$socket-type$ $stream$ ( TCP ) 或 $dgram$ ( UDP )
$protocol$ 必须在 $/etc/protocols$ 文件中定义:TCPUDP
$wait-flag$ 对于TCP一般为 $nowait$ ,对于UDP一般为 $wait$
$login-name$ 来自 $/etc/passwd$ 的用户名,一般为 $root$
$server-program$ 调用 $exec$ 指定的完整路径名
$server-program-arguments$ 调用 $exec$ 指定的命令行参数

        当inetd调用 $exec$ 执行某个服务器程序时,该服务器的真实名字总是作为程序的第一个参数传递。
        inetd守护进程的工作流程如下:

  1. 在启动阶段,读入配置文件并给该文件中指定的每个服务创建一个适当类型 ( 字节流或数据报 ) 套接字。inetd能够处理的服务器的最大数目取决于其能创建的描述符的最大数量。新创建的每个套接字都被加入将来由 $select$ 调用使用的一个描述符集中;
  2. 为每个套接字调用 $bind$ ,指定捆绑相应服务器的众所周知端口和通配地址。这个TCPUDP端口号通过 $getservbyname$ 获得,作为函数参数的是配置文件中的 $service-name$ 字段和 $protocol$ 字段;
  3. 对于每个TCP套接字,调用 $listen$ 以接收外来的连接请求,数据报套接字则不执行;
  4. 创建完毕所有套接字后,调用 $select$ 等待其中任何一个套接字变为可读;
  5. 如果可读的套接字是TCP套接字,并且服务器的 $wait-flag$ 值为 $nowait$ ,那么就调用 $accept$ ;
  6. inetd守护进程调用 $fork$ ,交由子进程处理服务请求。子进程会关闭除要处理的套接字描述符之外的所有描述符,然后调用 $exec$ 执行由相应的 $server-program$ 字段指定的程序来具体处理请求,相应的 $server-program-arguments$ 字段则作为命令行参数传递;
  7. 如果第 $5$ 步中返回的是字节流套接字,那么父进程必须关闭已连接套接字,再次调用 $select$ 等待下一个可读套接字。

        给一个数据报服务指定 $wait$ 标志导致父进程执行的步骤发生变化,这个标志要求inetd必须在这个套接字再次成为 $select$ 调用的候选套接字之前等待为当前服务套接字创建的子进程的终止,发生的变化有:

  1. $fork$ 返回到父进程时,父进程保存子进程PID,从而之后可以通过 $waitpid$ 返回值确定子进程终止时间;
  2. 父进程通过 $FD_-CLR$ 宏关闭这个套接字在 $select$ 所用描述符集中对应的位,从而在将来的 $select$ 调用中禁止该套接字;
  3. 子进程终止时,父进程会收到 $SIGCHILD$ 信号,父进程通过打开相应的套接字在 $select$ 所用描述符集中对应的位,使得该套接字再次成为 $select$ 的候选套接字。

        inetd通常不适用于服务密集型服务器,通常有邮件服务器和Web服务器。

Unix网络编程(6):守护进程