第十三章 守护进程

来源:互联网 发布:java socket select 编辑:程序博客网 时间:2024/06/14 16:18
在看第十二章之前,这里要先提一个十一章遗留下来的一个问题.
    在使用条件变量的时候,下面哪个步骤是正确的?
        1、对互斥量加锁
        2、改变互斥量保护的条件
        3、给等待条件的线程发信号
        4、对互斥量解锁
    或者
        1、对互斥量加锁
        2、改变互斥量保护的条件
        3、对互斥量解锁
        4、给等待信号的线程发信号

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

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

                            http://bbs.csdn.net/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
0 0
原创粉丝点击