诛仙手游 齐昊 搭配:从kernel源代码的角度分析signal的错误用法和注意事项 - 技术文档 - 系统管理 Linux时代 - 开源、自由、共享 - 中国最大的Linux技术社区

来源:百度文库 编辑:九乡新闻网 时间:2024/04/28 22:48:47
从kernel源代码的角度分析signal的错误用法和注意事项 来源:ChinaUnix博客  日期:2006.11.21 15:02 (共有0条评论) 我要评论 
从kernel源代码的角度分析signal的错误用法和注意事项
                                                        !声明:按照Linux的
习惯, 我的这篇文档也遵循GPL 协议: 你可以随意应用并修改本文档,必须发布你的修改,使其他人可以获得一份Copy,尤其是给我一份Copy!
我的mail :bob_zhang2004@163.com | zhanglinbao@gmail.com 均可。欢迎论坛转载!
目前有些内容已经在 www.linuxforum.net中进行过讨论,可以前往:
http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=607800&page=0&view=&sb=&o=&fpart=&vc=1
   和
http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=607228&page=1&view=collapsed&sb=5&o=7&fpart
=
  欢迎大家继续讨论,以便文档更加完善! 多谢!周末愉快!                                          
                                                                     
                                                                     
                                          --bob读这份文档之前,建议先浏览一下 《UnixAdvanced Programming》里面的signal一章和下面这份出自IBM论坛的文章:进程间通信 信号(上)
http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html
  ,和 进程间通信 信号(下)
http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index2.html
该作者写了一个系列的进程间通信的文章,我只是希望对该篇作个补充!因为它们都没有从源代码的角度分析,所以我尝试了一下把上层应用与kernel实现代码分析结合起来,这样使用者才可能真正的理解signal的用法和原理! 目前介绍signal理论和用法书不少,缺点是只介绍其用法,非常深奥拗口,不容易理解;而介绍kernel源代码的书,侧重于代码分析,不讲实际应用!我就想到如果把两者结合起来,对上层使用signal函数的用户必然能知起所以然了,而且只要顺着我的代码注释大概粗读一下源码就可以理解signal的特性和用法以及你碰到的种种疑惑和不解了。 如果你对signal的特性和用法有什么疑惑的话,如果对kernel也感兴趣的话,就可以继续读源码 , 把这篇文章加以补充和完善! 前提是遵守上面的声明!   因为工作的需要,详细的读了一下 linux
kernel 2.4.24 版本的signal部分的源代码,收获不小。以前读>的时候,对signal的掌握只是停留在表面,只是会用而已,但是并不知道它是怎么实现,signal的本质到底
是什么。读了源码之后,才真正知道了signal的奥妙所在,对其用法理解的就更深了, 主要的是以后用signa的时候,犯错的机会就少了。有的时候看着一个系统调用成堆的手册页,还真不如看看它的实现来得更快, 当然两下对照着看就快了。    在此通过阅读源码,弄清楚了5
个问题,每个问题我都给出了结论,当然这些结论肯定是正确的,至少《Unix Advanced Programming》是这样认为的,
我只是从kernel的角度是验证它的正确性(简单的写了几个测试程序,以验证kernel的做法),而且也归纳了 一些结论,比如如何避免
Zobie进程
等。 相信对大家会有价值,也可以mail讨论!或者上相应的论坛!当然有个别的也是我自己的结论,如果您认为有值得商榷的地方,可以Email给我:
bob_zhang2004@163.com  首先总结一下:在PC linux(RHT 9.0 + kernel-2.4.24) 键盘产生的信号:Ctrl + c     
[/url]
SIGINT(2) terminate ,以前我总想当然以为是 SIGTERM(15)!Ctrl + \      SIGQUIT(3)   terminate Ctrl + z      SIGTSTP(20)   挂起进程 对于一般应用:挂起一个进程: kill(pid,SIGSTOP)   或 kill(pid,SIGTSTP) , 或 SIGTTIN , SIGTTOU 信号恢复一个进程kill(pid,SIGCONT); 杀死所有的符合某个名字的进程 :比如killall curl ,发送的是SIGTERM信号强制杀死某个进程 kill –9 curl ,发送的是SIGKILL 信号,在kernel中,SIGKILL和SIGSTOP是不能被忽略的....剩下的大家都清楚了,这里就不罗嗦了。子进程结束时候发给父进程的信号:  SIGCHLD ,这个比较特殊 , 且看下面3>的论述  Agenda :1>不可靠的信号2>Zombie进程(僵尸进程)与signal3>特殊的SIGCHLD 信号4>信号与进程的关系 ,进程的要求5>pause() 与 signal6>关于

