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. [email protected]-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. }

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

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

测试结果如下:

  1. [email protected]-hacks:~/code/c/self/signal$ gcc -o signal_glibc signal_fault_1.c
  2. [email protected]-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. [email protected]-hacks:~/code/c/self/signal$ ./signal_sysv
  2. input a string:
  3. ^COMG , I catch the signal SIGINT
  4. ^C
  5. [email protected]-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. [email protected]-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. [email protected]-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. [email protected]-hacks:~/code/c/self/signal$

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

  1. [email protected]-hacks:~/code/c/self/signal$ ./signal_traditional
  2. input a string:
  3. ^COMG , I catch the signal SIGINT
  4. ^C
  5. [email protected]-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. [email protected]-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. [email protected]-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. [email protected]-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. [email protected]-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-10-07 03:40:55

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

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 那些事儿 (3)【转】

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

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

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

linux signal 处理

v/:* {behavior:url(#default#VML);} o/:* {behavior:url(#default#VML);} w/:* {behavior:url(#default#VML);} .shape {behavior:url(#default#VML);} Normal 0 7.8 pt 0 2 false false false EN-US ZH-CN X-NONE /* Style Definitions */ table.MsoNormalTable {mso-s

(转载)linux那点事儿(中)

原文地址:http://www.cnblogs.com/fnng/archive/2012/03/19/2407162.html 本文只是转载供自己学习之用 2012-03-22 13:31 by 虫师, 2343 阅读, 5 评论, 收藏, 编辑 今天是辞职后的第一天,本来想写写工作总结,还有许多东西需要整理和学习.这是我继毕业之后的第二次焦虑和迷茫.希望我能早点找到工作吧! 步入正题,其实,linux要学的东西非常多.不是我分个上.中.下三篇博文就能写完的.不过,既然弄了个“上”出来,题目我

(转载)linux那点事儿(上)

原文地址:http://www.cnblogs.com/fnng/archive/2012/03/19/2407162.html 本文只是转载供自己学习之用 本文算是学linux的学习笔记吧!其实linux与window差别还是有挺大的,在学linux时进量清空自己的windows思维,不然容易钻牛角尖.记是学过C之后,去学JAVA,老是用C的思维去看JAVA,所以,就是难入门.因为一个是面向过程的,一个是面向对象的.如果你抛开windows的思想,从零来学linux,其实,它没想象的那么难.每

linux signal

当服务器close一个连接时,若client端接着发数据.根据TCP协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统 会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了.根据信号的默认处理规则SIGPIPE信号的默认执行动作是 terminate(终止.退出), 所以client会退出. 若不想客户端退出可以把 SIGPIPE设为SIG_IGN 如:    signal(SIGPIPE,SIG_IGN); 这时SIGPIPE交给了系统处理. 服

&lt;摘录&gt;linux signal 列表

Linux 信号表 Linux支持POSIX标准信号和实时信号.下面给出Linux Signal的简表,详细细节可以查看man 7 signal. 默认动作的含义如下: 中止进程(Term) 忽略信号(Ign) 中止进程并保存内存信息(Core) 停止进程(Stop) 继续运行进程(Cont) 信号 取值 默认动作 含义(发出信号的原因) SIGHUP 1 Term 终端的挂断或进程死亡 SIGINT 2 Term 来自键盘的中断信号 SIGQUIT 3 Core 来自键盘的离开信号 SIGIL

linux signal 列表

Linux 信号表 Linux支持POSIX标准信号和实时信号.下面给出Linux Signal的简表,详细细节可以查看man 7 signal. 默认动作的含义如下: Term    终止进程 信号 取值 默认动作 含义(发出信号的原因) SIGHUP 1 Term 终端的挂断或进程死亡 SIGINT 2 Term 来自键盘的中断信号 SIGQUIT 3 Core 来自键盘的离开信号 SIGILL 4 Core 非法指令 SIGABRT 6 Core 来自abort的异常信号 SIGFPE 8