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

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

 

 

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

 

另外提醒大家 <<UNIX Adanced Programming>> 可不是 《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( 1 31 , 注意这里讨论是 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;  // 跳 出了正常执行的范围

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

}

 

 

正确的: 1 31 都是不可靠的信号! 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(&current->sigmask_lock);

              sigorsets(&current->blocked,&current->blocked,&ka->sa.sa_mask);

              sigaddset(&current->blocked,sig);

              recalc_sigpending(current);

              spin_unlock_irq(&current->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);   才对呀 ,但事实上,我们大家都知道没有必要这样用 , 为什么呢? 请前往论坛讨论: 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");

      

            return 0;

}

 

但是执行结果确实: 

 

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()对子进程进行回收, 所以子进程还干占着一个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 = &current->sig->action[signr-1];//&current->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  0 00   test_pro

      22150  ?    Z   0:00    [test_pro <defunct>]    //这就是僵尸进程  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);// 立刻杀死儿子进程 ,这样孙子就成孤儿了, 有爷爷也算孤儿? 咱们国家就这么规定的, ^_^ 孙子进程会被 init 1 )领养的。 这样孙子就 有饭吃了, 呵呵!看 来全世界都一样啊!

  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

       to call a signal-catching function.

RETURN VALUE

       The  pause  function only returns when a signal was caught and the sig-

       nal-catching function returned. In this  case  pause  returns  -1,  and

       errno is set to EINTR.

 

上面的手册说得很清楚了, 对于pause过的进程,  只 有发送类似 SIGKILL , SIGQUIT, SIGTERM 的信号或者是 注册了新的处理函数(对于父子进程而言的),才可以唤醒它!

下面是测试的例子: 

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

#include <sys/stat.h>

#include <sys/wait.h>

 

 

void sig_handler(int signo)

{

        printf("signo = %d/n",signo);

        if(signo == SIGCHLD) {

                pid_t child_pid = 0;

                int status = 0;

 

                 printf("into singal handler/n");

 

                //wait() fault : it will blocked if no defunced  process ,so I will use waitpid(.. WNOHANG) ,it will return immidietly

 

              while( (child_pid = waitpid(-1,&status,WNOHANG) )> 0) // 循环回收所有 的 Zombie 进程

                      printf("child_pid = %d  ,  status = %d/n",child_pid,status);

        }

 

}

 

int main(void)

{

        pid_t pid = 0;

 

/* 感兴趣的读 者可以试试!你可以试着注释掉下面的两个 signal() 函数, 用这个,试 着回答下面的两个问题

        struct sigaction sa,old_sa;

        sigaction(SIGCHLD,&sa,&old_sa);

        sa = old_sa;

        sa.sa_flags |= SA_NOCLDSTOP; // 当子进程结束的 时候,阻止子进程向其父进程发 SIGCHLD

        sa.sa_handler = SIG_IGN;

        sigaction(SIGCHLD,&sa,NULL);

*/

 

         signal(SIGCHLD,sig_handler);  // 避免僵尸进程

         // signal(SIGCHLD,SIG_IGN);    // 注释上面那行,用这行 再试着重新回答下面的两个问题

         pid = fork();

        if(pid < 0) {

                perror("create child process failure /n");

                exit(-1);

        }

        else if(pid == 0) {

                setsid();

                umask(0);

                 close(0);

                //close(1);

                close(2);

                chdir("/");

               

                 sleep(5); // 为了确保 testing 的正确性, 需要确保子进程结束之前 ,父进程已经 pause 了,所以子进程 sleep(5)

 

                printf("child ................... /n");

                exit(0);

        }

        else {

                printf("parent pause() ............../n");

                pause();   // 父进程会被唤醒吗???????????????????

                fflush(stdout);

                printf("parent process has been waken up /n");

                return 0;

        }

}

 

大家可以思考一下?

1  子进程会成为孤儿进程吗?                     

2  父进程会被唤醒吗?

 

 

