Chinaunix首页 | 论坛 | 博客
  • 博客访问: 135205
  • 博文数量: 17
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 40
  • 用 户 组: 普通用户
  • 注册时间: 2018-08-08 00:05
文章分类
文章存档

2020年(4)

2019年(10)

2018年(3)

我的朋友

分类: LINUX

2020-01-21 13:56:26

原文地址:Linux signal那些事儿 作者:Bean_lee

    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内核
 linux内核源代码情景分析
3 signal ppt 
 蘇維農
4 linux系统编程
阅读(11840) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~