转自: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. }

编译的时候,我没有定义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系统编程

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

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

    转自:http://blog.chinaunix.net/uid-24774106-id-4084864.html 上一篇博文提到了,如果同时有多个不同的信号处于挂起状态,kernel如何选择deli ...

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

    转自:http://blog.chinaunix.net/uid-24774106-id-4065797.html 这篇博客,想集中在signal 与线程的关系上,顺带介绍内核signal相关的结构. ...

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

    转自:http://blog.chinaunix.net/uid-24774106-id-4064447.html 上一篇博文,基本算是给glibc的signal函数翻了个身.现在glibc的sign ...

  4. linux signal 处理

    v/:* {behavior:url(#default#VML);} o/:* {behavior:url(#default#VML);} w/:* {behavior:url(#default#VM ...

  5. linux signal之初学篇

    前言 本博文只总结signal的应用,对signal的kernel实现暂不讨论. 1. linux signal是什么? signal是linux提供的用于进程间通信的一种IPC机制. 2. 如何发送 ...

  6. <摘录>linux signal 列表

    Linux 信号表   Linux支持POSIX标准信号和实时信号.下面给出Linux Signal的简表,详细细节可以查看man 7 signal. 默认动作的含义如下: 中止进程(Term) 忽略 ...

  7. linux signal 列表

    Linux 信号表   Linux支持POSIX标准信号和实时信号.下面给出Linux Signal的简表,详细细节可以查看man 7 signal. 默认动作的含义如下: Term    终止进程 ...

  8. linux signal 用法和注意事项

    http://blog.chinaunix.net/uid-9354-id-2425031.html 所以希望能用相同方式处理信号的多次出现,最好用sigaction.信号只出现并处理一次,可以用si ...

  9. Linux signal 编程(转载)

    转载地址:http://blog.sina.com.cn/s/blog_4b226b92010119l5.html 当服务器close一个连接时,若client端接着发数据.根据TCP协议的规定,会收 ...

随机推荐

  1. 常见的js算法面试题收集,es6实现

    1.js 统计一个字符串出现频率最高的字母/数字 let str = 'asdfghjklaqwertyuiopiaia'; const strChar = str => { let strin ...

  2. python3下最全的wordcloud用法,附源代码及相关文件

    一.wordcloud是什么 词云,在一段文本中提取关键词进行扁平化的展示,更能吸引目标客户的眼球. 市面上有很多在线生成词云的工具,本文以Python中的第三方库wordcloud为例讲解如何自动生 ...

  3. VMWare workstation Pro 14 For Linux key

    VMWare workstation Pro 14 For Linux key: (我使用的Linux 系统是 Ubuntu16.04, 64位 ) 镜像是官方网址下载的,你也可以自己去官方网址下载: ...

  4. 拓扑排序+不是字典序的优先级排列(POJ3687+HDU4857)

    一.前言 在过去的一周里结束了CCSP的比赛,其中有一道题卡了我9个小时,各种调错都没法完整的调处来这题,于是痛下决心开始补题,这个是计划的一部分.事实上,基于错误的理解我写了若干发拓扑排序+字典序的 ...

  5. 笔记-python-tutorial-5.data structure

    笔记-python-tutorial-5.data structure 1.      data structure 1.1.    list operation list.append(x) #尾部 ...

  6. IOS开发学习笔记028-UITableView单组数据显示代码优化

    1.如果表格中又几百条数据的话,系统会自动加载显示在界面上得数据,逐一加载 添加100个数据到UITableView中 ; i < ; i ++) { NSString *icon = [NSS ...

  7. csu-2018年11月月赛Round2-div2题解

    csu-2018年11月月赛Round2-div2题解 A(2193):昆虫繁殖 Description 科学家在热带森林中发现了一种特殊的昆虫,这种昆虫的繁殖能力很强.每对成虫过x个月产y对卵,每对 ...

  8. Map 中的EntrySet() ,Map的遍历

      我们循环Map时一般用到EntrySet(),EntrySet() 返回的时Set集合(Set<Map.Entry<K, V>>). 那么这里的有Map.Entry< ...

  9. "R6002 floating point support not loaded"错误

    R6002 floating point support not loaded 错误,在Debug模式下会弹出如下错误: "floating point support not loaded ...

  10. vivo和OPPO手机刷机

    vivo和OPPO手机是蓝绿两厂,定位年轻时尚女士,比较注重拍照和听音乐,其他无视. 系统很少更新,Root和刷机也比较困难,建议是直接卡刷. 小米手机耍猴(猴子精,代指懂点手机的人) 蓝绿两厂是耍猪 ...