如果你看明白了上面的kernel代码 ,你就很快明白了答案了: (上 面没有被注释的代码的运行结果 ,注释的部分,读者自己可以验证试着读源代码解释程序行为!

(1)答:不会,正常结束 ,因为有声明 :signal(SIGCHLD,SIG_IGN);   //避免僵尸进程

            2 )答:(至少在linux-2.4.24 上 会,我在linux-2.4.20-8 上试了一下,就不会唤醒, 代码肯定不同了), 因为子进程结束后,会给parent 进 程发送一个SIGCHLD 信号, 此信号会唤醒 parent 进程! 有关这方面的讨论可以访问论坛页:http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=607949&page=0&view=collapsed&sb=5&o=7&fpart=&vc=1

 

 

               

 

4> 在给一个进程发送信号的过程中,只要目标进程(迟早要成为当前running 的进程)没有block该信号, kernel都会调用 wake_up_process () 函数来唤醒它 , 为什么呢?   因为 只有当前活动进程才会handle signal ,过程是这样的:  当一个进程被唤醒后, 它肯定处于kernel空间 , 在它即将返回道用户空间的时候, 开始检测 task_struct->sigpending ,如果为1   就说明该进程收到了信号(现在这个信号叫pending信号,只要有pending 信号, sigpending 就是等于1 ) ,开始调用do_signal() 函数来处理 , 也就是重要的一点 , 只有当前活动进程才可以处理信号(类似中断, 当一个进程收到一个信号后, 就active了, 至于该信号怎么处理,是 "kernel处理信号的任务“ 。

 

具体说明如下:

关于这个函数我觉得也也值得注意!

 

1> 当 向一个进程发送 SIGCONT 信号时候,

如果进程本身还有一些类似SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU 等 会使进程停止的信号,

就要把他们删掉

 

2> 如 果想要停止某个进程的话,

就一定要删除SIGCONT 信 号(这个肯定,否则kernel 在处理的时候,

进程优先处理SIGCONT 信 号,然后再处理这4 个 ,那就多余了,没有必要。既然要停止,

就直接停止,忽略之前的SIGCONT 操 作)

 

static void handle_stop_signal(int sig, struct task_struct *t)

{

   switch (sig) {

   case SIGKILL: case SIGCONT:

          /* Wake up the process if stopped.  */

          if (t->state == TASK_STOPPED)

              wake_up_process(t);

          t->exit_code = 0;

          rm_sig_from_queue(SIGSTOP, t);    //删除这些未决信号

          rm_sig_from_queue(SIGTSTP, t);

          rm_sig_from_queue(SIGTTOU, t);

          rm_sig_from_queue(SIGTTIN, t);

          break;

 

   case SIGSTOP: case SIGTSTP://因为这些信号排在SIGCONT信号的后面,如果不删除队列中的SIGCONT信号, 在do_singal()会先执行SIGCONT的操作的,这样就多次一举了。 Note : 我看代码里面是 kernel 在检测信号的时候, 先处理sigset_t 类 型值中前面的bit 对应的信号

   case SIGTTIN: case SIGTTOU:

          /* If we're stopping again, cancel SIGCONT */

          rm_sig_from_queue(SIGCONT, t);

          break;

   }

}

 

             

5> 对于 3> 提到的特殊的SIGCHLD 信号, 我们提到了到底哪些信号要被忽略。那么对于善于思考您,不知道你是否有此疑问:

       (1)对于一个普通的进程发SIGCONT 信号肯定是要被kernel忽略的;

       (2)但是一般的上层熟悉signal用法的R&D都知道 SIGCONT信号是SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU的后继信号,是专门用来把挂起的进程恢复running的,根据上 面的结论SIGCONT不是也要被忽略的吗???? 那进程又怎么可能恢复执行呢?

       答 案: 要回答这个问题就要弄清楚kernel 在发一个signal 的流程:  

1.     判断信号是否是bad的,  参考 kernel/signal.c ==>bad_signal()

2.     处理状态是TASK_STOPPED的进程, 如果是 , 就调用wake_up_process() , 参考 kernel/signal.c==>handle_stop_signal() 函数

3.     判断哪些信号该忽略,参考 kernel/signal.c==>ignore_signal()

4.     最后 调用 deliver_signal() 正是发送信号(其实发送信号,说白了,就是修改task_struct 相应的数据成员),发送完信号成功后,如果进程是处于TASK_INTERRUPTABLE 状态的(且信号没有被阻塞), 就唤醒它。

到此为止,信号就算正式发送完毕了。

 

所以,你现在你就知道答案了:

虽然SIGCONT 信号要在 3.被忽略,可是2. 却可以被执行, 进程被变为TASK_RUNNING 状态了( TASK_RUNNING状态的进程有很多,但是同一个时间,占用CPU的就只有一个)

那为什么要必须把 TASK_STOPPED 状态的进程 变为 TASK_RUNNING 呢?  也许你可以从 4> 中得到答案!

 

 

 

6> 关于信号的技巧:

·   Q :如何判断一个进程是否还活着 ?

A : 发一个空信号就好了, 什么?什么是空信号?  就 是  0

·   Q :如何暂时挂起某个进程?

  A: 如 果你想暂时挂起某个进程的运行,以后要恢复,请用 SIGSTOP 信号 , 想重新执行该进程的时候,就发SIGCONT 信号。 记住当你对一个进程发SIGSTOP 信号的时候,  子 进程会给父进程发SIGCHLD 信号,这样父进程会被唤醒。具体为什么请看 3>

  • Q :我想父子进程间通信而且同步,比如: 父进程需要等待子进程执行的结果,然后最会退出, 当然不能无限期等待,比如等 5 秒钟 , 用信号好吗? 

A :你以为信号是万能的? 上述三个要求,我们一个个分析

  •  
    1. 同步:这是信号的最基本的功能了, 无论你用signal()/kill() 系 列还是  sigaction()/sigqueue() 系 列肯定能满足你的要求 。
    2. 通信:如果你用kill(2) 函数发信号, 通信肯定不可能了, 传递不了信息啊,  这个时候你可以用sigqueue ()函数 ,里面有项参数就是用来传递数据的 ,内核里面有个结构叫 siginfo_t ,就是干这个用的。具体请看http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index2.html (进程间通信 信号(下),里面举了个例子)
    3. 父进程有限时间等待子进程,in my opinion ,我觉 得signal 好像是干不了这活 。

另辟蹊径吧! 我原来写过一个用无名管道/select 来实现 上述功能非常简洁方便。

你可以随意copy 我的代码,

 

思路:

       父 进程创建了一个无名管道, 子进程在管道写端写入value , 父进程呢通过select ()函数检测管道的读端,如果5 秒钟内读端 无反应,说明超时,否则就可以读value ! 这样就是简单的实现父子进程同步,通信,且有限时间等待的要求!

 

       pid_t pid = 0;

       int fd[2];                        //pipe operation :

              unsigned char share_buffer[3];    //share info between parent process and child process

             

       fd_set read_fds;

       int fd_max ;     /* for select */

       struct timeval tv;

       int select_rc = 0;

 

 

       if(pipe(fd) < 0)

       {

              perror("create pipe");

              return SERVER_PIPE_FAIL;

       }

      

       signal(SIGCHLD,SIG_IGN);   // 防止出现Zombie 进 程,如果忘记了,复习一下上面!

      

       pid = fork();

       if(pid < 0)  {

              perror("fork a child to download file failure");

              return SERVER_FORK_FAIL;

       }

       else if(pid == 0) {

              unsigned char buffer[3]; // 子进程往里面写数据

              int rc = -1;

 

              setsid();

              /* generate a daemon process

                       * setsid()是创建daemon的关键函数, (1)成为session的leader process ,

                       * (2)成为进程组的leader process ,(3)没有终端

                       */

                     umask(0);

                     /* 当 创建文件的时候和目录的时候 默认是 umask(022)

                       * umask ()函数可以改便创建文件时候的默认许可权位, 据个例子,当你用root 权 限

                       * 创建一个文件 , > bob.txt   , 你会发现: ll bob.txt ,  显 示 -rw-r--r--

                       * 这就是umask(022) 的作用 , 022 对应的二进制: 000     010 010 ,表示 对于

                       * 组内用户和其他的用户 不可有w 的权限。   w 位置1 就 表示不可以w !以此类推!

                     close(0);     // 关 闭标准输入

              close(1);     // 关闭标准输出

              close(2);     // 关闭标准错误输出

              chdir( / );

 

              close(fd[0]); // 把管道的读 一端 关闭,只留写 一端 即可

 

// 执 行你的程序 ,你的code

。。。

// exit0 )之前,通知你的父进程你的执行结果, rc 就是执行结果

 

buffer[0] = rc;     

buffer[1] = '/0';

buffer[2] = '/0';

write(fd[1],buffer,sizeof(buffer));

close(fd[1]); // end of write to the "write pipe" ,must close it

exit(0);

              }  // 子 进程结束!

              // 父 进程内

              close(fd[1]); // 关闭写端 ,只要留着读端即可!

                    

              FD_ZERO(&read_fds);  //clear the read_fds

               FD_SET(fd[0], &read_fds);

              

               tv.tv_sec = 5  // 假设父进程就等待5s

               fd_max = fd[0]+1;

                     

               //select 不熟悉select() 的朋友可以到google 搜索它的用法,一定要掌握!

               select_rc = select(fd_max,&read_fds,NULL,NULL,&tv);

             

              if(!select_rc) //wait 超 时 ,

                     // 你 的处理code , 也许这正是你期待的呢!

              else {

                     read(fd[0],share_buffer,sizeof(share_buffer));

                     jprintf("read successfully/n");

                     jprintf("in father :buffer[0] = %d/n",share_buffer[0]);

                     jprintf("in father :buffer[1] = %d/n",share_buffer[1]);

                     jprintf("in father :buffer[2] = %d/n",share_buffer[2]);

                     close(fd[0]); //close read pipe , 读完毕后记着关闭它!

 

                     // 你 可以根据buffer 读出的内容作进一步的处理

                     // 你 的code

              }

              ……………………………… ..

                    

       附 录:

 

参考资料:

1.       linux 内核源代码情景分析》上册 ,通过参考它的分析,我才真正的读懂了signal 的一部分源码,果然是经验老道的大师写的,花了80 块 钱买的,超值,就是200 也买!

2.       《深 入理解linux 内核》,虽然没有《情景分析》那么详尽细节, 但是总是有画龙点睛之笔,让人惊叹作者的功力之深看问题的锐利!建议先看它,看代码的时候,再看情景分析!

3.       Unix 环境高级编程》,是每个linux 必备的手 册,其实我认为它signal 一章讲的真是一般,而且很罗嗦,半天让人摸不着头脑,主要是它老是比 较BSDSystem V ,把搞linux 的人头晕了,也不知道它要说什么,感觉上好像是在抄手册(我没有对作者不敬的意思,毕竟入土为安 吗)。但不管怎么说仍然是极好的书。在当年可算是开山之作了!但历史局限性肯定是免不了的了!向Richard Steven 致敬!要是还活着该有多好啊,太可惜了!

4.       进 程间通信 信号(上) http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html 这位博士分析的相当透彻,博士可不是白当的,理论水平就是高,表达能力强,我一看就明白了。绝对的好文章!强烈推荐!

5.       进 程间通信 信号(下) http://www-128.ibm.com/developerworks/cn/linux/l-ipc/part2/index2.html 一个人写的!

6.       signal () ,signaction () 等其他函数manual

 

文档完

 

作者:张林宝 ,英文名bob 

birth 1980/3/28  联 系方式: 中国江苏苏州工业园区星海街26 号中怡科技5

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值