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

今天在论坛上有朋友问如果截获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 <stdio.h><br>int check( char * name ) <br>{ <br> int len = strlen(name),i = 0; <br> for(;name[len-1-i]!='/'&amp;&amp;i<len></len> name = name+len-i; <br> char cmd[64]; <br> sprintf(cmd,"ps -e|grep %s|awk '{print $4}'",name); //拼串,这就是创举的证明,ps,grep,awk都是用管道结合的,而且代价很小,并且ps,grep,awk都不大 <br> FILE *fp; <br> char pro[64]; //64个字节足够存放进程名字了 <br> fp = popen(cmd, "r"); <br> if(fp != 0) <br> { <br> int count = 0; //计数器,记录一共有多少个该进程在运行 <br> while(fgets(pro, 64, fp) &gt; 0) <br> { <br> pro[strlen(pro)-1]=0; //去掉后面的换行 <br> if(!strcmp(pro,name)) <br> count++; <br> } <br> if( count &gt; 1 ) //超过一个就退出 <br> return 1; <br> pclose(fp); <br> } <br> return 0; <br>} <br>int main(int argc, char* argv[]) <br>{ <br> if(check(argv[0])) <br> { <br> printf("This programe is already runing/n"); <br> exit(1); <br> } <br>...//做真正的事 <br> return 0; <br>} <br>注释中提到管道的代价不大,事实上很小,看看内核的sys_pipe就知道了,这里就不说了,但是还是有必要说一下用户空间的popen,它是管道创建和执行shell的封装: <br>FILE * popen(const char *cmdstring, const char *type) <br>{ <br> int i, pfd[2]; <br> pid_t pid; <br> FILE *fp; <br>... //判断参数,并且验证其正确性 <br> pipe(pfd) //创建一对管道 <br> if ( (pid = fork()) == 0) //准备在子进程里面执行新的任务cmdstring <br> { <br> if (*type == 'r') { //如果父进程是用来read的,那么子进程就是write的,因此关闭掉子进程一对管道的read端 <br> close(pfd[0]); <br> if (pfd[1] != STDOUT_FILENO) { <br> dup2(pfd[1], STDOUT_FILENO); //因为子进程是write端,因此将子进程的标准输出重新定向到新创建管道的写端,也就是exec出来的进程的输出全部写到管道里面了,然后父进程将之读出。 <br> close(pfd[1]); //关闭掉管道描述符,它已经没有用了,因为标准输出已经dup到它的内核实体了 <br> } <br> } else { //对于父进程是写端的情况和上述情况相反,故而不再赘述。 <br>...//关闭文件描述符 <br> execl(SHELL, "sh", "-c", cmdstring, (char *) 0);//真正执行命令 <br>... <br> } <br> if (*type == 'r') { //如果父进程为read端,那么要将新创建管道的write端返回给调用的用户 <br> close(pfd[1]); <br> if ( (fp = fdopen(pfd[0], type)) == NULL) <br> return(NULL); <br> } else {//如果父进程为write端则相反 <br>... <br> return(fp); //返回文件描述符 <br>}</stdio.h>

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

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

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

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

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

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

你可能感兴趣的:(linux)