Linux signal那些事儿【转】

转自:http://blog.chinaunix.net/uid-24774106-id-4061386.html

Linux编程,信号是一个让人爱恨交加又不得不提的一个领域。最近我集中学习了Linux的signal相关的内容,分享出来,也为防止自己忘记。
   
信号的本质是异步。异步一这个词,听着高端大气上档次,又让人云山雾绕,其则不然。其实我们想想,我们这个世界是异步的,每个人干事儿,并不总是A->B->C->D这种。比如我在网上买了东西,我其实并不知道快递几时能到。我可能在公司里面,在喝水,在回邮件,在查bug,在写代码,突然收到了快递小哥的电话,注意这就是信号的delivery。由于快递的到来,我不得不停下我手头的活儿,去签收快递。这就是传说中的典型的异步。我不知道快递小哥几时给我电话,但是我收到电话就去签收,这是我的信号处理函数。更高级一点,如果我在参加重要的会议,我可能需要屏蔽快递小哥的电话(假如我知道其电话),这已经是linux下信号的高级应用(sigprocmask)了。
   
信号是一种机制,是在软件层次对中断机制的一种模拟,内核让某进程意识到某特殊事情发生了。强迫进程去执行相应的信号处理函数。至于信号的来源可能来自硬件如按下键盘或者硬件故障(如ctrl+c发送SIGINT),可能来自其他进程(kill,sigqueue),可能来自自己进程(raise)。 
    信号的本质是一种进程间的通信,一个进程可以向另一个进程发送信号,至少传递了signo这个int值。实际上,通信的内容,可以远不止是signo,可以通过SA_SIGINFO标志位通知进程去取额外的信息。
    我痛恨片汤话儿,可是上面一大坨片汤话儿,却真真的道出了信号的本质。
    前面也提到了,signal是个让人爱恨交加的feature,原因在于沉重的历史包袱。下面我将一一道来。
    在上古时期,UNIX就已经有了signal这个feature,但是当时的signal存在几个问题:
   1 传统的信号处理函数是一次性的,而非永久性的。
    linux为了向下兼容,依然实现了这个有缺陷的signal系统调用。你可看到signal系统调用的内核代码中有SA_ONESHOT这个标志位。

  1. #ifdef __ARCH_WANT_SYS_SIGNAL
  2. /*
  3.  * For backwards compatibility. Functionality superseded by sigaction.
  4.  */
  5. SYSCALL_DEFINE2(signal, int, sig, __sighandler_t, handler)
  6. {
  7.     struct k_sigaction new_sa, old_sa;
  8.     int ret;
  9.     new_sa.sa.sa_handler = handler;
  10.     new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK;
  11.     sigemptyset(&new_sa.sa.sa_mask);
  12.     ret = do_sigaction(sig, &new_sa, &old_sa);
  13.     return ret ? ret : (unsigned long)old_sa.sa.sa_handler;
  14. }
  15. #endif /* __ARCH_WANT_SYS_SIGNAL */

    这个SA_ONESHOT标志位,等同于SA_RESETHAND标志:在arch/x86/include/uapi/asm/signal.h中有:

  1. #define SA_ONESHOT    SA_RESETHAND

   
