转自: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这个标志位。

  • #ifdef __ARCH_WANT_SYS_SIGNAL
  • /*
  •  * For backwards compatibility. Functionality superseded by sigaction.
  •  */
  • SYSCALL_DEFINE2(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;
  •     sigemptyset(&new_sa.sa.sa_mask);

  •     ret = do_sigaction(sig, &new_sa, &old_sa);

  •     return ret ? ret : (unsigned long)old_sa.sa.sa_handler;
  • }
  • #endif /* __ARCH_WANT_SYS_SIGNAL */
  •     这个SA_ONESHOT标志位,等同于SA_RESETHAND标志:在arch/x86/include/uapi/asm/signal.h中有:

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

  •         if (ka->sa.sa_handler == SIG_IGN) /* Do nothing. */
  •             continue;
  •         if (ka->sa.sa_handler != SIG_DFL) {
  •             /* Run the handler. */
  •             *return_ka = *ka;

  •             if (ka->sa.sa_flags & SA_ONESHOT)
  •                 ka->sa.sa_handler = SIG_DFL;

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

  • manu@manu-hacks:~/code/c/self/signal$ cat signal_fault_1.c
  • #include <stdio.h>
  • #include <stdlib.h>
  • #include <signal.h>
  • #include <string.h>
  • #include <errno.h>

  • #define MSG "OMG , I catch the signal SIGINT\n"
  • #define MSG_END "OK,finished process signal SIGINT\n"
  • int do_heavy_work()
  • {
  •     int i ;
  •     int k;
  •     srand(time(NULL));

  •     for(i = 0 ; i < 100000000;i++)
  •     {
  •         k = rand()%1234589;
  •     }

  • }

  • void signal_handler(int signo)
  • {
  •     write(2,MSG,strlen(MSG));
  •     do_heavy_work();
  •     write(2,MSG_END,strlen(MSG_END));
  • }

  • int main()
  • {
  •     char input[1024] = {0};

  • #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

  •     {
  •         fprintf(stderr,"signal failed\n");
  •         return -1;
  •     }

  •     printf("input a string:\n");
  •     if(fgets(input,sizeof(input),stdin)== NULL)
  •     {
  •         fprintf(stderr,"fgets failed(%s)\n",strerror(errno));
  •         return -2;
  •     }
  •     else
  •     {
  •         printf("you entered:%s",input);
  •     }

  •     return 0;

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

  • rt_sigaction(SIGINT, {0x8048736, [INT], SA_RESTART}, {SIG_DFL, [], 0}, 8) = 0
  •     测试结果如下:

  • manu@manu-hacks:~/code/c/self/signal$ gcc -o signal_glibc signal_fault_1.c
  • manu@manu-hacks:~/code/c/self/signal$ ./signal_glibc
  • input a string:
  • input^COMG , I catch the signal SIGINT
  • ^COK,finished process signal SIGINT
  • OMG , I catch the signal SIGINT
  • OK,finished process signal SIGINT
  • ^COMG , I catch the signal SIGINT
  • OK,finished process signal SIGINT
  • ^COMG , I catch the signal SIGINT
  • OK,finished process signal SIGINT
  • ^Z
  • [1]+ Stopped ./signal_glibc
  •     我们安装的信号处理函数并不是一次性的,原因就是glibc的signal函数调用的函数并非是signal系统调用,并没有SA_ONESHOT标志位。
        我们如何体验下老古董的signal,glibc提供了一个sysv_signal接口,manual中这样描述:

  •    However sysv_signal() provides the System V unreliable signal semantics, that is: a) the disposition of the sig‐
  •    nal is reset to the default when the handler is invoked; b) delivery of further instances of the signal is not
  •    blocked while the signal handler is executing; and c) if the handler interrupts (certain) blocking system calls,
  •    then the system call is not automatically restarted.
  •     对于我们的例子只需要:

  • gcc -DSYSTEMV_SIGNAL_API -o signal_sysv signal_fault_1.c
  •     我们看下:

  • manu@manu-hacks:~/code/c/self/signal$ ./signal_sysv
  • input a string:
  • ^COMG , I catch the signal SIGINT
  • ^C
  • manu@manu-hacks:~/code/c/self/signal$ man sysv_signal
  •     第二个ctrl+C导致了进程的推出,原因是sysv_signal这种传统的signal的安装函数是一次性的。strace也证明了这一点:

  • rt_sigaction(SIGINT, {0x8048756, [], SA_INTERRUPT|SA_NODEFER|SA_RESETHAND}, {SIG_DFL, [], 0}, 8) = 0
  •     还记得么:

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

  • gcc -DTRADITIONAL_SIGNAL_API  -o signal_traditional signal_fault_1.c 
  •     我们发现第二个SIGINT信号的信号处理函数已经SIG_DFL,使进程退出了。

  • manu@manu-hacks:~/code/c/self/signal$ ./signal_traditional
  • input a string:
  • ^COMG , I catch the signal SIGINT
  • ^C
  •     我们通过strace可以证明,的确调用了signal系统调用: 

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

  • manu@manu-hacks:~/code/c/self/signal$ ./signal_traditional
  • input a string:
  • ^COMG , I catch the signal SIGINT
  • OK,finished process signal SIGINT
  • fgets failed(Interrupted system call)
  • manu@manu-hacks:~/code/c/self/signal$
  •     如果我在进程处理信号处理函数的时候,再次发送一个SIGINT,这个SIGINT也可能被内核deliver。那么信号处理函数就被中断掉,

  • manu@manu-hacks:~/code/c/self/signal$ ./signal_traditional
  • input a string:
  • ^COMG , I catch the signal SIGINT
  • ^C
  • manu@manu-hacks:~/code/c/self/signal$
  •     我们看到我们收到了I catch the signal SIGINT的打印,但是,并没有收到OK,I finished process signal SIGINT,这表明传统的signal并没有屏蔽正在处理的信号。
        那么我们现在的glibc的signal函数如何?
        strace又来帮忙了?

  • rt_sigaction(SIGINT, {0x8048736, [INT], SA_RESTART}, {SIG_DFL, [], 0}, 8) = 0
  •     glibc的signal函数,调用的是rt_sigaction 系统调用:
        

  • SYSCALL_DEFINE4(rt_sigaction, int, sig,
  •         const struct sigaction __user *, act,
  •         struct sigaction __user *, oact,
  •         size_t, sigsetsize)

  • struct sigaction {
  •     union {
  •      __sighandler_t _sa_handler;
  •      void (*_sa_sigaction)(int, struct siginfo *, void *);
  •     } _u;
  •     sigset_t sa_mask;
  •     unsigned long sa_flags;
  •     void (*sa_restorer)(void);
  • }
  •     我们把strace中的信息,和sigaction数据对比,发现,[INT],就是传说中的sa_mask,当处理SIGINT的时候,看起来是在处理SIGINT信号处理函数的时候,SIGINT会被被屏蔽,防止重入。实际如何呢? 

  • manu@manu-hacks:~/code/c/self/signal$ ./signal_glibc
  • input a string:
  • ^COMG , I catch the signal SIGINT
  • ^C^C^C^COK,finished process signal SIGINT
  • OMG , I catch the signal SIGINT
  • ^C^COK,finished process signal SIGINT
  • OMG , I catch the signal SIGINT
  • OK,finished process signal SIGINT
  • ^COMG , I catch the signal SIGINT
  • OK,finished process signal SIGINT
  • ^COMG , I catch the signal SIGINT
  • ^Z
  • [2]+ Stopped ./signal_glibc
  •     从未出现OMG,I catch the SIGINT连续出现。这是偶然还是必然呢?答案是必然,内核是如何做到的呢?
        在上图的handle_signal函数的末尾,调用了signal_delivered函数: 

  • /**
  •  * signal_delivered -
  •  * @sig:        number of signal being delivered
  •  * @info:        siginfo_t of signal being delivered
  •  * @ka:            sigaction setting that chose the handler
  •  * @regs:        user register state
  •  * @stepping:        nonzero if debugger single-step or block-step in use
  •  *
  •  * This function should be called when a signal has succesfully been
  •  * delivered. It updates the blocked signals accordingly (@ka->sa.sa_mask
  •  * is always blocked, and the signal itself is blocked unless %SA_NODEFER
  •  * is set in @ka->sa.sa_flags. Tracing is notified.
  •  */
  • void signal_delivered(int sig, siginfo_t *info, struct k_sigaction *ka,
  •             struct pt_regs *regs, int stepping)
  • {
  •     sigset_t blocked;

  •     /* A signal was successfully delivered, and the
  •      saved sigmask was stored on the signal frame,
  •      and will be restored by sigreturn. So we can
  •      simply clear the restore sigmask flag. */
  •     clear_restore_sigmask();

  •     sigorsets(&blocked, &current->blocked, &ka->sa.sa_mask);
  •     if (!(ka->sa.sa_flags & SA_NODEFER))
  •         sigaddset(&blocked, sig);
  •     set_current_blocked(&blocked);
  •     tracehook_signal_handler(sig, info, ka, regs, stepping);
  • }
  •     这个函数很有意思,只要用户没有指定SA_NODEFER标志位,当前处理的信号总是加入到屏蔽信号之中。深入理解Linux内核在也提到了这一点。经典教材是这么说的:

  • 当进程执行一个信号处理程序的函数时,通常屏蔽相应的信号,即自动阻塞这个信号,直到处理程序结束。因此,所处理的信号的另一次出现,并不能中断信号处理程序,所以信号处理函数不必是可以重入的。
  •     这个结论很震惊吧。是的你用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

  • 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函数很不错。

  • rt_sigaction(SIGINT, {0x8048736, [INT], SA_RESTART}, {SIG_DFL, [], 0}, 8) = 0
  •     signal系统调用和sysv_signal都有这个弊端:请看: 

  • manu@manu-hacks:~/code/c/self/signal$ ./signal_traditional
  • input a string:
  • ^COMG , I catch the signal SIGINT
  • OK,finished process signal SIGINT
  • fgets failed(Interrupted system call)
  • manu@manu-hacks:~/code/c/self/signal$ ./signal_sysv
  • input a string:
  • ^COMG , I catch the signal SIGINT
  • OK,finished process signal SIGINT
  • fgets failed(Interrupted system call)
  • manu@manu-hacks:~/code/c/self/signal$
  •     原因就是没有SA_RESTART标志位。内核代码如何体现:

  • static void
  • handle_signal(unsigned long sig, siginfo_t *info, struct k_sigaction *ka,
  •         struct pt_regs *regs)
  • {
  •     /* Are we from a system call? */
  •     if (syscall_get_nr(current, regs) >= 0) {
  •         /* If so, check system call restarting.. */
  •         switch (syscall_get_error(current, regs)) {
  •         case -ERESTART_RESTARTBLOCK:
  •         case -ERESTARTNOHAND:
  •             regs->ax = -EINTR;
  •             break;

  •         case -ERESTARTSYS:
  •             if (!(ka->sa.sa_flags & SA_RESTART)) {
  •                 regs->ax = -EINTR;
  •                 break;
  •             }
  •         /* fallthrough */
  •         case -ERESTARTNOINTR:
  •             regs->ax = regs->orig_ax;
  •             regs->ip -= 2;
  •             break;
  •         }
  •     }
  •     。。。
  • }
  •     很多文章都都将signal函数描述的多么不堪,其实glibc的signal函数非常靠谱,传统的signal的几个弊端都不存在,在日常的工作中,signal完全可以满足需要。


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


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

    参考文献


    1 深入理解linunx内核


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

    相关文章:

    • 2021-11-03
    • 2021-10-15
    • 2021-11-25
    • 2021-08-02
    • 2022-12-23
    • 2022-12-23
    猜你喜欢
    • 2021-12-01
    • 2021-05-10
    • 2021-12-18
    • 2021-12-04
    • 2021-08-17
    • 2021-11-08
    相关资源
    相似解决方案