《APUE》读书笔记—第十三章守护进程

简介:

  守护进程也称为精灵进程是一种生存期较长的一种进程。它们独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。他们常常在系统引导装入时启动,在系统关闭时终止。unix系统有很多守护进程,大多数服务器都是用守护进程实现的,例如inetd守护进程。

1、守护进程的特征

用ps命令察看一些常用的系统守护进程,看一下他们和几个概念:进程组、控制终端和会话有什么联系。执行: ps –axj ,结果如下所示:

从结果可以看出守护进程没有控制终端,其终端名设置为?,终端前台进程组ID设置为-1,init进程ID为1。系统进程依赖于操作系统实现,父进程ID为0的各进程通常是内核进程,它们作为系统自举的一部分而启动。内核进程以超级用户特权运行,无控制终端,无命令行。大多数守护进程的父进程是init进程。

 守护进程与后台进程的区别:(1) 后台运行程序,即加&启动的程序,(2)后台运行的程序拥有控制终端,守护进程没有。

2、守护进程编程规则

(1)调用umask将文件模式创建屏蔽字设置为0。因为进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:调用umask(0)。

(2)调用fork,然后使父进程退出。这样可避免挂起控制终端将Daemon放入后台执行。

(3)调用setsid以创建一个新会话。这样可以使得调用进程成为新会话的首进程,成为一个新进程组的组长进程,没有控制终端。

(4)将当前工作目录更改为根目录。进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmpchdir("/") 。

(5)关闭不再需要的文件描述符。进程从父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。

(6)某些守护进程打开/dev/null使其具有文件描述符0、1和2。使得任何一个试图读标准输入、写标准输出或者标准出错的历程都不会产生任何效果。

(7)处理SIGCHLD信号 。处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。

初始化一个守护进程的程序如下:

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <fcntl.h>
#include <syslog.h>
#include <sys/resource.h>

void daemonize(const char *cmd)
{
    int                 i,fd0,fd1,fd2;
    pid_t               pid;
    struct rlimit       r1;
    struct sigaction    sa;
    umask(0);
    //获取文件描述符最大值
    getrlimit(RLIMIT_NOFILE,&r1);
    //创建子进程
    if((pid = fork()) < 0)
    {
         perror("fork() error");
         exit(0);
    }
    else if(pid > 0)  //使父进程退出
        exit(0);
    setsid();  //创建会话
   //创建子进程避免获取终端
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGHUP,&sa,NULL);
    if((pid = fork()) < 0)
    {
         perror("fork() error");
         exit(0);
    }
    else if(pid > 0)
        exit(0);
   //修改目录
    chdir("/");
    //关闭不需要的文件描述符
    if(r1.rlim_max == RLIM_INFINITY)
        r1.rlim_max = 1024;
    for(i=0;i<r1.rlim_max;++i)
        close(i);
    //打开文件描述符
    fd0 = open("/dev/null",O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);
    openlog(cmd,LOG_CONS,LOG_DAEMON);
    if(fd0 != 0 || fd1 != 1 || fd2 != 2)
    {
        syslog(LOG_ERR,"unexpected file descriptors %d %d %d",fd0,fd1,fd2);
        exit(1);
    }
}

int main()
{
    daemonize("ls");
    sleep(30);  //主进程休眠,以便查看守护进程状态
    exit(0);
}
复制代码

 第一次调用fork的目的是保证调用setsid的调用进程不是进程组长。(而setsid函数是实现与控制终端脱离的唯一方法);setsid函数使进程成为新会话的会话头和进程组长,并与控制终端断开连接;第二次调用fork的目的是:即使守护进程将来打开一个终端设备,也不会自动获得控制终端。(因为在SVR4中,当没有控制终端的会话头进程打开终端设备时,如果这个终端不是其他会话的控制终端,该终端将自动成为这个会话的控制终端),这样可以保证这次生成的进程不再是一个会话头。忽略SIGHUP信号的原因是,当第一次生成的子进程(会话头)终止时,该会话中的所有进程(第二次生成的子进程)都会收到该信号。

 程序执行结果,输入ps -axj命令查看守护进程的信息:

3、出错记录

  守护进程没有控制终端,不能将错误写到标准输错上。大多数进程使用集中的守护进程出错syslog设施,该设施的接口是syslog函数,原型如下:
#include <syslog.h> 
  void openlog(const char *ident, int option, int facility);
  void syslog(int priority, const char *format, ...);
  void closelog(void); 
  int setlogmask(int mask);
#include <stdarg.h> 
void vsyslog(int priority, const char *format, va_list ap);

大多数syslog实现将使消息多时间处于队列中,如果在此时间中到达了重复消息,那么syslog守护进程将不把它写到日志记录中,而是打印输出重复消息。

4、单实例守护进程

  为了正常运作,某些守护进程实现为单实例,即在任一时刻只运行该守护进程的一个副本。采用文件锁和记录锁机制可以实现单实例守护进程,如果每一个守护进程创建一个文件,并且在整个文件上加上一把锁,那就只允许创建一把这样的写锁,之后试图再创建这样的一把写锁将会失败。这样就保证守护进程只有一个副本在运行。使用文件和记录锁保证只运行某守护进程的一个副本,守护进程的每个副本都试图创建一个文件,并将其进程ID写到该文件中。程序如下:

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <syslog.h>
#include <sys/stat.h>