信号产生,到信号处理函数开始执行,中间肯定是有时间差的。内核开始开始强迫进程对信号作出响应,这叫作信号的传递。也就是说信号产生,内核只是在进程描述符记录了一笔,该进程收到信号X一枚,并没有马上强迫进程对信号作出响应。已经产生但尚未传递的信号叫挂起信号。对于非实时而言,信号不排队,位图占个位即可。对于实时信号,则排队,同一信号可能有多个挂起信号。这个不多说,后面自然提到。
    
    上图反映了内核如何传递信号。基本就是选择一个挂起信号,然后处理一个信号。get_signal_to_deliver 是在进程中选择一个信号来handle。代码在kernel/signal.c,其中有如下code:

  1.         if (ka->sa.sa_handler == SIG_IGN) /* Do nothing. */
  2.             continue;
  3.         if (ka->sa.sa_handler != SIG_DFL) {
  4.             /* Run the handler. */
  5.             *return_ka = *ka;
  6.             if (ka->sa.sa_flags & SA_ONESHOT)
  7.                 ka->sa.sa_handler = SIG_DFL;
  8.             break; /* will return non-zero "signr" value */
  9.         }

    我们看到了linux也实现了signal这个有缺陷的系统调用。传统的signal系统调用,他的信号处理函数是一次性的,执行过后,该信号的信号处理函数就变成了SIG_DFL。
    值得一提的是,glibc的signal函数,调用的已经不是传统的signal系统调用,而是rt_sigaction系统调用,这种一次性的缺陷早已经解决了。怎么证明: 

  1. manu@manu-hacks:~/code/c/self/signal$ cat signal_fault_1.c
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <signal.h>
  5. #include <string.h>
  6. #include <errno.h>
  7. #define MSG "OMG , I catch the signal SIGINT\n"
  8. #define MSG_END "OK,finished process signal SIGINT\n"
  9. int do_heavy_work()
  10. {
  11.     int i ;
  12.     int k;
  13.     srand(time(NULL));
  14.     for(i = 0 ; i < 100000000;i++)
  15.     {
  16.         k = rand()%1234589;
  17.     }
  18. }
  19. void signal_handler(int signo)
  20. {
  21.     write(2,MSG,strlen(MSG));
  22.     do_heavy_work();
  23.     write(2,MSG_END,strlen(MSG_END));
  24. }
  25. int main()
  26. {
  27.     char input[1024] = {0};
  28. #if defined TRADITIONAL_SIGNAL_API
        if(syscall(SYS_signal ,SIGINT,signal_handler) == -1)
    #elif defined SYSTEMV_SIGNAL_API
        if(sysv_signal(SIGINT,signal_handler) == -1)
    #else
        if(signal(SIGINT,signal_handler) == SIG_ERR)
    #endif
  29.     {
  30.         fprintf(stderr,"signal failed\n");
  31.         return -1;
  32.     }
  33.     printf("input a string:\n");
  34.     if(fgets(input,sizeof(input),stdin)== NULL)
  35.     {
  36.         fprintf(stderr,"fgets failed(%s)\n",strerror(errno));
  37.         return -2;
  38.     }
  39.     else
  40.     {
  41.         printf("you entered:%s",input);
  42.     }
  43.     return 0;
  44.     
  45. }

    编译的时候,我没有定义SYSTEMV_SIGNAL_API,就是标准的glibc的signal函数,我用strace跟踪glibc的signal函数调用的系统调用是: 

  1. rt_sigaction(SIGINT, {0x8048736, [INT], SA_RESTART}, {SIG_DFL, [], 0}, 8) = 0

    测试结果如下:

  1. manu@manu-hacks:~/code/c/self/signal$ gcc -o signal_glibc signal_fault_1.c
  2. manu@manu-hacks:~/code/c/self/signal$ ./signal_glibc
  3. input a string:
  4. input^COMG , I catch the signal SIGINT
  5. ^COK,finished process signal SIGINT
  6. OMG , I catch the signal SIGINT
  7. OK,finished process signal SIGINT
  8. ^COMG , I catch the signal SIGINT
  9. OK,finished process signal SIGINT
  10. ^COMG , I catch the signal SIGINT
  11. OK,finished process signal SIGINT
  12. ^Z
  13. [1]+ Stopped ./signal_glibc

    我们安装的信号处理函数并不是一次性的,原因就是glibc的signal函数调用的函数并非是signal系统调用,并没有SA_ONESHOT标志位。
    我们如何体验下老古董的signal,glibc提供了一个sysv_signal接口,manual中这样描述:

  1.    However sysv_signal() provides the System V unreliable signal semantics, that is: a) the disposition of the sig‐
  2.    nal is reset to the default when the handler is invoked; b) delivery of further instances of the signal is not
  3.    blocked while the signal handler is executing; and c) if the handler interrupts (certain) blocking system calls,
  4.    then the system call is not automatically restarted.

    对于我们的例子只需要:

  1. gcc -DSYSTEMV_SIGNAL_API -o signal_sysv signal_fault_1.c

    我们看下:

  1. manu@manu-hacks:~/code/c/self/signal$ ./signal_sysv
  2. input a string:
  3. ^COMG , I catch the signal SIGINT
  4. ^C
  5. manu@manu-hacks:~/code/c/self/signal$ man sysv_signal

    第二个ctrl+C导致了进程的推出,原因是sysv_signal这种传统的signal的安装函数是一次性的。strace也证明了这一点:

  1. rt_sigaction(SIGINT, {0x8048756, [], SA_INTERRUPT|SA_NODEFER|SA_RESETHAND}, {SIG_DFL, [], 0}, 8) = 0

    还记得么:

  1. #define SA_ONESHOT SA_RESETHAND

    我们发现sysv调用的不是signal系统调用,而是rt_sigaction系统调用。如果你非要品尝传统的signal系统调用,这也不难。

  1. gcc -DTRADITIONAL_SIGNAL_API  -o signal_traditional signal_fault_1.c 

    我们发现第二个SIGINT信号的信号处理函数已经SIG_DFL,使进程退出了。

  1. manu@manu-hacks:~/code/c/self/signal$ ./signal_traditional
  2. input a string:
  3. ^COMG , I catch the signal SIGINT
  4. ^C

    我们通过strace可以证明,的确调用了signal系统调用: 

  1. signal(SIGINT, 0x8048736) = 0 (SIG_DFL) 

    2早期的信号,没有屏蔽正在处理的信号。
   
