《网络编程》守护进程

前言

        守护进程是在后台运行并独立于所有终端控制的进程。守护进程没有控制终端源于它们通常是由系统初始化脚本启动,但是也有可能从某个终端由用户在 shell 提示符下键入命令行启动,这种启动方式的守护进程必须亲自脱离与控制终端的关联,从而避免与作业控制、终端会话管理、终端产生信号等发生任何不期望的交互,也可以避免在后台运行的守护进程非预期地输出到终端。有关作业控制、终端控制的内容可参考文章《作业控制、终端控制 和 守护进程

        由于守护进程没有控制终端,当守护进程出错时,必须通过某种输出函数输出错误消息,而不能使用标准输出函数。syslog 函数是输出这些消息的标准方法,它把这些消息发送给 syslogd 守护进程。


syslogd 守护进程

Unix 系统中的 syslogd 守护进程通常由某个系统初始化脚本启动,而且在系统工作期间一直运行。其启动步骤如下:

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

以下是守护进程产生日志消息的方式:

  1. 内核例程可以调用 log 函数。任何一个用户进程通过打开(open)然后读(read)/dev/klog 设备就可以读这些消息;
  2. 大多数用户进程(守护进程)调用 syslog 函数以产生日志消息,并使日志消息发送至 Unix 域数据报套接字 /dev/log;
  3. 在此主机上的一个用户进程,或通过 TCP/IP 网络连接到此主机的其他主机上的一个用户进程可将日志消息发送到 UDP 端口 514;




syslog 函数

由于守护进程没有控制终端,所以不能把错误日志消息 fprintf 到 stderr 上,但是可以使用 syslog 函数进行处理日志消息。其定义如下:

/* 守护进程 */

#include <syslog.h>

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

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

void closelog(void);

/*
 * 说明:
 * 函数openlog和closelog是可选的,若不调用openlog函数,则在第一次调用syslog函数时,会自动调用openlog函数;
 * openlog可以在首次调用syslog前调用,closelog可以在应用进程不再需要发送日志消息时调用;
 * ident参数是一个由syslog冠于每个日志消息之前的字符串,通常是程序名;
 * ******************************************************************
 * options参数由以下值一个或多个逻辑“或”组成:
 * ******************************************************************
 * (1)LOG_CONS    若无法发送到syslogd守护进程,则将消息登记到控制台
 * (2)LOG_NDELAY  不延迟打开,立即创建套接字
 * (3)LOG_PERROR  既发送到syslogd守护进程,又登记到标准错误输出
 * (4)LOG_PID     每条日志消息都登记进程ID
 * (5)LOG_NOWAIT  不等待在消息记入日志过程中可能已创建的子进程
 * (6)LOG_ODELAY  在记录第一条消息之前延迟打开至syslogd守护进程的连接
 *********************************************************************
 * 参数 priority是级别(level)和设施(facility)两者的组合;
 * level和facility的目的在于允许在/dev/syslog.conf文件中统一配置来自同一个给定设施的所有消息,
 * 或统一配置具有相同级别的所有消息;
 * *********************************************************
 * 其中level取值如下:(注:以下按优先级从高到低)
 * *********************************************************
 * LOG_EMERG    系统不可使用
 * LOG_ALERT    必须立即采取修复
 * LOG_CRIT     临界条件
 * LOG_ERR      出错条件
 * LOG_WARNING  警告条件
 * LOG_NOTICE   正常然而重要的条件(默认值)
 * LOG_INFO     通告消息
 * LOG_DEBUG    调试级消息
 ***********************************************************
 ***********************************************************
 * 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_MALL     邮件系统
 * LOG_NEWS     网络新闻系统
 * LOG_SYSLOG   由syslogd内部产生的消息
 * LOG_USER     任意用户级消息(默认)
 * LOG_UUCP     UUCP系统
 * *********************************************************
 */

       当 syslog 函数被应用进程首次调用时,它创建一个 Unix 域数据报套接字,然后调用 connect 函数连接到由 syslogd 守护进程创建的 Unix 域数据报套接字的路径名(例如 /var/run/log)。这个套接字一直打开,直到进程终止为止。

       当 openlog 函数被应用程序调用时,通常并不立即创建 Unix 域套接字,直到首次调用 syslog 函数时套接字才被创建并打开。但是 openlog 函数可以指定选项 LOG_NDELAY 要求调用 openlog 函数时立即创建套接字。


守护进程编程

守护进程编程步骤
  1. 创建子进程,父进程退出
    ?所有工作在子进程中进行
    ?形式上脱离了控制终端
  2. 在子进程中创建新会话
    ?setsid()函数
    ?使子进程完全独立出来,脱离控制
  3. 改变当前目录为根目录
    ?chdir()函数
    ?防止占用可卸载的文件系统
    ?也可以换成其它路径
  4. 重设文件权限掩码
    ?umask()函数
    ?防止继承的文件创建屏蔽字拒绝某些权限
    ?增加守护进程灵活性
  5. 关闭文件描述符
    ?继承的打开文件不会用到,浪费系统资源,无法卸载
    ?返回所在进程的文件描述符表的项数,即该进程打开的文件数目


