Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1133450
  • 博文数量: 221
  • 博客积分: 10152
  • 博客等级: 上将
  • 技术积分: 1518
  • 用 户 组: 普通用户
  • 注册时间: 2005-07-22 10:42
文章分类

全部博文(221)

文章存档

2018年(1)

2015年(6)

2014年(3)

2013年(4)

2012年(1)

2011年(5)

2010年(14)

2009年(10)

2008年(28)

2007年(33)

2006年(114)

2005年(2)

我的朋友

分类:

2006-10-23 16:00:48

从kernel源代码的角度分析signal的错误用法和理解

关键词:                                                

 

!声明按照Linux的习惯, 我的这篇文档也遵循GPL 协议: 你可以随意应用并修改本文档,必须发布你的修改,使其他人可以获得一份Copy,尤其是给我一份Copy! 我的mail bob_zhang2004@163.com | zhanglinbao@gmail.com 均可。欢迎论坛转载! 目前有些内容已经在 中进行过讨论,可以前往:     =   欢迎大家继续讨论,以便文档更加完善! 多谢!周末愉快!                                                                                                                               

--bob

读这份文档之前,建议先浏览一下 《Unix Advanced 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也感兴趣的话, 就可以继续读源码 , 把这篇文章加以补充和完善!  前提是遵守上面的声明!

 

 

 

因为工作的需要,用了2天的时间详细的读了一下 linux kernel 2.4.24 版本的signal方面的源代码,收获不小,因为以前发现看<>的时候 ,不知道是大师的话太深奥,还是中文版太烂,有的东西就是理解不了,象吃满头嗫住了,很是不爽,总觉得心里不踏实。看看源码才真正明白什么是信号,以及它的kernel流程,所以建议大家对某个系统调用,函数什么的,如果存在疑惑和不理解的,强烈建议读读源码,粗读也非常不错,关键要由参考书领着读,比如<源代码 情景分析>> 就非常不错。

 

 

有的时候看着一个系统调用成堆的手册页,还真不如看看它的实现来得更快, 当然两下对照着看就快了。  

 

另外提醒大家 <> 可不是 《Linux Advanced Programming》啊!尽信书不如无书 ......

 

 

在此通过阅读源码,弄清楚了5个问题,每个问题我都给出了结论,当然这些结论肯定是正确的,至少《Unix Advanced Programming》是这样认为的, 我只是从kernel的角度是验证它的正确性(简单的写了几个测试程序,以验证kernel的做法),而且也归纳了 一些结论,比如如何避免 Zobie进程 等。  相信对大家会有价值,也可以mail讨论!或者上相应的论坛!

 

 

首先总结一下:在PC linuxRHT 9.0 + kernel-2.4.24) 键盘产生的信号:

Ctrl + c     SIGINT(2)   terminate ,以前我总想当然以为是 SIGTERM(15)!

Ctrl + \      SIGQUIT(3) terminate

Ctrl + z      SIGTSTP(20)   挂起进程

 

对于一般应用:

挂起一个进程:   kill(pid, SIGSTOP)   或 kill(pid,SIGTSTP) , 或 SIGTTIN , SIGTTOU 信号

恢复一个进程  kill(pid,SIGCONT); 

....

剩下的大家都清楚了,这里就不罗嗦了。

子进程结束时候发给父进程的信号:   SIGCHLD ,这个比较特殊 , 且看下面3>的论述

 

 

Agenda :

1>不可靠的信号

2>Zombie进程(僵尸进程)与signal

3>特殊的SIGCHLD 信号

4>信号与进程的关系 ,进程的要求

5>pause() 与 signal

6>关于信号的技巧

 