如何证明这一点呢?我上面的例子中故意在信号处理函数中做了很heavy很耗时的操作,从而容易造出处理信号A的时候,另一信号A又被deliver的场景。
    因为do_heavy_work是个很耗费时间的操作,信号处理完成我们会在标准错误上输出处理完成的语句,这就表征了信号处理结束了没有。
    我们看下传统signal的,收到一个SIGINT的信号的情况:

  1. manu@manu-hacks:~/code/c/self/signal$ ./signal_traditional
  2. input a string:
  3. ^COMG , I catch the signal SIGINT
  4. OK,finished process signal SIGINT
  5. fgets failed(Interrupted system call)
  6. manu@manu-hacks:~/code/c/self/signal$

    如果我在进程处理信号处理函数的时候,再次发送一个SIGINT,这个SIGINT也可能被内核deliver。那么信号处理函数就被中断掉,

  1. manu@manu-hacks:~/code/c/self/signal$ ./signal_traditional
  2. input a string:
  3. ^COMG , I catch the signal SIGINT
  4. ^C
  5. manu@manu-hacks:~/code/c/self/signal$

    我们看到我们收到了I catch the signal SIGINT的打印,但是,并没有收到OK,I finished process signal SIGINT,这表明传统的signal并没有屏蔽正在处理的信号。
    那么我们现在的glibc的signal函数如何?
    strace又来帮忙了?

  1. rt_sigaction(SIGINT, {0x8048736, [INT], SA_RESTART}, {SIG_DFL, [], 0}, 8) = 0

    glibc的signal函数,调用的是rt_sigaction 系统调用:
    

  1. SYSCALL_DEFINE4(rt_sigaction, int, sig,
  2.         const struct sigaction __user *, act,
  3.         struct sigaction __user *, oact,
  4.         size_t, sigsetsize)
  5. struct sigaction {
  6.     union {
  7.      __sighandler_t _sa_handler;
  8.      void (*_sa_sigaction)(int, struct siginfo *, void *);
  9.     } _u;
  10.     sigset_t sa_mask;
  11.     unsigned long sa_flags;
  12.     void (*sa_restorer)(void);
  13. }

    我们把strace中的信息,和sigaction数据对比,发现,[INT],就是传说中的sa_mask,当处理SIGINT的时候,看起来是在处理SIGINT信号处理函数的时候,SIGINT会被被屏蔽,防止重入。实际如何呢? 

  1. manu@manu-hacks:~/code/c/self/signal$ ./signal_glibc
  2. input a string:
  3. ^COMG , I catch the signal SIGINT
  4. ^C^C^C^COK,finished process signal SIGINT
  5. OMG , I catch the signal SIGINT
  6. ^C^COK,finished process signal SIGINT
  7. OMG , I catch the signal SIGINT
  8. OK,finished process signal SIGINT
  9. ^COMG , I catch the signal SIGINT
  10. OK,finished process signal SIGINT
  11. ^COMG , I catch the signal SIGINT
  12. ^Z
  13. [2]+ Stopped ./signal_glibc

    从未出现OMG,I catch the SIGINT连续出现。这是偶然还是必然呢?答案是必然,内核是如何做到的呢?
    在上图的handle_signal函数的末尾,调用了signal_delivered函数: 

  1. /**
  2.  * signal_delivered -
  3.  * @sig:        number of signal being delivered
  4.  * @info:        siginfo_t of signal being delivered
  5.  * @ka:            sigaction setting that chose the handler
  6.  * @regs:        user register state
  7.  * @stepping:        nonzero if debugger single-step or block-step in use
  8.  *
  9.  * This function should be called when a signal has succesfully been
  10.  * delivered. It updates the blocked signals accordingly (@ka->sa.sa_mask
  11.  * is always blocked, and the signal itself is blocked unless %SA_NODEFER
  12.  * is set in @ka->sa.sa_flags. Tracing is notified.
  13.  */
  14. void signal_delivered(int sig, siginfo_t *info, struct k_sigaction *ka,
  15.             struct pt_regs *regs, int stepping)
  16. {
  17.     sigset_t blocked;
  18.     /* A signal was successfully delivered, and the
  19.      saved sigmask was stored on the signal frame,
  20.      and will be restored by sigreturn. So we can
  21.      simply clear the restore sigmask flag. */
  22.     clear_restore_sigmask();
  23.     sigorsets(&blocked, &current->blocked, &ka->sa.sa_mask);
  24.     if (!(ka->sa.sa_flags & SA_NODEFER))
  25.         sigaddset(&blocked, sig);
  26.     set_current_blocked(&blocked);
  27.     tracehook_signal_handler(sig, info, ka, regs, stepping);
  28. }

    这个函数很有意思,只要用户没有指定SA_NODEFER标志位,当前处理的信号总是加入到屏蔽信号之中。深入理解Linux内核在也提到了这一点。经典教材是这么说的:

  1. 当进程执行一个信号处理程序的函数时,通常屏蔽相应的信号,即自动阻塞这个信号,直到处理程序结束。因此,所处理的信号的另一次出现,并不能中断信号处理程序,所以信号处理函数不必是可以重入的。

    这个结论很震惊吧。是的你用glibc的signal函数,不必担心信号处理函数的嵌套问题。至于重入问题我们后文讨论。
    那么传统的signal系统调用和sysv_signal又如何?为何他们存在信号的可重入问题?   

  1. SYSCALL_DEFINE2(signal, int, sig, __sighandler_t, handler)
  2. {
  3.     struct k_sigaction new_sa, old_sa;
  4.     int ret;
  5.     new_sa.sa.sa_handler = handler;
  6.     new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK;
  7.     sigemptyset(&new_sa.sa.sa_mask);
  8.     ret = do_sigaction(sig, &new_sa, &old_sa);
  9.     return ret ? ret : (unsigned long)old_sa.sa.sa_handler;
  10. }

