第十三章 守护进程

本文详细介绍了守护进程的特性、编程规则及其实现方法。探讨了如何处理SIGHUP信号以实现配置文件的重读,以及如何确保守护进程的单实例运行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在看第十二章之前,这里要先提一个十一章遗留下来的一个问题.
    在使用条件变量的时候,下面哪个步骤是正确的?
        1、对互斥量加锁
        2、改变互斥量保护的条件
        3、给等待条件的线程发信号
        4、对互斥量解锁
    或者
        1、对互斥量加锁
        2、改变互斥量保护的条件
        3、对互斥量解锁
        4、给等待信号的线程发信号

    总的来说,这两种情况可能都是正确的,但每一种方法都有不足之处。
        在第一种情况下,等待线程可能会被安排在调用 pthread_cond_broadcast之后运行。如果程序运行在多处理机上,由于还持有互斥锁,一些线程就会运行而马上阻塞。
        在第二种情况下,运行线程可以在第三步和第四步之间获取互斥锁,然后使条件失效,最后释放互斥锁。接着,当调用 pthread_cond_broadcast时,条件不再为真,线程无需运行,这就使为什么唤醒线程必须重新检查条件,不能仅仅因为 pthread_cond_wait返回就假定条件就为真。

      看过书上之后,发现看的迷迷糊糊,就上网提问了,得到了以下回答: 

                            http://bbs.youkuaiyun.com/topics/390980156

                           1、对互斥量加锁
                           2、改变互斥量保护的条件
                           3、给等待条件的线程发信号
                           4、对互斥量解锁

                          / /等待线程收到在 第3步发送的信号 而运行,但又立刻阻塞在互斥锁上,因为此时持有锁的线程 第4步 还未运行

                         或者
                        1、对互斥量加锁
                        2、改变互斥量保护的条件
                        3、对互斥量解锁
                        4 、给等待信号的线程发信号

                       //第3步后,可能另一线程运行并使条件为假,这时在第 4 步后,线程就不应该运行,如果不判断条件、仅仅因为 pthread_cond_wait返回就运行则错了,强调的是要同时判断两者





守护进程特征

    守护进程常常在系统引导装入时启动,仅在系统关闭时才终止。是在后台运行的。

    大多数首进程都是以超级用户特权运行。所有的守护进程都没有控制终端,其终端名设置为问号。
    用户层守护进程的父进程是init进程

守护进程编程规则:
    这个规则之前提到过,这里我们详细的看一看
    (一)、首先要做的是调用 umask 将文件创建模式创建屏蔽字设置为一个已知值
            因为这个是会被继承的,这里要为将来使用作准备
    (二)、调用fork,然后使父进程 exit。
            1、如果该守护进程是作为一条简单的shell命令启动的,那么父进程终止会让shell认为这条命令已经执行完毕
            2、让子进程不是进程组组长,作为下面要调用的setsid的先决条件。
    (三)、调用setsid创建一个新会话
            1、成为新会话的会话首进程
            2、成为一个新进程组的组长进程
            3、没有控制终端
    (三. 五)    在这里,有人建议此时再使用一个fork, 终止父进程,这样就保证了该进程不是会话首进程,可以防止它获取控制终端。
            另一种方式是 无论何时打开一个终端设备,都指定 O_NOCTTY
    (四)、将当前工作目录改为根目录
            从父进程继承来的当前工作目录可能是一个挂在的文件系统,因为守护进程必须是一直存在的,那么该文件系统就不能被卸载
            当然,可能是其他目录
    (五)、关闭不再需要的文件描述符
    (六)、某些守护进程打开 /dev/null 使其具有文件描述父 0,1,2,这样任何一个试图读标准输入、写标准输出或标准错误的库例程都不会产生任何效果


#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