#define LOCKFILE "/var/run/daemon.pid"
#define LOCKMODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH )
extern int lockfile(int);

int already_running(void)
{
    int     fd;
    char    buf[16];
    //打开文件,不存在则创建
    fd = open(LOCKFILE,O-RDWR|O_CREAT,LOCKMODE);
    if(fd < 0)
    {
        syslog(LOG_ERR,"can't open %s : %s",LOCKFILE,strerror(errno));
        exit(1);
    }
    //对文件加锁
    if(lockfile(fd)<0)
    {
        if(errno == EACCES | errno == EAGAIN)
        {
            close(fd);
            return 1;
        }
        syslog(LOG_ERR,"can,t lock %s : %s",LOCKFILE,strerror(errno));
        exit(1);
    }
    ftruncate(fd,0); //将文件长度截短为0
    sprintf(buf,"%ld",(long)getpid());
    write(fd,buf,strlen(buf)+1);
    return 0;
}
复制代码

 5、守护进程的惯例

(1)若守护进程使用锁文件,那么该文件通常存放在/var/run目录中。

(2)若守护进程支持配置选项,那么配置文件通常存放在/etc中目录中。

(3)守护进程可以用命令行启动,通常是系统初始化脚本。

(4)若一守护进程有一配置文件,那么当该守护进程启动时,读取该文件,此后一把不会在查看它。

使用sigwait及多线程实现守护进程重读配置文件程序如下:

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <syslog.h>
#include <sys/stat.h>
#include <sys/resource.h>

#define LOCKFILE "/var/run/daemon.pid"
#define LOCKMODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH )

sigset_t mask;
int lockfile(int fd)
{
    struct flock f1;
    f1.l_type = F_WRLCK;
    f1.l_start = 0;
    f1.l_whence = SEEK_SET;
    f1.l_len = 0;
    return fcntl(fd,F_SETLK,&f1);
}
int already_running(void)
{
    int     fd;
    char    buf[16];

    fd = open(LOCKFILE,O_RDWR|O_CREAT,LOCKMODE);
    if(fd < 0)
    {
        syslog(LOG_ERR,"can't open %s : %s",LOCKFILE,strerror(errno));
        exit(1);
    }
    if(lockfile(fd)<0)
    {
        if(errno == EACCES | errno == EAGAIN)
        {
            close(fd);
            return 1;
        }
        syslog(LOG_ERR,"can,t lock %s : %s",LOCKFILE,strerror(errno));
        exit(1);
    }
    ftruncate(fd,0);
    sprintf(buf,"%ld",(long)getpid());
    write(fd,buf,strlen(buf)+1);
    return 0;
}

void daemonize(const char *cmd)
{
    int                 i,fd0,fd1,fd2;
    pid_t               pid;
    struct rlimit       r1;
    struct sigaction    sa;
    umask(0);
    getrlimit(RLIMIT_NOFILE,&r1);
    if((pid = fork()) < 0)
    {
         perror("fork() error");
         exit(0);
    }
    else if(pid > 0)
        exit(0);
    setsid();
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGHUP,&sa,NULL);
    if((pid = fork()) < 0)
    {
         perror("fork() error");
         exit(0);
    }
    else if(pid > 0)
        exit(0);
    chdir("/");
    if(r1.rlim_max == RLIM_INFINITY)
        r1.rlim_max = 1024;
    for(i=0;i<r1.rlim_max;++i)
        close(i);
    fd0 = open("/dev/null",O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);
    openlog(cmd,LOG_CONS,LOG_DAEMON);
    if(fd0 != 0 || fd1 != 1 || fd2 != 2)
    {
        syslog(LOG_ERR,"unexpected file descriptors %d %d %d",fd0,fd1,fd2);
        exit(1);
    }
}

void reread()
{
    printf("read daemon config file again.\n");
}
void * thread_func(void *arg)
{
    int err,signo;
    while(1)
    {
        sigwait(&mask,&signo);
        switch(signo)
        {
        case SIGHUP:
            syslog(LOG_INFO,"Re-reading configuration file.\n");
            reread();
            break;
        case SIGTERM:
            syslog(LOG_INFO,"got SIGTERM;exiting.\n");
            exit(0);
        default:
            syslog(LOG_INFO,"unexpected signal %d.\n",signo);
        }
    }
    return NULL;
}
int main(int argc,char *argv[])
{
    pthread_t           tid;
    char                *cmd;
    struct sigaction    sa;
    if((cmd = strrchr(argv[0],'/')) == NULL)
        cmd = argv[0];
    else
        cmd++;
    daemonize(cmd);
    if(already_running())
    {
          syslog(LOG_ERR,"daemon already running.\n");
          exit(1);
    }
    sa.sa_handler =SIG_DFL;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGHUP,&sa,NULL);
    sigfillset(&mask);
    pthread_sigmask(SIG_BLOCK,&mask,NULL);
    pthread_create(&tid,NULL,thread_func,0);
    sleep(90);
    exit(0);
}
复制代码

 关于本章介绍的守护进程,很多地方不是很懂,具体怎么应用还不清楚,先了解个大概,知道什么是守护进程及其作用。具体怎么用日后再来补充,有待加强。

相关文章