#define SA_NOMASK SA_NODEFER

    至于sysv_signal

  1. rt_sigaction(SIGINT, {0x8048756, [], SA_INTERRUPT|SA_NODEFER|SA_RESETHAND}, {SIG_DFL, [], 0}, 8) = 0

    不多说了,不作死就不会死,signal系统调用和sysv_signal都作死:sa_mask是空,更要命的是都有SA_NODEFER 。自作孽,不可活。之所以如此自作孽,就是为了向下兼容,向传统的signal致敬。
    
    3 早期的signal,会中断系统调用。  

    何意?  

    某些系统调用可能会被信号中断,此时系统调用返回错误EINTR,表示被信号中断了。非常多的系统调用都会被中断,我前面有篇博文重启系统调用探究,就详细介绍了系统被信号中断的问题,传统的signal会出现这个问题。那么glibc的signal函数有没有这个问题?答案是没有这个问题,glibc的signal函数很不错。

  1. rt_sigaction(SIGINT, {0x8048736, [INT], SA_RESTART}, {SIG_DFL, [], 0}, 8) = 0

    signal系统调用和sysv_signal都有这个弊端:请看: 

  1. manu@manu-hacks:~/code/c/self/signal$ ./signal_traditional
  2. input a string:
  3. ^COMG , I catch the signal SIGINT
  4. OK,finished process signal SIGINT
  5. fgets failed(Interrupted system call)
  6. manu@manu-hacks:~/code/c/self/signal$ ./signal_sysv
  7. input a string:
  8. ^COMG , I catch the signal SIGINT
  9. OK,finished process signal SIGINT
  10. fgets failed(Interrupted system call)
  11. manu@manu-hacks:~/code/c/self/signal$

    原因就是没有SA_RESTART标志位。内核代码如何体现:

  1. static void
  2. handle_signal(unsigned long sig, siginfo_t *info, struct k_sigaction *ka,
  3.         struct pt_regs *regs)
  4. {
  5.     /* Are we from a system call? */
  6.     if (syscall_get_nr(current, regs) >= 0) {
  7.         /* If so, check system call restarting.. */
  8.         switch (syscall_get_error(current, regs)) {
  9.         case -ERESTART_RESTARTBLOCK:
  10.         case -ERESTARTNOHAND:
  11.             regs->ax = -EINTR;
  12.             break;
  13.         case -ERESTARTSYS:
  14.             if (!(ka->sa.sa_flags & SA_RESTART)) {
  15.                 regs->ax = -EINTR;
  16.                 break;
  17.             }
  18.         /* fallthrough */
  19.         case -ERESTARTNOINTR:
  20.             regs->ax = regs->orig_ax;
  21.             regs->ip -= 2;
  22.             break;
  23.         }
  24.     }
  25.     。。。
  26. }

    很多文章都都将signal函数描述的多么不堪,其实glibc的signal函数非常靠谱,传统的signal的几个弊端都不存在,在日常的工作中,signal完全可以满足需要。

