《网络编程》守护进程

前言

        守护进程是独立在后台执行和终端控制的所有进程。

DAEMON没有控制终端,由于其通常是由系统初始化脚本启动,但是,有可能根据由用户终端 shell 提示符下,键入命令行启动。护进程必须亲自脱离与控制终端的关联,从而避免与作业控制、终端会话管理、终端产生信号等发生不论什么不期望的交互。也能够避免在后台执行的守护进程非预期地输出到终端。有关作业控制、终端控制的内容可參考文章《作业控制、终端控制 和 守护进程

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


syslogd 守护进程

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

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

    来自内核中的不论什么出错消息觉得是这个设备的输入;

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

  1. 内核例程能够调用 log 函数。不论什么一个用户进程通过打开(open)然后读(read)/dev/klog 设备就能够读这些消息;
  2. 大多数用户进程(守护进程)调用 syslog 函数以产生日志消息。并使日志消息发送至 Unix 域数据报套接字 /dev/log;
  3. 在此主机上的一个用户进程。或通过 TCP/IP 网络连接到此主机的其它主机上的一个用户进程可将日志消息发送到 UDP port 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 函数。指定捆绑对应server的port和通配地址。
  3. 对于每一个 TCP 套接字,调用 listen 函数以接受外来的连接请求;
  4. 创建全部套接字后,调用 select 函数等待当中不论什么一个套接字变为可读。
  5. 当 select 返回指出某个套接字已可读之后,若是 TCP 套接字,且server的 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 网络编程》

版权声明:本文博客原创文章,博客,未经同意,不得转载。

原文地址:https://www.cnblogs.com/bhlsheji/p/4618559.html