void mdaemonize()
{
        int i,n,fd0,fd1,fd2;
        pid_t pid;
        struct sigaction sa;

        umask(0);

        if((pid=fork()) < 0)
                exit(1);
        else if(pid != 0)
                exit(0);

        setsid();

        sa.sa_handler = SIG_IGN;
        sa.sa_flags = 0;
        sigemptyset(&sa.sa_mask);
                                // why do we have to set SIGHUP? 我查了一下,可能是因为避免session leader退出的时候发送过来的SIGHUP,但因为我们不是前台进程,所以不会收到这个信号
        if(sigaction(SIGHUP,&sa,NULL) < 0)
                exit(1);
                                //<strong>在UNP中,书上说这里必须要设置SIGHUP,因为当会话头进程(首个fork产生的子进程)终止时,其会话中的所有进程(即再次fork产生的子进程)都收到SIGHUP信号
</strong>        if((pid=fork()) < 0)
                exit(1);
        if(pid != 0)
                exit(0);

        if(chdir("/") < 0)
                exit(1);
                                // close all the descriptors
        n = getdtablesize();
        for(i=0; i<n; i++)
                close(i);
                                // Attach file descriptor 0,1 and 2 to /dev/null
        fd0 = open("/dev/null",O_RDWR);
        fd1 = dup(0);
        fd2 = dup(0);

        if(fd0!=0 || fd1!=1 || fd2 !=2)
        {
                printf("wrong , %d %d %d\n",fd0,fd1,fd2);
                exit(1);
        }

        sleep(10);
}

int main()
{
        mdaemonize();
        return 0;
}



    以上程序的运行结果为
 
   uid        pid     ppid     pgid    sid
    500       5046     1      5045    5045     0    15:27 ?        00:00:00 ./DZ



    大部分是理解了,就是不知到为什么需要处理SIGHUP信号呢?  哪里说会话首进程终止会发送信号SIGHUP了? 我自己试了下,接收到该信号进程终止,进程还是照旧运行啊。。。



出错记录
    守护进程是如何处理出错消息的呢?
        因为其本身不具备控制终端,所以不能只是简单的写道标准错误。
    大多数守护进程都使用了 syslog 函数。

    有3种产生日志消息的方法:
        1、内核例程调用 log函数。    
        2、大多数守护进程调用 syslog 函数老产生日志信息。
        3、无论是本机进程还是通过 TCP/IP 链接到此主机的其他主机上,都可以将日志消息发向 UDP端口 514.
    
    此系列函数:
        openlog、 syslog、 closelog 、setlogmask
        


单实例守护进程
    为了正常运作,某些守护进程会实现为, 在同一时刻只运行该守护进程的一个副本。
    例如,对cron 守护进程而言, 如果同时有多个实例运行,那么每个副本都可能试图开始某个预定的操作,于是造成该操作的重复执行,从而导致出错。
    这里我们使用文件和记录锁解决这个问题。如果每一个守护进程创建一个固定名字的文件,并在该文件的整体上加一把写锁,那么只允许创建一把这样的写锁。在此之后的创建写锁的尝试都会失败,这就向守护进程指明已经有一个副本正在运行。
    在守护进程终止时,这把锁被自动删除。

    以下函数就通过使用文件锁说明如何使守护进程只存在一个副本。文件中存当前守护进程的进程ID。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

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

int lockfile(int fd)        //详情见下一章关于记录锁部分
{
        struct flock fl;
        fl.l_type = F_WRLCK;
        fl.l_start = 0;
        fl.l_whence = SEEK_SET;
        fl.l_len = 0;

        return (fcntl(fd,F_SETLK, &fl));
}

int already_daemon()
{
        int fd;
        char buf[16];

        if(fd = open(LOCKFILE, O_RDWR | O_CREAT, LOCKMODE) < 0)
                exit(1);
        
        if(lockfile(fd) < 0)
      {
            if(errno == EACCESS || errno == EAGAIN)        //如果当前已经有守护进程副本在运行了,就设置 errno 为 EACCESS或EAGAIN
             {
                close(fd);
                return 1;
             }
                exit(1);
      }

        ftruncate(fd,0);        //因为我们要文件存的是当前守护进程的进程ID,这个函数用于将文件长度截断为0。另一方面,如果不截断,虽然会覆盖,但若是之前的长度超过当前要插入的字符串,那么之前的字符串会遗留一部分在文档中。
        sprintf(buf,"ld",(long)getpid());
        write(fd,buf,strlen(buf)+1);
        return 0;
}






