linux下单例进程的一个实现方式

晨曦之光 发布于 2012/04/10 15:03
阅读 451
收藏 1

今天在论坛上有朋友问如果截获SIGKILL信号,然后删除锁文件,他主要想要做一个单例进程,进程开始时判断一个文件config的存在性,如果不存在,则创建之然后运行,如果已经存在那么进程退出,进程在退出的时候必须把这个config文件删除以确保不能影响下一次进程的运行,但是问题出来了,如果有人用SIGKILL把他的进程杀死怎么办?于是我的回答就是再建一个进程守护着它的运行,比如用心跳检测就可以,一旦检测不到这个进程的心跳了,那么就帮忙删除掉这个config文件,另一种做法就是将这个单例的进程作为一个父进程的子进程运行,然后在父进程中设置SIGCHLD信号处理器,一旦子进程被杀,那么就会发送信号给父进程,然后父进程在信号处理中可以删除config文件,这样做的话仅仅是多了一层保护,试想如果守护此进程运行的进程或者这个进程的父进程被杀怎么办?改内核是一种方式,比如钩住do_exit内核函数,然后判断结束的是哪个进程后做善后处理,但是如果谁都要通过改内核行为来解决问题的话,那么操作系统的意义何在,内核是为系统服务的,而不是为用户服务的,因此不要指望内核行为完成用户策略,因此这些方式都还不是十全十美的方式。十全十美的方式就是自洽的,不需要别的东西帮忙的方式,不用别的进程,不用什么锁文件,而且这种方式还不能影响别的进程,不能给系统带来不稳定性,因此linux的“小即是好”的思想派上了用场,linux总是可以用很多很小的东西组合成一个很猛的功能,比如今天我想到的一个办法就是用管道实现的。在windows上也可以这么实现但是windows上的管道要复杂些,而且windows中还可以有别的很多方式,比如windows中可以设置全局的信号量和互斥体,它们都可以实现,windows的粒度某种意义上更小,比如windows可以基于窗口发送消息,而linux在系统范围内发送消息的最小单位就是进程,比如基于进程发送信号,虽然如此,windows总是带来很多混乱因为它没有处理好大和小的边界,粒度小是好的,比如像linux哲学所述的那样,但是过于小的话就会相互杂糅不稳定,过大又会臃肿不灵活。因此设计粒度必须是可控范围内的最小值,这个可控范围就是进程,而且必须有一个统一的实体来控制粒度,比如unix/linux中将很多东西往进程的意义上靠拢,因此进程就是unix/linux的中心。下面看看我实现单例的方式吧:
#include
int check( char * name )
{
    int len = strlen(name),i = 0;
        for(;name[len-1-i]!='/'&&i         name = name+len-i;
    char cmd[64];
    sprintf(cmd,"ps -e|grep %s|awk '{print $4}'",name); //拼串,这就是创举的证明,ps,grep,awk都是用管道结合的,而且代价很小,并且ps,grep,awk都不大
        FILE *fp;
        char pro[64];  //64个字节足够存放进程名字了
        fp = popen(cmd, "r");
        if(fp != 0)
        {
                int count = 0; //计数器,记录一共有多少个该进程在运行
                while(fgets(pro, 64, fp) > 0)
                {
                        pro[strlen(pro)-1]=0; //去掉后面的换行
                        if(!strcmp(pro,name))
                                count++;
                }
                if( count > 1 )    //超过一个就退出
            return 1; 
                pclose(fp);
        }
    return 0;
}
int main(int argc, char* argv[])
{
    if(check(argv[0]))
    {
        printf("This programe is already runing/n");
        exit(1);
    }
...//做真正的事
        return 0;
}
注释中提到管道的代价不大,事实上很小,看看内核的sys_pipe就知道了,这里就不说了,但是还是有必要说一下用户空间的popen,它是管道创建和执行shell的封装:
FILE * popen(const char *cmdstring, const char *type)
{
    int        i, pfd[2];
    pid_t    pid;
    FILE    *fp;
...   //判断参数,并且验证其正确性
    pipe(pfd)  //创建一对管道
    if ( (pid = fork()) == 0)  //准备在子进程里面执行新的任务cmdstring
    {                               
        if (*type == 'r') {  //如果父进程是用来read的,那么子进程就是write的,因此关闭掉子进程一对管道的read端
            close(pfd[0]);
            if (pfd[1] != STDOUT_FILENO) {
                dup2(pfd[1], STDOUT_FILENO); //因为子进程是write端,因此将子进程的标准输出重新定向到新创建管道的写端,也就是exec出来的进程的输出全部写到管道里面了,然后父进程将之读出。
                close(pfd[1]); //关闭掉管道描述符,它已经没有用了,因为标准输出已经dup到它的内核实体了
            }
        } else {  //对于父进程是写端的情况和上述情况相反,故而不再赘述。
...//关闭文件描述符
        execl(SHELL, "sh", "-c", cmdstring, (char *) 0);//真正执行命令
...
    }
    if (*type == 'r') {  //如果父进程为read端,那么要将新创建管道的write端返回给调用的用户
        close(pfd[1]);
        if ( (fp = fdopen(pfd[0], type)) == NULL)
            return(NULL);
    } else {//如果父进程为write端则相反
...
    return(fp);  //返回文件描述符
}

那位朋友问是否不应该用文件锁实现单例,我的回答是:

你的办法是标准的做法,别人用SIGKILL杀你的进程才是不好的做法,任何事情都不能做那么绝,冲动是魔鬼。如果你再怕别人用SIGKILL杀你的进 程,那么我刚才给你的程序会有用,不过最好在linux和unix上都测试一下,我怕ps和awk的输出语义在不同系统上会有不同。

他问:除了再设置一个守护进程看护这个单例进程外是否还有别的办法。我的回答:

写内核模块吧,钩住do_exit内核函数,在里面判断是否退出的是你的那个进程,如果是的话就删除掉那么config文件,然后继续正常的流程。不过我 觉得没有必要这么做,就好像人死亡一样,如果是得病死去或者自杀,那么有足够的时间交待后事,当然猝死病除外,然而意外死亡的话,比如车祸,误杀等等几乎 没有任何机会交待后事的。

他最开始问:能否捕捉到SIGKILL信号,然后做善后工作。我的回答:

对于SIGKILL,被杀进程本身没有机会做任何事情,但是并不是说就没有办法做善后工作了,你可以把你的进程作为一个进程的子进程,然后在父进程里面设 置SIGCHLD处理器,一旦子进程被杀或者退出,那么父进程可以帮忙善后;另外你还可以写任何一个进程来看护你的这个需要善后的进程,办法多的是,比如 可以用心跳


原文链接:http://blog.csdn.net/dog250/article/details/5303274
加载中
返回顶部
顶部