守护进程创建的流程图如下:




/* 初始化守护进程 */
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <syslog.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/resource.h>

extern void err_quit(const char *,...);
void daemonize(const char *pname, int facility)
{
    int i, fd0, fd1, fd2;
    pid_t pid;
    struct rlimit rl;
    struct sigaction sa;
    umask(0);

    /* 限制进程资源,参数RLIMIT_NOFILE指定进程不能超过资源最大数 */
    if(getrlimit(RLIMIT_NOFILE, &rl) < 0)
        err_quit("%s: can't get file limit", pname);

    /* 创建子进程*/
    if((pid =fork()) < 0)
        err_quit("%s: can't fork", pname);
    else if(pid != 0)
        exit(0);/* 父进程退出 */
    setsid();/* 创建会话 */

    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if(sigaction(SIGHUP, &sa, NULL) < 0)
        err_quit("%s: can't ignore SIGHUP");
    if((pid =fork()) < 0)
        err_quit("%s: can't fork", pname);
    else if(pid != 0)
        exit(0);
    /* 把工作目录改为根目录 */
    if(chdir("/") < 0)
        err_quit("%s: can't change directory to /");

    if(rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;
    /* 关闭所有打开的描述符 */
    for(i = 0; i < (int)rl.rlim_max; i++)
        close(i);

    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);

    /* 使用syslogd处理错误 */
    openlog(pname, LOG_CONS, facility);
    if(fd0 != 0 || fd1 != 1 || fd2 != 2)
    {
        syslog(LOG_ERR, "unexpectrd file descriptors %d %d %d", fd0, fd1, fd2);
        exit(1);
    }

}

上面守护进程编程的具体步骤是:

  1. 首先限制守护进程的资源,然后调用 fork 创建子进程,接着终止父进程,留下子进程继续运行。若本进程是从前台作为一个 shell 命令启动的,当父进程终止时,shell 就认为该命令执行完毕。这样子进程就自动在后台运行。另外,子进程继承了父进程的进程组 ID,但是子进程也有自己的进程 ID,这保证子进程不是一个进程组的首进程,所以必须调用 setsid 函数使子进程称为进程组的首进程;
  2. 调用 setsid 函数创建一个新会话。当前进程变为新会话的会话首进程以及新进程组的进程组首进程,从而不再有控制终端;
  3. 调用 sigaction 函数忽略 SIGHUP 信号,并再次调用 fork 函数,该函数返回时,父进程实际上是上一次调用 fork 产生的子进程,它被终止,留下新的子进程继续运行。再次调用 fork 的目的是确保守护进程将来即使打开一个终端设备时,也不会自动获得控制终端。当没有控制终端的一个会话首进程打开一个设备时,该终端自动成为这个会话首进程的控制终端。然而再次调用 fork 之后,确保新的子进程不再是一个会话首进程,从而不能自动获得一个控制终端。当会话首进程(即首次 fork 产生的子进程)终止时,其会话中的所有进程(即再次 fork 产生的子进程)都会收到 SIGHUP 信号,所以必须调用 sigaction 函数忽略该信号;
  4. 把工作目录改为根目录。因为守护进程可能在某一个任意文件系统中启动,若不改变工作目录,那么文件系统将无法拆卸;
  5. 关闭本守护进程所有打开的描述符;
  6. 使用 syslogd 处理错误;

inetd 守护进程


inetd 守护进程的特点:
  1. 简化守护进程程序的编写;
  2. 单个进程就能为多个服务等待外来的客户请求;
以下是 inetd 守护进程的工作流程:



inetd 守护进程工作步骤:
  1. 启动阶段,读入/etc/inetd.conf 文件并给该文件中指定的每个服务创建一个适当类型(字节流或数据报)的套接字。新创建的每个套接字都被加入到将由某个 select 调用使用的一个描述符集里面;
  2. 为每个套接字调用 bind 函数,指定捆绑相应服务器的端口和通配地址;
  3. 对于每个 TCP 套接字,调用 listen 函数以接受外来的连接请求;
  4. 创建所有套接字后,调用 select 函数等待其中任何一个套接字变为可读;
  5. 当 select 返回指出某个套接字已可读之后,若是 TCP 套接字,且服务器的 wait-flag 值为 nowait,则调用 accept 接受这个新连接;
  6. inetd 守护进程调用 fork 派生进程,并由子进程处理服务请求;
  7. 子进程关闭除要处理的套接字描述符之外的所有描述符;
以下函数是守护进程化由inetd运行的进程,相对于上面的守护进程编程要简单。
#include	"unp.h"
#include	<syslog.h>

extern int	daemon_proc;	/* defined in error.c */

void
daemon_inetd(const char *pname, int facility)
{
	daemon_proc = 1;		/* for our err_XXX() functions */
	openlog(pname, LOG_PID, facility);
}



参考资料:
《Unix 网络编程》

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。