守护进程的惯例
    1、若守护进程使用锁文件,那么通常存储在 /var/run 目录中,名字常常是  name.pid
    2、若守护进程支持配置选项,那么配置文件常常在 /etc中, 名字常常是 name.conf
    3、守护进程可以用命令行启动,但通常它们是由系统初始化脚本(/etc/rc* 或 /etc/init.d/*)之一启动的。如果守护进程终止时,应当自动的重新启动它,则我们可以在 /etc/inittab中为该守护进程包括 respawn项,这样,init 就将重新启动该守护进程
    4、守护进程启动时会读取 配置文件,之后就不会再读取。 若我们修改了配置文件,可以发送 SIGHUP信号给守护进程,它就会重新读取配置文件。


    以下我们就看一下如何通过 SIGHUP 信号来重读,这里会用到上面提到的 mdaemonize()函数和 already_daemon()函数(此程序使用 sigwait函数 )(这里依旧不进行守护进程的出错处理)
#include <stdio.h>
#include <pthread.h>
#include <signal.h>

sigset_t mask;

void reread()
{
    //....
}

void *th_fun(void *arg)
{
    int signo;
    while(1)
    {
        if(sigwait(&mask,&signo) != 0)        //利用线程处理 SIGHUP和 SIGTERM信号,一个是重读配置文件,一个是终止
            exit(1);
        switch(signo)
        {
        case SIGHUP:
            reread();
            break;
        case SIGTERM:
            exit(0);
        default:
            //出错处理
        }
    }
}

int main(int ac, char **av)            //参数即为配置文件
{
    char *cmd;
    pthread_t tid;
    struct sigaction sa;
    
    if((cmd=strrchr(av[0],'/')) == NULL)    //取出配置文件的名字
        cmd = av[0];
    else
        cmd ++;
    
    mdaemonize(cmd);

    if(already_daemon())            //发现已经有一个当前守护进程的副本存在,选择退出
        exit(1);
    
    sa.sa.handler = SIG_DFL;            //接着就是设置SIGHUP的处理为默认。因为在 mdaemonize函数中,我们将此信号设置为忽略,如果这里不改变,那么创建的线程也将会忽略,从而即使有sigwait函数在等他,这个信号也永远不会出现。
    sigemptyset(&sa.sa_mask);
    sa.sa_flag = 0;
    if(sigaction(SIGHUP, &sa, NULL) < 0)
        exit(1);

    sigfillempty(&mask;)            //我们处理信号的是用一个线程来处理的,所以首先在主线程里屏蔽所有信号
    if(pthread_sigmask(SIG_BLOCK,&mask,NULL) != 0)
        exit(1);

    if(pthread_create(&tid,NULL,th_fun,0) != 0)
        exit(1);

    // other things to deal with

    return 0;
}




    虽然SIGHUP和SIGTERM的默认动作是终止进程。因为我们阻塞了这些信号,所以当SIGHUP和SIGTERM的其中一个发送到守护进程的时候,守护进程不会消亡,作为代替,调用sigwait的线程在返回时候将指示已经接收到该信号。

    当然,我们也可以不用线程而是简单的将处理放在信号处理函数中。


#include <stdio.h>
#include <pthread.h>
#include <signal.h>

sigset_t mask;

void reread()
{
    //....
}

void sigterm(int s)
{
    //守护进程消息处理syslog
    exit(1);
}

void sighup(int s)
{
    reread();
}

int main(int ac, char **av)            //参数即为配置文件
{
    char *cmd;
    pthread_t tid;
    struct sigaction sa;
    
    if((cmd=strrchr(av[0],'/')) == NULL)    //取出配置文件的名字
        cmd = av[0];
    else
        cmd ++;
    
    mdaemonize(cmd);

    if(already_daemon())            //发现已经有一个当前守护进程的副本存在,选择退出
        exit(1);
    
    sa.sa.handler = sighup;            
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask,SIGHUP);
    sa.sa_flag = 0;
    if(sigaction(SIGHUP, &sa, NULL) < 0)
        exit(1);

    sa.sa.handler = sigterm;            
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask,SIGTERM);
    sa.sa_flag = 0;
    if(sigaction(SIGTERM, &sa, NULL) < 0)
        exit(1);


    // other things to deal with

    return 0;
}







在学习过程中,我发现对 SIGHUP的理解一直存在疑惑。这里先贴一个地址,虽然看了依旧不是非常清楚,但至少能学到点什么
    http://bbs.chinaunix.net/thread-766356-1-1.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值