阻塞信号7>关于不可重入函数8>关于信号的技巧 1>   不可靠的信号(linux继承Unix的结果,考虑兼容性), 和可靠的信号(主要就是信号可以排队处理,信号不丢失,linux自己的,但大家好像用的不多)什么是不可靠的信号:简单的说,就是当你向一个进程发送singal( 1~31,注意这里讨论是 1~31 )的时候 , 当进程还没有处理该信号(这时候叫pending,
未决信号)或者是正在调用信号处理函数的时候, 进程又收到了一个同样的信号 ,
kernel会把第二个信号丢弃,或者叫和一个信号合并,这样的信号就是 不可靠的信号 ,具体正方面的比较权威的解释请参考
http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html,这篇文章对于信号理论介绍的非常详细清楚明白, 个人认为比《Unix advanced Programming》要更好!系统实现是这样的:==> kernel/signal.c  int send_sig_info(int sig, structsiginfo *info, struct task_struct *t){      .............................................      /*          如果当前进程的未决信号集中已经包括了这个信号,就不重新注册后来现在的同样的信号了,        据个例子: 给进程发了 SIGTERM信号 , 但是kernel还没有来得及处理(进程只有在kernel空间即将返回道用户空间的时候,      kernel才会检测pending信号 ,然后才会调用do_signal()函数去处理)       这个时候又发了一个SIGTERM,那么第二个SIGTERM 肯定要被cut掉了。      */       if (sigpending.signal, sig)) //SIGRTMIN 是分水岭 , 小于它的都是不可靠的信号,否则就是实时信号       goto out; //跳出了正常执行的范围       ....................................................}  正确的:1~31都是不可靠的信号! SIGRTMIN ~SIGRTMAX都是可靠的信号!  以前大家有个误区: 误区1>以为不可靠的信号,是指 给进程发了一个信号(之前没有发过),那么这个信号可能丢失,也就是进程收不到              这样的理解是错误的,根据上面的定义 , 应该是”一个信号发了多遍,后来的信号丢失了,而不是第一个丢了“。             具体的原因可以参照上面的代码分析,就一目了然,还可以看《unix advanced programming》,不过我觉得它讲的都是老的Unix ,对Linux只能是参考而已! 误区2>signal()发送的是不可靠的信号 ,而 sigaction()发送的是可靠的信号只要是1-31的信号,它就是不可靠的信号。无论在注册信号处理函数的时候用的是sigaction() ,还是signal() ,只要你发送的信号 是1-31,那么就是不可靠的信号。中国有句俗语叫”烂泥扶不上墙“,我看放在这里挺合适! signal()和sigaction()的差别到底在哪里呢?   通过对比一看便知:   对于signal() ,它的kernel实现函数,也叫系统调用服务历程sys_signal() ==>kernel/signal.casmlinkageunsigned longsys_signal(int sig, __sighandler_t handler){structk_sigaction new_sa, old_sa;int ret; new_sa.sa.sa_handler =handler;new_sa.sa.sa_flags = SA_ONESHOT |SA_NOMASK;  //SA_ONESHOT:当执行一次信号处理程序后, 马上恢复为SIG_DFL ,   //SA_NOMASK :表示在信号处理函数执行期间,不屏蔽的当前正在处理的那个信号 ret = do_sigaction(sig, &new_sa,&old_sa);   //sys_sigaction 也调用这个函数 return ret ? ret : (unsignedlong)old_sa.sa.sa_handler;} 而sigaction()函数的kernel实现是: sys_sigaction()==>arch/i386/kernel/signal.casmlinkage int sys_sigaction(int sig, const struct old_sigaction*act,struct old_sigaction *oact){struct k_sigaction new_ka, old_ka;intret; if (act) {old_sigset_t mask;if (verify_area(VERIFY_READ, act,sizeof(*act)) ||    __get_user(new_ka.sa.sa_handler,&act->sa_handler) ||    __get_user(new_ka.sa.sa_restorer,&act->sa_restorer))return -EFAULT;__get_user(new_ka.sa.sa_flags,&act->sa_flags);__get_user(mask,&act->sa_mask);siginitset(&new_ka.sa.sa_mask, mask);} ret =do_sigaction(sig, act ? &new_ka : NULL, oact ? &old_ka :NULL);//都调的这个函数 if (!ret && oact) {if (verify_area(VERIFY_WRITE,oact, sizeof(*oact)) ||    __put_user(old_ka.sa.sa_handler,&oact->sa_handler) ||    __put_user(old_ka.sa.sa_restorer,&oact->sa_restorer))return -EFAULT;__put_user(old_ka.sa.sa_flags,&oact->sa_flags);__put_user(old_ka.sa.sa_mask.sig[0],&oact->sa_mask);} return ret;}signal()和sigaction()都是用do_signaction()来包装的, 都是用 struct sigaction()这个结构体的,差别在下面标出来了 structsigaction {__sighandler_t sa_handler; //2// typedef void(*__sighandler_t)(int);  signal()和sigaction()函数都要求要户提供信号处理函数unsignedlong sa_flags; //signal()函数默认就用 SA_ONESHOT | SA_NOMASK; //sigaction()要由用户自己指定!void (*sa_restorer)(void); //没用了sigset_t sa_mask;   //执行信号处理函数的时候要阻塞的信号,signal()使用默认的,就屏蔽正处理的信号,其他的不屏蔽,sigaction()要求用户自己指定!};  ? 讨论时间:读到这里我有个疑问:sys_signal()函数明明把 sa_flags =SA_ONESHOT | SA_NOMASK; 而且在kernel执行信号处理函数之前,它会检查SA_ONESHOT标志 ,如果有这个标志,就把sa_handler = SIG_DFL ,如果是这样的话, 我们需要反复注册某个信号的处理函数才行啊,但是事实上,我们并没有这样作,而且程序运行的很好! Kernel的signal()函数实现代码如下:             ==>arch/i386/kernel/signal.cstatic voidhandle_signal(unsigned longsig, struct k_sigaction *ka,      siginfo_t *info, sigset_t *oldset,struct pt_regs *regs){.........................................................../* Setup the stack frame */if (ka->sa.sa_flags &SA_SIGINFO)setup_rt_frame(sig, ka, info, oldset,regs);elsesetup_frame(sig, ka, oldset, regs);//here , 我加了debug信息,确实执行到这里了, if (ka->sa.sa_flags & SA_ONESHOT){//sys_signal()函数明明设置了这个标志//通过debug ,知道居然没有到这里,就说明, sa_flags根本就没有SA_ONESHOT标志了 ,可是sys_signal() 却又明明设置了这个标志,而且我搜索过, 根本没有地方,取消了SA_ONESHOT 标志printk(" the signal (%d) handler will reset toSIG_DFL\n",sig);ka->sa.sa_handler = SIG_DFL; //这难道还不明确吗? if(!(ka->sa.sa_flags & SA_NODEFER)){spin_lock_irq(¤t->sigmask_lock);sigorsets(¤t->blocked,¤t->blocked,&ka->sa.sa_mask);sigaddset(¤t->blocked,sig);recalc_sigpending(current);spin_unlock_irq(¤t->sigmask_lock);}}既然这样的话 ,如果我们调用signal()就应该在信号处理函数中反复注册自己的信号处理函数才对 , 否则无法处理下一个同样的信号了。 比如void signal_catch(int signo) {              //信号处理函数细节             //最后一行              signal(signo, signal_catch);      //再注册一遍, 否则就变成SIG_DFL 了 。 }对于这个问题 《Unix Advanced Programming》 也提到过,说早期的Unix 也存在这个问题,是信号不可靠的一个原因 (见 P206)  但是实际上我们在用signal()函数的时候, 我们好像并不需要这么作 ,比如一个简单的测试程序。为了测试, 我写了一个最简单的例子:void sigterm_handler(int signo){          printf("Havecaught sig N.O. %d\n",signo);         //按照kernel代码,应该还要有signal(signo,sigterm_handler);   才对呀,但事实上,我们大家都知道没有必要这样用 ,为什么呢? 请前往论坛讨论:
http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=607961&page=0&view=collapsed&sb=5&o=7&fpart=&vc=1&PHPSESSID
= } int main(void) {     printf("-------------111111111111111-------------\n");         signal(SIGTERM,sigterm_handler);   pause();     printf("----------222222222222222----------------\n");              pause();//如果按照kernel代码里面写的, 当再发一个SIGTERM信号的时候 , sa_handler 就编程SIG_DFL了,那默认就是 //terminate ,所以不会打出来 333333333333333333 了,      printf("-------------3333333333333333----------\n");              return0;} 但是执行结果确实:  333333333333333333333333 也打出来了, 这就又说明signal函数,不需要反复注册信号处理函数 , 这不就矛盾吗?  所以现在问题就是if (ka->sa.sa_flags &SA_ONESHOT){ ka->sa.sa_handler = SIG_DFL; 是在什么情况下 改变了sigaction->sa_flags (去掉了 SA_ONESHOT 标志呢?)我在代码里面搜索不到啊!如果感兴趣的朋友可以前往论坛讨论:
http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=607949&page=0&view=collapsed&sb=5&o=7&fpart=&vc=1
   2>       僵尸进程:也叫Zombie进程:  僵尸进程定义:进程结束后,该进程的父进程没有调用wait或waitpid()对子进程进行回收 , 子进程一直是Zombie状态。关于kernel如何杀死Zombie 请看kernel/exit.c ==>sys_wait4() 函数 , waitpid 就是sys_wait4()实现的。          首先看看正确的编程方法:当一个进程fork()出一个子进程的时候,正确的情况下,父进程应该回收进程的资源:通过下面两个办法中的一个即可避免Zombie(僵尸进程): Ø     父进程显式的忽略SIGCHLD 信号只要在fork一个子进程之前加上这么 一行:  signal(SIGCHLD, SIG_IGN);//这样肯定不会出现僵尸进程,        为什么呢? 看kernel的代码吧:==>asm/i386/signal.c==>do_signal()       ka =¤t->sig->action[signr-1];//¤t->sig :signal_struct       if (ka->sa.sa_handler == SIG_IGN) {            if(signr != SIGCHLD)            continue; //对于信号处理方式是 SIG_IGN,非SIGCHLD的信号 ,kernel什么也不作! SIGCHLD 比较特殊啊!            /* Check forSIGCHLD: it's special.             类似调用waitpid()来回收child process的进程表项           */      //SIG_CHLD 信号的行为设置为SIG_IGN , 由内核来处理僵死进程。      
//如果你的程序中没有特别的要求需要处理SIGCHLD ,
为了避免僵尸进程(Zombie进程),你可以显式的忽略它,kernel会调用sys_wait4()来处理僵尸进程的),它执行一个while()
loop , 来处理系统中所有的僵尸进程,老黄牛精神啊!               while (sys_wait4(-1,NULL, WNOHANG, NULL) > 0)   // 看看是不是和waitpid的用法一样啊!             /*nothing */;            continue;      }      如果 SIGCHLD 是默认的 SIG_DFL的话:kernel就不管了,所以肯定会有僵尸进程的!==>asm/i386/signal.c ==>do_signal()     if (ka->sa.sa_handler == SIG_DFL) {              int exit_code =signr;               /* Init gets no signals it doesn't want. */             if (current->pid == 1) //谁都不可以给init(1) 进程发信号, 这样说比较准确:发了也白发,kernel不认可              continue;               switch (signr) {              case SIGCONT: case SIGCHLD: case SIGWINCH: case SIGURG:             continue; //对于SIGCHLD 信号,kernel对它默认是忽略的, (请不要和SIG_IGN 混淆了)             //所以很明显, kernel并没有调用sys_wait4() 来处理僵尸进程 ,你要自己处理了,^_^       ..............       } Ø    父进程给SIGCHLD信号注册handler(里面调用waitpid()回收child Zombieprocess)比如:这样写:while(waitpid(-1,NULL,WNOHANG) > 0) {  //自动处理所有的僵尸进程,当然你可以不用while,只调用一次,看需要 : 比如父进程是个httpserver,就会fork()出很多子进程, 所以while()是有必要的。//WNOHANG 很关键,如果没有僵死进程,就马上返回,这样while()才可以结束啊 , 可是wait()就没有这个参数, 所以wait就阻塞了。所以一般情况下,我们用waitpid还是最好的了!   ;//什么也不必作了, 可以打印看看到底回收了哪些进程pid}!如果你没有用上面任何一个办法,就会出现僵尸进程。 ps ax 命令可能会显示:       22149 tty8 S 0:00  test_pro      22150 ?    Z 0:00    [test_pro ]   //这就是僵尸进程 Z就是Zombie的意思 , 你用kill -9 也无法杀掉它 。 怎么杀掉Zombie进程呢?你可以kill它的父进程就可以杀掉Zombie进程。 kill -SIGTERM 22149 , 你在ps ax 看看 ,两个进程都没有了。  避免僵尸进程的第三种办法           个人不推荐! 因为上面两种方法已经够用了,除非你还有其他的要求,比如使子进程无法获得控制终端,这种情况下, 就必须fork()两次了 。否则一般情况下,我们需要父子进程同步和通信的,父亲和儿子交流尚且比较方便(用pipe最好,配合使用
[url=http://blog.csdn.net/kendiv/archive/2006/08/30/1146024.aspx]
select()) , 你让爷爷和孙子通信不是比较困难吗? 两代人的代沟呢。。。。 当       你也可以fork()两次,父亲(比如http server,循环处理) -> 儿子进程(exit) -> 孙子进程(处理每次的任务,正常结束,就不会成为Zombie)                   下面是事例代码:   pid_t pid =0;pid = fork();if(pid  //error  exit( -1);else if(pid >0)  //这里可能是个Server一类的,父亲进程永远不会结束的,是while()循环else {  //现在儿子 process 了,if(pid = fork()  //error exit(-1); else if(pid > 0) //儿进程也结束了exit(0);//立刻杀死儿子进程,这样孙子就成孤儿了,孙子进程会被init(1)领养的。 else { //到孙子进程了。 /* somecode …………..   */ exit(0);   }} 对于 原理其实很简单: 儿子死了, 只有孙子了, 孙子是孤儿了,那么init(1)进程就会领养这个 孤儿, 同时孤儿就认为init(1)就是它的父进程,由init进程负责收尸!     3> 特殊的 SIGCHLD 信号SIGCHLD 特殊在哪里呢?? 一般情况下, 子进程结束后 ,都会给父进程发送 SIGCHLD 信号 ,但是这不是绝对的。 ·    当一个父进程fork()一个子进程后, 当父进程没有为SIGCHLD 注册新的处理函数,处理方式为SIG_DFL,那么当子进程结束的时候, 就不会给父进程发送SIGCHLD 信号 。    从代码的角度:执行到send_sig_info(),会在isgnore_signal() 函数里面做是否要发信号的判断,结果 SIGCHLD被忽略了! ·    就是普通的进程,在某个地方pause(),也不是随便发一个信号就可以唤醒它, 比如 发 SIGCONT信号(在kernel中当SIGCONT 的处理方式为SIG_DFL的时候, 它要被ignore的) ,就不可以!例子: intmain(void) {  pause();  printf(“I am waken up\n”);  return 0;}如果你在外部随便发下列信号:SIGCONT , SIGWINCH , SIGCHLD SIGURG ,肯定是要被进程忽略的,并不能唤醒该进程!如果你发SIGTERM,SIGQUIT,SIGINT等信号,没有注册handler , 那么默认是中止它;如果注册了handler ,则可以唤醒该进程。  且看下面的代码分析:/* * Determine whether a signal should be posted ornot. * * Signals with SIG_IGN can be ignored, except for the * specialcase of a SIGCHLD.  * * Some signals with SIG_DFL default to anon-action. */ //定义了那些信号要被忽略! static int ignored_signal(int sig, structtask_struct *t){/* Don't ignore traced or blocked signals */if((t->ptrace & PT_PTRACED) || sigismember(&t->blocked,sig))return 0; return signal_type(sig, t->sig) == 0; } /* *Signal type: *     *    = 0 : ignored *    > 0 : wake up. *///#define SIG_DFL ((__sighandler_t)0) /* default signal handling */#defineSIG_IGN ((__sighandler_t)1) /* ignore signal */#define SIG_ERR((__sighandler_t)-1) /* error return from signal *///static signal_type(intsig, struct signal_struct *signals){unsigned long handler;//-----------------------------空信号 ignore-----------------------------if (!signals)return 0;   // handler =(unsigned long) signals->action[sig-1].sa.sa_handler;
从kernel源代码的角度分析signal的错误用法和注意事项 - 技术文档 - 系统管理 Linux时代 - 开源、自由、共享 - 中国最大的Linux技术社区 Fedora / Redhat 软件包管理指南 - 技术文档 - 系统管理 Linux时代 - 开源、自由、共享 - 中国最大的Linux技术社区 从kernel原始码的角度分析signal的错误用法和注意事项(zt) BMP位图文件的存储格式 - 技术文档 - 新手入门 Linux时代 - 开源、自由、共享... Linux下NFS服务器的配置 - 技术文档 - 网络技术 Linux时代 - 开源、自由... Ubuntu 更改文件夹权限 - 技术文档 - 系统管理 Linux时代 - 开源、自由、... linux 线程实现机制分析 - 技术文档 - 网络技术 Linux时代 - 开源、自由、... Linux IPC 参数设定 - 技术文档 - 网络技术 Linux时代 - 开源、自由、... Linux Kernel 2:用户空间的初始化 linux源代码分析和阅读工具比较 技术分析的真正用法 linux的strace命令用法 linux?sort命令的用法 关于Linux下kernel.shmmax的设置问题 linux 批处理的方法- 技术类文章 - 空愁居社区(空愁郡) 空愁居@十万图纸.军史... linux 完美运行 SourceInsight - 技术文档 - 网络技术 Linux时... Linux下串口参数VTIME和VMIN的用法 linux多线程的总结(pthread用法) - 红联Linux门户 - 中国领先的Lin... linux中fdisk的用法-Linux认证考试-考试大 linux源代码分析方法谈 Linux操作系统下的常见系统资源共享 技术分析的三大注意事项 linux下losetup命令的用法 linux中uptime命令的用法