1> 不可靠的信号(linux继承Unix的结果,考虑兼容性) ,  和可靠的信号(主要就是信号可以排队处理,信号不丢失,linux自己的,但大家好像用的不多

什么是不可靠的信号:简单的说,就是当你向一个进程发送 singal( 131,注意这里讨论是 131 )的时候 , 当进程还没有处理该信号(这时候叫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, struct siginfo *info, struct task_struct *t)

{

      .............................................

      /*   

              如果当前进程的未决信号集中已经包括了这个信号,就不重新注册后来现在的同样的信号了, 

              据个例子:  给进程发了 SIGTERM 信号 , 但是kernel还没有来得及处理(进程只有在kernel空间即将返回道用户空间的时候,

              kernel才会检测pending信号 ,然后才会调用do_signal()函数去处理)

              这个时候又发了一个SIGTERM,那么第二个SIGTERM 肯定要被cut掉了。

      */

       if (sig < SIGRTMIN && sigismember(&t->pending.signal, sig))  //SIGRTMIN 是分水岭 , 小于它的都是不可靠的信号,否则就是实时信号

              goto out;  //跳出了正常执行的范围

       ....................................................

}

 

正确的: 131都是不可靠的信号! SIGRTMIN SIGRTMAX都是可靠的信号!

 

 

以前大家有个误区:

 

误区1>

              以为不可靠的信号,是指 给进程发了一个信号(之前没有发过),那么这个信号可能丢失,也就是进程收不到

              这样的理解是错误的, 根据上面的定义 , 应该是”一个信号发了多遍,后来的信号丢失了, 而不是第一个丢了“。

              具体的原因可以参照上面的代码分析,就一目了然,还可以看 《unix advanced programming 》,不过我觉得它讲的都是老的Unix ,对Linux只能是参考而已!

误区2>

              signal() 发送的是不可靠的信号 ,而 sigaction()发送的是可靠的信号

             

              只要是131的信号,它就是不可靠的信号。无论在注册信号处理函数的时候用的是sigaction() ,还是signal() ,只要你发送的信号 是  1-31,那么就是不可靠的信号。中国有句俗语叫”烂泥扶不上墙“,我看放在这里挺合适!

 

signal()和 sigaction()的差别到底在哪里呢?   通过对比一看便知:

   对于signal() ,它的kernel实现函数,也叫系统调用服务历程sys_signal()

 

==>kernel/signal.c

asmlinkage unsigned long

sys_signal(int sig, __sighandler_t handler)

{

       struct k_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 : (unsigned long)old_sa.sa.sa_handler;

}

 

而sigaction()函数的kernel实现是: sys_sigaction()

==>arch/i386/kernel/signal.c

asmlinkage int

sys_sigaction(int sig, const struct old_sigaction *act,struct old_sigaction *oact)

{

       struct k_sigaction new_ka, old_ka;

       int ret;

 

       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()这个结构体的,差别在下面标出来了

 

       struct sigaction {

       __sighandler_t sa_handler;  //2// typedef void (*__sighandler_t)(int);  signal()和sigaction()函数都要求要户提供信号处理函数

       unsigned long 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 ,代码如下:

      

       ==>arch/i386/kernel/signal.c

static void

handle_signal(unsigned long sig, struct k_sigaction *ka,

             siginfo_t *info, sigset_t *oldset, struct pt_regs * regs)

{

       ...........................................................

       /* Set up the stack frame */

       if (ka->sa.sa_flags & SA_SIGINFO)

              setup_rt_frame(sig, ka, info, oldset, regs);

       else

              setup_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("<0> the signal (%d) handler will reset to SIG_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("Have caught sig N.O. %d\n",signo);

          //按照kernel代码,应该还要有signal(signo,sigterm_handler);   才对呀 ,但事实上,我们大家都知道没有必要这样用 ,为什么呢? 请前往论坛讨论: =

}

 

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");

      

           return 0;

}

 

但是执行结果确实: 

 

333333333333333333333333 也打出来了, 这就又说明signal函数 ,不需要反复注册信号处理函数 ,  这不就矛盾吗? 

 

所以现在问题就是

if (ka->sa.sa_flags & SA_ONESHOT){ 

         ka->sa.sa_handler = SIG_DFL;

 是在什么情况下改变了 sigaction->sa_flags (去掉了 SA_ONESHOT 标志呢?)我在代码里面搜索不到啊!

 如果感兴趣的朋友可以前往论坛讨论:

 

 

2>       僵尸进程:也叫Zombie进程: 

 

主 要指:进程结束后,它的父进程没有调用wait或waitpid()对子进程进行回收, 所以子进程还干占着一个task_struct 呢,关于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 for SIGCHLD: 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 Zombie process)

比如:这样写:

while(waitpid(-1,NULL,WNOHANG) > 0)  {   //自动处理所有的僵尸进程,当然你可以不用while,只调用一次,看需要 : 比如父进程个http server,就会fork()出很多子进程 , 所以while()是有必要的

//WNOHANG 很关键,如果没有僵死进程,就马上返回 ,这样while()才可以结束啊 , 可是wait()就没有这个参数,  所以wait就阻塞了。所以一般情况下,我们用waitpid还是最好的了!

   ;//什么也不必作了, 可以打印看看到底回收了哪些进程pid

}

 

如果你没有用上面任何一个办法, 太遗憾了, 就会出现僵尸进程了! 

ps ax 命令可能会显示: 

      22149 tty8  S  000   test_pro

      22150  ?    Z  0:00    [test_pro ]   //这就是僵尸进程  Z 就是Zombie的意思 , 你用kill -9 也无法杀掉它 。

怎么杀掉呢?  你就杀掉 它的父进程就好了。 kill -SIGTERM 22149 ,  你在ps ax 看看  ,两个进程都没有了。

 

 

 

避免僵尸进程的第三种办法

 

 

FYI :个人不推荐! 因为上面两种方法已经够用了, 除非你还有其他的要求,比如 使子进程无法获得控制终端,这种情况下, 就必须fork()两次了 。 否则一般情况下,我们需要父子进程同步和通信的, 父亲和儿子交流尚且比较方便(用pipe最好,配合使用select()) , 你让爷爷和孙子通信不是比较困难吗?  两代人的代沟呢。。。。

 

当然你也可以fork()两次,  父亲(比如http server,循环处理,不死鸟) ->  儿子(死掉) -> 孙子进程(处理每次的任务,正常结束,就不会成为Zombie 

       杀死儿子,有点残忍,不过为了国家和人民的利益大义灭亲吧,咳!!都是linux也的祸

下面是事例代码:  

pid_t pid = 0;

pid = fork();

if(pid < 0)

  //error

  exit( -1);

else if(pid > 0)

  //这里可能Server一类的, 父亲进程永远不会结束的,式while() 循环

else {

 

  //现在儿子 process 了,

  if(pid = fork() < 0)

  //error

        exit(-1);

  else if(pid > 0) //儿进程也结束了

        exit(0);//立刻杀死儿子进程 ,这样孙子就成孤儿了, 有爷爷也算孤儿? 咱们国家就这么规定的,^_^ 孙子进程会被init1)领养的。这样孙子就有饭吃了, 呵呵!看来全世界都一样啊!

  else { //到孙子进程了。

        //执行一些代码

        exit(0);

   }

}

 

对于 原理其实很简单: 儿子死了, 只有孙子了, 孙子是孤儿了,那么init(1)进程就会领养这个 孤儿,  同时孤儿就认为init(1)就是它的父进程,由init进程负责收尸!  

 

 

3> 特殊的 SIGCHLD 信号

 

SIGCHLD 特殊在哪里呢?? 一般情况下, 子进程结束后 ,都会给父进程发送 SIGCHLD 信号 ,但是这不是绝对的 。

当一个父进程fork()一个子进程后,  当父进程没有为SIGCHLD 注册新的处理函数,比如默认 SIG_DFL  ,那么当子进程结束的时候,就不会给父进程发送SIGCHLD 信号 。 从代码的角度: 执行到send_sig_info(),会在isgnore_signal() 函数里面做是否要发信号的判断,结果 SIGCHLD被忽略了!

就是普通的进程,你也不能随便发一个信号唤醒它, 比如 发 SIGCONT 信号,

 

且看下面的代码分析:

/*

 * Determine whether a signal should be posted or not.

 *

 * Signals with SIG_IGN can be ignored, except for the

 * special case of a SIGCHLD.

 *

 * Some signals with SIG_DFL default to a non-action.

 */

 //定义了那些信号要被忽略!

static int ignored_signal(int sig, struct task_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 : global action (kill - spread to all non-blocked threads)

 *    = 0 : ignored

 *    > 0 : wake up.

 */

//

 #define SIG_DFL     ((__sighandler_t)0)  /* default signal handling */

#define SIG_IGN      ((__sighandler_t)1)  /* ignore signal */

#define SIG_ERR      ((__sighandler_t)-1) /* error return from signal */

//

static signal_type(int sig, struct signal_struct *signals)

{

       unsigned long handler;

 

//-----------------------------空信号 ignore  -----------------------------

       if (!signals)

              return 0;   //

      

       handler = (unsigned long) signals->action[sig-1].sa.sa_handler;

       if (handler > 1)   //该信号有特定的信号处理函数不能ignore ,必须wake_up ()

              return 1;     //can't ignore

 

// -----父进程设置SIGCHLD 的处理方式为 SIG_IGN : 子进程结束的时候不会给父进程发信号,也就无法唤醒了。

       /* "Ignore" handler.. Illogical, but that has an implicit handler for SIGCHLD */

       if (handler == 1)  

              return sig == SIGCHLD;//当信号是 SIGCHLD的时候,信号不能被忽略,其他的要被活略

 

 

// --------------------------当把信号设置为SIG_DFL 时的情况---------------------

       /* Default handler. Normally lethal, but.. */

       switch (sig) {

 

       /* Ignored */

       case SIGCONT: case SIGWINCH:

       case SIGCHLD: case SIGURG:

              return 0; //这些信号忽略干脆就忽略了 ,那你可能奇怪了?那SIGCONT 信号如何唤醒 TASK_STOPPED状态的进程呢?  如果你有这个疑问 ,请看 5>的讨论!

       /* Implicit behaviour */    //can't ignore

       case SIGTSTP: case SIGTTIN: case SIGTTOU:  //这些信号就时要中止进程的

              return 1; //这些信号会唤醒该进程的, 程序会接着望下跑的,  最后 把进程的状态置为 TASK_STOPPED 的。

 

       /* Implicit actions (kill or do special stuff) */

       default:  //对于象SIGKILL , SIGTERM ,SIGQUIT 这样的信号直接就默认操作,一般就是terminate 该进程

              return -1;

       }

}

怎么在应用程序验证上述kernel的代码呢

既然提到了”唤醒“ ,肯定要用上  pause(2)函数了, 且看pause(2)的manunal :

DESCRIPTION

       The  pause  library function causes the invoking process (or thread) to

       sleep until a signal is received that either terminates it or causes it
阅读(2337) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~