但是存在一个问题,就会可移植性。由于不同的平台可能不同。单就linux平台而言,glibc的signal函数还不错。

    那么signal还有什么问题呢?为啥有引入了实时信号?那是下一篇内容。

参考文献

1 深入理解linunx内核

2  linux内核源代码情景分析
3 signal ppt  蘇維農
4 linux系统编程

时间: 2024-08-02 13:56:46

Linux signal那些事儿【转】的相关文章

Linux signal 那些事儿 (3)【转】

转自:http://blog.chinaunix.net/uid-24774106-id-4065797.html 这篇博客,想集中在signal 与线程的关系上,顺带介绍内核signal相关的结构.如何组织我其实并没想好,想到哪就写到哪里吧.主题一定会落在signal之内而不跑题.     提到signal与thread的关系,就得先提POSIX标准.POSIX标准决定了Linux为何将signal如此实现:    1 信号处理函数必须在多线程应用的所有线程之间共享,但是,每个线程要有自己的挂

Linux signal 那些事儿(2)【转】

转自:http://blog.chinaunix.net/uid-24774106-id-4064447.html 上一篇博文,基本算是给glibc的signal函数翻了个身.现在glibc的signal基本修正了传统的UNIX的一些弊端,我们说signal并没有我们想象的那么不堪.但是signal也有不尽人意的地方.比如信号处理期间,我们期望屏蔽某些信号,而不仅仅是屏蔽自身,这时候signal就不行了.信号既然是进程间通信IPC的一种机制,我们期望获取更多的信息,而不仅仅是signo,这时候s

Linux signal 那些事儿(4)信号的deliver顺序【转】

转自:http://blog.chinaunix.net/uid-24774106-id-4084864.html 上一篇博文提到了,如果同时有多个不同的信号处于挂起状态,kernel如何选择deliver那个信号.        next_signal 负责从挂起信号中选择deliver的signo:当然,有线程显存私有的penging,有线程组共有的pending,对于线程而言,先从自己私有的pending中选,处理完毕私有的才会去处理线程组共有的pending,这个逻辑的代码在: int

linux那点事儿(八)----shell 编程

一个简单的shell程序                                                                  下面直接来看一个shell程序. ----------------- #!/bin/sh # This is to show what a example looks like. echo "Our first example" echo # This inserts an empty line in output echo &qu

linux signal 用法和注意事项

http://blog.chinaunix.net/uid-9354-id-2425031.html 所以希望能用相同方式处理信号的多次出现,最好用sigaction.信号只出现并处理一次,可以用signal.   signal函数每次设置具体的信号处理函数(非SIG_IGN)只能生效一次,每次在进程响应处理信号时,随即将信号处理函数恢复为默认处理方式.所以如果想多次相同方式处理某个信号,通常的做法是,在响应函数开始,再次调用signal设置,如下图: int sig_int(); //My s

linux那点事儿(中)

      今天是辞职后的第一天,本来想写写工作总结,还有许多东西需要整理和学习.这是我继毕业之后的第二次焦虑和迷茫.希望我能早点找到工作吧!        步入正题,其实,linux要学的东西非常多.不是我分个上.中.下三篇博文就能写完的.不过,既然弄了个"上"出来,题目我就不改了.这篇就定为 "中"吧!如果写了"下",还没写完,也许会弄个"下续" ,"下续二"出来.^_^ :) ,也许,写了这个就没下了

初学者必看:Linux压缩那些事儿

Linux的压缩命令的源文件只能有一个,这意味在压缩之前不得不先将要压缩的所有文件打包成一个包,然后再压缩包,这样来完成对多个文件的压缩.所以在了解解压缩之前就必须先了解打包命令.Linux的打包一般都是通过tar命令来完成的,通过man tar,可以得到一些信息,tar来完成不同的动作是通过指定不同的参数来完成的,通常使用的也就是--x/c,这两个参数,它们的字面含义是c--create,x--extract,创建和分包提取,另外还有一些不常用的参数,比如a,将tar添加到另外一个tar包的末

linux那点事儿(六)----进程管理详解(推荐)

目录:(内容较多,加个目录) |-进程管理  进程常用命令 |- w查看当前系统信息 |- ps进程查看命令 |- kill终止进程 |- 一个存放内存中的特殊目录/proc |- 进程的优先级 |- 进程的挂起与恢复 |- 通过top命令查看进程 计划任务 |- 计划任务的重要性 |- 一次性计划at和batch |- 周期性计划crontab 进程管理的概念                                                                    

linux那点事儿(五)----用户管理常用命令

  上一节,将的那内容有些复杂,如果是新手,又不想一下子对用户管理了解的那么深入,欢迎阅读本节内容.      ps:其实,对前面两节内容做了大的调整,在方便自己查阅的同时,也希望方便别人的阅读.呵呵!   用户组权限实例