【APUE】Chapter10 Signals

Signal主要分两大部分:html

  A. 什么是Signal,有哪些Signal,都是干什么使的。linux

  B. 列举了很是多不正确(不可靠)的处理Signal的方式,以及怎么样设计来避免这些错误出现。shell

 

10.2 Signal Concepts安全

  1. Signal的实体就是在头文件中定义的正整数(在我使用的linux系统中在/usr/include/bits/signum.h中),以下:数据结构

    

  2. 列举了可能会产生Signal的条件:app

    (1)终端的user的案件操做:如,Ctrl+c,Ctrl+\;由terminal发出。异步

    (2)硬件异常抛出的Signal:如,除0,非法内存引用;由kernel发出。函数

    (3)kill(2) kill(1) :向process或process group发送Signal;由process发出。post

    (4)软件执行时产生的Signal :好比后面要提到的SIGALRM,定点到时信号。ui

  3. Signal是典型的异步驱动时事件,当Signal出现的时候,能够设定程序来告知kernal去作什么事情:

    (1)忽略Signal。这里有两个Signal不能忽略:分别是SIGKILL和SIGSTOP。不能忽略的缘由是必须让kernal或root权限能够强制终止进程。若是遇到了hardware exception状况,不去处理这类异常的话,进程后续如何执行就不肯定了。

    (2)捕获Signal。这里的作法是预先告诉kernal“若是出现了这个信号,用哪一个函数去处理”。另,SIGKILL和SIGSTOP是不能被捕获的。

    (3)默认处理策略。系统预先指定了几个Signal默认的处理策略。在signum.h中:

        

      具体能够对照Figure10.1中每一个Signal的默认处理策略去查看;有些默认策略是terminal+core类型的,可用于系统down了以后的debug。可是,也不必定保证core必定会产生;若是权限不足的话,极可能没法产生core。这个也要留意。

  4. 这里插播一个Signal的解释。SIGTSTP,当执行终端命令交互的程序到时候,按下ctrl+z能够触发这个信号,向foreground process group全部的。这个信号名字里虽然叫stop,可是并非真的给停了。这里与stop相对的是continue(即SIGCONT),只是给暂停的意思。这个信号若是只是望文生义,就容易理解误差。不只仅是这个信号,后面还有更没法望文生义的名词解释。

 

 

10.3 signal function

  signal这个函数的含义就是告诉系统用什么函数处理什么信号

  1. 看一个例子:

 1 #include "apue.h"
 2 #include <stdio.h>
 3 #include <signal.h>
 4 
 5 static void sig_usr(int signo)
 6 {
 7     if (signo==SIGUSR1)
 8     {
 9         printf("received SIGUSR1\n");
10     }
11     else if (signo==SIGUSR2)
12     {
13         printf("received SIGUSR2\n");
14     }
15     else
16     {
17     }
18 }
19 
20 int main(void)
21 {
22     if (signal(SIGUSR1, sig_usr) == SIG_ERR)
23     {
24         err_sys("can't catch SIGUSR1");
25     }
26     if (signal(SIGUSR2, sig_usr) == SIG_ERR)
27     {
28         err_sys("can't catch SIGUSR2");
29     }
30     for (;;)
31     {
32         pause();
33     }
34 }

  执行结果以下:

    

  初步分析以下:

  (1)main中给SIGUSR1和SIGUSR2都注册了signal handler

  (2)pause()函数的做用是阻塞进程,并一直等着signal到来。

  (3)kill跟它叫kill没有关系,纯粹是unix系统的一个misnomer,它的做用就是向process或process group发送信号。所以就是发送SIGUSR1和SIGUSR2信号。

  (4)kill默认状况下发送的信号是SIGTERM,即终止进程。

  若是连续两次发送SIGUSR1信号会怎样?以下:

  

  经过上面的运行结果可知,用signal给某个信号注册一次handler,只能管一次。即,在信号发生跳转到自定的 handler 处理函数执行后, 系统会自动将此处理函数换回原来系统预设的处理方式。那么有没有注册一次,可以处理屡次信号的方法呢?有,改用sigaction函数去注册signal handler。代码以下:

 1 #include "apue.h"
 2 #include <stdio.h>
 3 #include <signal.h>
 4 
 5 static void sig_usr(int signo)
 6 {
 7     if (signo==SIGUSR1)
 8     {
 9         printf("received SIGUSR1\n");
10     }
11     else if (signo==SIGUSR2)
12     {
13         printf("received SIGUSR2\n");
14     }
15     else
16     {
17     }
18 }
19 
20 int main(void)
21 {
22     struct sigaction sa1, sa2;
23     sa1.sa_handler = sig_usr;
24     sa2.sa_handler = sig_usr;
25     sigemptyset(&sa1.sa_mask);
26     sigemptyset(&sa2.sa_mask);
27     sigaddset(&sa1.sa_mask, SIGUSR1);
28     sigaddset(&sa2.sa_mask, SIGUSR2);
29     sigaction(SIGUSR1, &sa1, NULL);
30     sigaction(SIGUSR2, &sa2, NULL);
31     /* 
32     if (signal(SIGUSR1, sig_usr) == SIG_ERR)
33     {
34         err_sys("can't catch SIGUSR1");
35     }
36     if (signal(SIGUSR2, sig_usr) == SIG_ERR)
37     {
38         err_sys("can't catch SIGUSR2");
39     }
40     */
41     for (;;) pause();
42 }

    运行结果以下:

    

  Program Start-Up

  这里面提到了一种状况,若是是由某个程序经过执行exec产生的新的Program,进而替代原来的Process(虽然进程号不变,可是即便是执行原来的程序,程序的地址也变化了);那么,由原来的Porcess注册的各类Signal处理相关的内容,在新的Program中是无效的,由于原来的signal-handler中注册的信号处理函数的地址失效了,不是原来的函数了。

 

从下面的部分开始,更多的从反面出发,分析signal处理上经历的各类不靠谱设计,从而理解为何要设计各类靠谱的机制。

10.4 Unreliable Signals

  1. 不靠谱伪代码(1)

int sig_int(); /*signal处理函数*/

...

signal(SIGINT, sig_int); /*注册signal handler*/

...

sig_int()
{
    signal(SIGINT, sig_int); /*从新注册signal handler以便处理下一次信号*/    
    ...
}

  了解过signal函数以后,能够知道,上述代码的意思大概是:每次出现SIGINT信号,都会触发sig_int()函数来处理信号;因为signal函数只能管处理一次SIGINT,所以为了可以连续处理SIGINT信号,在每次进入sig_int()函数时,都从新用signal函数注册一下sig_int。

  上面的代码看似是没有问题的,可是却隐含着比较大的漏洞。

  考虑以下的状况:若是以前已经来了一个SIGINT信号,系统开始调用注册的sig_int()进行信号处理;若是偏偏在进入sig_int()函数以前,又来了一个SIGINT信号;因为此时系统已经没有用于处理SIGINT信号的函数了,则第二个到来的SIGINT信号就被采用默认的信号处理策略(对于SIGINT这个信号来讲,就是直接将进程terminates了),每每达不到咱们预期的信号处理效果。这种漏洞的可怕之处还在于,大部分时间程序都是工做正确的,偶尔会出现问题,这种bug是最难排除的。

  2. 不靠谱伪代码(2)

int sig_int(); /*SIGINT信号处理函数*/
int sig_int_flag; /*SIGINT信号出现时候将其赋值为零*/

main()
{
    signal(SIGINT, sig_int); /*注册信号处理函数*/
    ...
    while(sig_int_flay == 0)
    {
        pause();  /*等着信号到来*/
    }  
}   

sig_int()
{
    signal(SIGINT, sig_int); /*从新注册信号处理函数*/
    sig_int_flag = 1; /*修改环境变量*/
}

  上述的伪代码的本意是:main函数中的while循环就要等着SIGINT信号的到来,而且处理完sig_int_flag标志标量,才往下进行。

  代码的本意是好的,可是仍是存在漏洞。

  考虑以下的状况:若是已经注册完了sig_int函数,首次进入while循环的判断条件,变量为0;而偏偏在执行while循环体中的pause()以前,来了一个SIGINT信号;开始执行sig_int的过程当中已经把sig_int_flag设置为1了;sig_int执行完毕,开始执行pause()函数;若是之后不再来SIGINT信号了,那么pause()就一直等下去了。仍是跟上一个不靠谱的代码问题同样,这样的代码大部分时间能够正常运行,偶尔出现问题,很是难debug。

  总结一下上面两个不靠谱的代码,其核心问题我认为是:signal是异步出现的,随时都能打断当前执行的程序;而上述代码的设计思路都是传统的同步顺序执行的,因此会遇到各类细节问题

  (1)第一种状况是signal“该来的时候来了,不应来的时候也来了”,即信号处理函数失效。

  (2)第二种状况是signal“该来的时候不来,不应来的时候也来了”,即信号丢失。

 

10.5 Interrupted System Calls

  这个部分阐述与System Call相关的signal处理问题。我没太理解深刻,暂时记录如下两点:

  (1)早期的unix系统中,若是某个process正在被“a slow system call”给阻塞;那么这个时候来个信号,原来正在执行的“a slow system call”就被打断了,返回一个error而且errno被设置为EINTR。

  (2)为了不这样的问题,有的系统提供了处理上述问题的system call restart的机制。可能的作法就是每次执行slow system call的时候,都去检查error的返回值,是不是EINTR,来决定是否启动restart。

  有个伪代码以下:

again:
    if ( (n = read(fd, buf, BUFFSIZE)) < 0 )
    {
        if ( error == EINTR )
            goto again; /*被interrupted的system call*/
        
         ...  /*处理其余问题*/
    
    }    

  在后续的14.4节中,select()和poll()函数的时候还会细说interrupted system calls

  

10.6 Reentrant Functions (可重入函数)

  这部分说的是signal处理中安全问题,是否可重入。(能够查阅这个bloghttp://particle128.com/posts/2014/05/reentrant.html

  所谓的安全问题,是指signal到来与处理,打断了原来执行的程序;在执行完信号处理函数后,原来正在执行的程序可能就受到影响了,与预想的结果不太同样,这就是我理解的signal安全。  

  书上举了两个例子,来讲明因为重入某些函数可能带来的signal不安全状况:

  (1)若是原进程正执行malloc分配内存呢,分配到一半的时候来个signal信号;而后进入到信号处理函数中,又用到malloc函数动态分配内存了。

  (2)若是原进程正执行getpwnam函数呢(简单理解getpwnam往一个static的存数据),正执行到一半忽然来个signal信号;而后进入到signal信号处理函数中,又调用getpwnam;结果就是上次存一半的结果被新的覆盖了;再次回到原来的进程顺序执行的时候,getpwnam得到结果就乱套了。

  上述的例子只是一个热身,系统些来讲,判断一个signal handler函数是否是reentrant的,通常从signal handler function以下的几个方面进行考虑:

  (1)是否用到了static data:若是信号发生时正调用getwpnam,而且在signal handler中也调用了getwpnam,则在handler中新获取的值就会覆盖以前进程中获取的值。

  (2)是否调用了free或者malloc函数:若是信号发生时正调用malloc(修改堆上的存储空间链接表),而且信号处理程序又调用malloc,会破坏内核的数据结构。

  (3)是否调用了standard IO:由于好多standard I/O函数都使用了全局的数据结构(如,printf中用到的文件偏移量是全局的

  (4)是否用了longjmp这类的函数:信号发生时候程序正在修改一个数据结构,longjmp这种彻底推倒重来的函数在信号处理中一旦出现,就容易出现改一半就没改完的状况

  类unix系统中,若是对于signal是安全的,有两种要求:

  (1)首先函数必须是reentrant fucntion标准的(上述的4点),即从自身设计上不要出现signal不安全的漏洞

  (2)这些函数执行时已经block各种signal,即从外部影响上主动避开signal出现带来的问题

  另外,书上还提醒了一点:即便是调用Figure10.4中的reentrant function,还须要检验errno这个变量值;缘由是,可能在信号处理函数中改变了error的实际值。好比,若是信号处理函数中调用了read(参考interrupted system call的内容),就有可能在信号处理完成后改变error的值。

  所以,即便在signal handler中调用的是Figure10.4中的reentrant function,也须要在信号处理函数中检验errno的值。

  例如,在Figure10.4中有fork函数,它的做用是分叉产生一个child process;当child process执行完了以后,就会产生一个SIGCHLD信号,返回给主进程;而这个信号的signal handler通常都有一个wait function,而wait function改变errno值。

  总结一下,之后再设计signal处理函数的时候,必定要注意是不是reentrant的,以及信号处理安全问题。

 

10.7 SIGCLD Semantics

   SIGCLD和SIGCHLD都是与child process结束状态有关的信号。

   这重点介绍的是SIGCLD这个信号,有一些类unix系统对于这个SIGCLD信号的处理是比较特殊的。

   (1)有时候,咱们不想关心一些child process的运行情况,就会把SIGCLD信号的处理方式设置为SIG_IGN。这样作的好处就是,不会产生僵尸进程,“the status of these child processes is discarded”;可是,若是父进程中不当心有wait()函数正等着这个child process,那就会一直阻塞了。

   (2)若是注册signal handler来处理SIGCLD信号,在调用signal函数的时候,kernel会检测是否已经有child process正在等待被回收处理。这点特性,也带来了以下的问题。

   有问题的示例代码以下:#include "apue.h#include <sys/wait.hstatic void sig_cld(int);

int main()
{
    pid_t pid;
    signal(SIGCLD, sig_cld);
    if ( (pid=fork()) ==0 ) /*child process*/
    {
        sleep(2);
        _exit(0);
    }
    pause();
    exit(0);
}

static void sig_cld(int signo)
{
int status; signal(SIGCLD, sig_cld);
/*reestablish handler*/
...
   pid = wait(&status); }

  main()中的sleep(2)是为了保证parent process先执行pause(),而后child process再执行_exit(0)。sig_cld()函数中在开始就调用signal(SIGCLD, sig_cld)是为了可以连续处理SIGCLD信号。

  不考虑其余代码漏洞,上述代码在正常逻辑顺序上有较大的问题:每次调用signal(SIGCLD, XXX)的时候,kernel就会去check是否有child process等待被回收;因为此时child process尚未被回收呢,所以kernel认为还有child process等着被处理,所以再调用sig_cld函数;整个代码陷入了循环。

  总结一下,凡是涉及到SIGCLD以及SIGCHLD的信号处理问题,须要参考具体所在系统对于SIGCLD以及SIGCHLD的实现分析。

 

10.8 10.9 10.10 kill和alarm函数

  三个部分放在一块儿,用一个例子综合在一块儿。

  1. kill函数:kill(pid_t pid, int signo),向某个进程发送信号。

    这里注意两点:

    (1)调用kill函数的进程是否有权限给另外一个进程发信号

    (2)kill(pid, 0) 这种形式能够用来验证pid进程号是否存在;可是考虑到类Unix系统,即便是一个进程的资源已经释放了,进程号pid仍然会被占用一段时间,所以这种经过检测pid号的方式来检测进程是否存在也不必定是彻底准确的,得看具体的系统实现。

  2. alarm函数:alarm(unsigned int seconds),启动一个倒计时器,到达seconds的时间后,会触发SIGALRM的信号;若是没有设定信号处理函数,则默认的行为就是将进程结束。

    这里注意两点:

    (1)一个进程只能同时有一个有效的alarm函数,若是在一个进程中屡次使用alarm函数:后一次的alarm时间会替代上一次的alarm的时间。具体示例以下:    

#include <unistd.h>
#include <stdio.h> 
#include <stdlib.h> 
#include <signal.h>
#include <sys/time.h> 

    
struct timeval start, end;

void sig_int()
{
    unsigned int seconds = 0;
    seconds = alarm(5);
    printf("left seconds: %u\n", seconds);
    gettimeofday(&start, NULL);
    pause();
}

void sig_alm()
{
    float time_interval;
    gettimeofday(&end, NULL);
    time_interval = 1000000*(end.tv_sec-start.tv_sec)+end.tv_usec-start.tv_usec;
    time_interval = time_interval/1000000;
    printf("Pasue time: %f\n", time_interval);
}

int main()
{
    signal(SIGINT, sig_int);
    signal(SIGALRM, sig_alm);
    alarm(100);
    pause();
}

  程序执行结果如图:

  

  分析以下:main中执行倒计时100秒;大概过了10秒以后在终端ctrl+c触发interrupt信号,进入SIGINT信号处理函数;在SIGINT信号处理函数中,修改alarm的倒计时为5秒。最后从执行结果看到,第二次执行的alarm(5)替代了前一次的alarm(100)。

  有时候为了不以前设定的alarm无效了,也能够检查相似上述代码中的seconds的值,来保证等够100秒。

  (2)在某些状况下,若是产生了资源竞争,调用pause()以前,alarm就执行完了,pasue()就会一直在等着了。为了不这种状况,书上给出了以下的sleep函数的设计。我在书上代码的基础上稍加修改,为的就是在代码中更好的体现资源竞争。

#include "apue.h"
#include <signal.h>
#include <unistd.h>
#include <setjmp.h> 

static jmp_buf env_alrm;

static void sig_alm(int signo)
{
    longjmp(env_alrm, 1);
}


unsigned int sleep2(unsigned int second)
{
    if (signal(SIGALRM, sig_alm) == SIG_ERR){ 
        return second; 
    }
    if ( setjmp(env_alrm)==0 )
    {
        alarm(second);
        kill(getpid(),SIGINT);
        pause();
    }
    return alarm(0);
} 

static void  sig_int(int signo)
{
    int i,j;
    volatile int k;
    printf("sig_int starting\n");
    for ( i=0; i<300000; i++)
    {
        for (j=0; j<40000; j++) k += i*j;
    }
    printf("sig_int finished\n");
}

int main()
{
    unsigned int unslept;
    if (signal(SIGINT, sig_int)==SIG_ERR) { 
        err_sys("signal(SIGINT) error"); 
    }  
    unslept = sleep2(5);
    printf("sleep2 returned : %u\n", unslept);
    exit(0);
}

  程序的执行结果以下:

  

  分析以下:

  a. 上述代码体现了setjmp longjmp可以在资源竞争条件下对alarm和pause对进行保护

  上述代码在main()中调用sleep2()函数。

  进入到sleep2()函数以后,先注册一个SIGALRM的信号处理函数 → 再在setjmp保护下,alarm(second)设定倒计时 → 随后立刻执行kill命令,向进程自身发送一个SIGINT信号,触发sig_int函数 → sig_int函数中执行的任务远超过alarm(second)中second的限制,为的就是模拟资源竞争时alarm已经倒计时完毕的时候,pause()还没开始。

  因为sleep2()中保护机制的存在,若是因为资源竞争致使“alarm已经执行完毕,可是pause还没开始”,longjmp就直接跳到setjmp的地方了,而且返回的值为1;这样就跳过了pause()的语句,天然也就不会无限期的等待了。

  b. 上述代码也体现了setjmp longjmp这种机制的隐患

  加入不是人为产生资源竞争,而是进程真的在执行某些任务;这个时候因为alarm到时,强制longjmp结束,颇有可能形成其余任务还没处理完成,就直接longjmp结束了。

  总结一下,要慎用setjmp和longjmp这样的长跳起色制。 

 

10.11  10.12  10.13  sigset_t & sigprocmask & sigpending

  1. sigset_t

  有时候咱们须要设定,一个进程要屏蔽哪些信号、要接受哪些信号、记录进程原来对信号的设定情况等。这个时候,就须要一种信号集合的数据结构,以及围绕其的一些周边函数来完成。其中数据结构就是sigset_t

  2. int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset)

  设定信号mask。

  how : 修改信号mask的方式,SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK

  ret : 信号mask被设定成设么样

  oret : 保留原来的信号mask

  返回值表示函数执行成功或失败。

  3. int sigpending(sigset_t *set)

  得到当前进程有哪些信号被pending了。

  看一个例子:

#include "apue.h"


static void sig_quit(int signo)
{
    printf("caught SIGQUIT\n");
    signal(SIGQUIT, SIG_DFL);
}

int main()
{
    sigset_t newmask, oldmask, pendmask;

    signal(SIGQUIT, sig_quit); /*捕捉退出信号*/
    sigemptyset(&newmask); /*初始化新的信号mask*/
    sigaddset(&newmask, SIGQUIT); /*在newmask中添加退出信号*/
    sigprocmask(SIG_BLOCK, &newmask, &oldmask); /*阻塞SIGQUIT信号*/

    sleep(5);

    sigpending(&pendmask); /*得到有当前进程被阻塞的信号*/
    if (sigismember(&pendmask, SIGQUIT)) { 
        printf("\nSIGQUIT pending\n");
    }
    sigprocmask(SIG_SETMASK, &oldmask, NULL); /*释放被阻塞的信号*/
    printf("SIGQUIT unblocked\n");
    sleep(5);
    exit(0);
}

  代码执行结果以下:

  

  分析以下:

  (1)上述代码首先用sigprocmask阻塞了SIGQUIT信号;通过第一个sleep(5)以后,立刻执行sigpending,得到了当前被阻塞的信号,而且获得了验证SIGQUIT确实在被阻塞的信号集合pendmask中。

  (2)紧接着再用sigprocmask函数释从新恢复了信号mask的值,即释放被阻塞的信号;随后,以前被阻塞的SIGQUIT信号立刻就进来了,而且触发了sig_quit信号处理函数;注意,在信号处理函数中恢复了对SIGQUIT的默认处理方式。

  (3)即便第一个sleep(5)的时候输入了多个ctrl+\输入信号,最终被处理的SIGQUIT信号也只有一个(最起码在我使用的Linux系统上是这样的)。

  在上述代码基础上再扩展一下,若是多个不一样的信号被pending住,当unblock的以后,会有什么效果呢?将书上的代码修改以下:

#include "apue.h"


static void sig_quit(int signo)
{
    printf("caught SIGQUIT\n");
    signal(SIGQUIT, SIG_DFL);
}

static void sig_int(int signo)
{
    printf("caught SIGINT\n");
    signal(SIGINT, SIG_DFL);
}

int main()
{
    sigset_t newmask, oldmask, pendmask;

    signal(SIGQUIT, sig_quit); /*捕捉退出信号*/
    signal(SIGINT, sig_int); /*捕捉中断信号*/
    sigemptyset(&newmask); /*初始化新的信号mask*/
    sigaddset(&newmask, SIGQUIT); /*在newmask中添加退出信号*/
    sigaddset(&newmask, SIGINT); /*阻塞interrupt信号*/
    sigprocmask(SIG_BLOCK, &newmask, &oldmask); /*阻塞SIGQUIT信号*/

    sleep(5);

    sigpending(&pendmask); /*得到有当前进程被阻塞的信号*/
    if (sigismember(&pendmask, SIGQUIT)) { 
        printf("\nSIGQUIT pending\n");
    }

    if (sigismember(&pendmask, SIGINT)) { 
        printf("\nSIGINT pending\n");
    }
    sigprocmask(SIG_SETMASK, &oldmask, NULL); /*释放被阻塞的信号*/
    printf("SIGQUIT unblocked\n");
    printf("SIGINT unblocked\n");
    sleep(5);
    exit(0);
}

  不只阻塞了SIGQUIT信号,并且还阻塞SIGINT信号。再执行代码以下:

  

  分析以下:

  (1)能够看到,对于同类的信号被阻塞信号,只获取一个;对于不一样类的阻塞信号,能够把不一样类等待的信号分别处理了。

  (2)对比两次运行程序,虽然输入信号的顺序是不一样的,可是执行信号处理函数的顺序却没有改变。

  针对以上两点内容,google了一下相关资料:这个blog的解释很是好http://galex.cn/【apue】信号/

  

  上述的信号是经典信号,不支持排队,并且信号的响应顺序和信号到来的顺序根本没有关系。恩,暂时这样理解。

 

10.14 10.15  sigaction Function & sigsetjmp siglongjmp Function

  1. int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact) 

  本质上用于信号处理函数的注册,signal(2)的增强改进版。执行成功返回0,出错返回-1。

  signo : 要处理的信号

  act : 对信号处理函数的新设定

  oact : 以前注册的signo信号处理函数的信息

  其中sigaction是结构体变量,里面不只存放了信号处理函数,并且还有须要屏蔽掉的信号集合,以及其余信息。

  struct sigaction{

    void (*sa_handler) (int); /*信号处理函数*/

    sigset_t sa_mask; /*须要屏蔽的信号*/

    ...

  }

  这里的屏蔽信号指的是执行信号处理函数前但愿屏蔽掉的信号;当信号处理函数执行完并return的时候,信号mask就会恢复到以前的状态。

  回想以前在signal handler中从新调用signal的例子,应用sigaction函数就能够作到“Hence, we are guaranteed that whenever we are processing a given signal,  another occurrence of that same signal is blocked until we're finished processing the first occurrence

  另外,用sigaction函数注册的信号处理函数,只要注册一次就一直生效;除非显式修改该信号的处理函数。

  

  2. sigsetjmp & siglongjmp

  int sigsetjmp(sigjmp_buf env, int savemask)

    返回0表明是设置jmp点;返回非零值表明从别处跳回来,具体是什么值在跳的时候决定。

    env : 目前还不详

    savemask : 执行跳转以前的信号mask值

  void siglongjmp(sigjmp_buf env, int val)

    跳到sigsetjmp的地方,其中val要求是非零值,即sigsetjmp的返回值。

  以前已经有setjmp和longjmp这种终极长跳转函数对了,为何还要有sigsetjmp和siglongjmp函数对呢?

  考虑下面这种状况:

  a. 假设进入到了信号处理函数中(此时,假设同类信号已经被blocked了),该类信号已经在mask中设置为blocked了;

  b. 若是signal handler被顺利执行完,这时信号mask就恢复到执行signal handler以前的状态了

  c. 可是,若是signal handler执行到中间,执行到了longjmp语句了,极可能就把信号mask恢复这个环节就个跳过去了

  d. 信号mask没有恢复的问题就是,原本同类信号能够再继续被处理,可是因为没有恢复信号mask,就不能被处理了

  上面的状况,也是sigsetjmp和siglongjmp的设计初衷。

  

下面看一个例子(基于书上Figure 10.20的示例改造的),实际体会一下sigsetjmp和siglongjmp的函数对的做用:

#include "apue.h"
#include <setjmp.h>
#include <time.h>
#include <errno.h> 

static sigjmp_buf jmpbuf;
static volatile sig_atomic_t canjmp;

/*得到当前进程是否mask了某种信号*/
void pr_mask(const char *str)
{
    sigset_t sigset;
    int errno_save;
    errno_save = errno;

    sigemptyset(&sigset);
    if ( sigprocmask(0,NULL,&sigset)<0 ) //得到当前的signal set状况
    {
        err_ret("sigprocmask error");
        errno = errno_save;
        return;
    }
    printf("%s",str); //打印调用信息title
    if ( sigismember(&sigset, SIGINT) ) printf(" SIGINT");
    if ( sigismember(&sigset, SIGQUIT) ) printf(" SIGQUIT");
    if ( sigismember(&sigset, SIGUSR1) ) printf(" SIGUSR1");
    if ( sigismember(&sigset, SIGALRM) ) printf(" SIGALRM"); 
    printf("\n");
    errno = errno_save;
}

/*SIGUSR1的信号处理函数*/
void sig_usr1(int signo)
{
    time_t starttime;
    if (canjmp==0) return; 
    
    pr_mask("starting sig_usr1: ");
    alarm(3);
    starttime = time(NULL);
    for (; ;) /*busy等待5秒*/
        if (time(NULL)>starttime+5)
            break;
    pr_mask("finishing sig_usr1: ");
    canjmp = 0;
    siglongjmp(jmpbuf,1);
    /*longjmp(jmpbuf,1);*/
}
/*SIGALRM信号处理函数*/
void sig_alarm(int signo)
{
    pr_mask("in sig_alrm: ");
}

int main()
{
    struct sigaction siga1,siga2;

    siga1.sa_handler = sig_usr1;
    sigemptyset(&siga1.sa_mask);
    sigaddset(&siga1.sa_mask, SIGUSR1);

    siga2.sa_handler = sig_alarm;
    sigemptyset(&siga2.sa_mask);
    sigaddset(&siga2.sa_mask, SIGALRM);

    sigaction(SIGUSR1, &siga1, NULL);
    sigaction(SIGALRM, &siga2, NULL);
    
    pr_mask("staring main: ");

    if (/*setjmp(jmpbuf)*/sigsetjmp(jmpbuf,1)) {
        pr_mask("ending main: ");
        exit(0); 
    } 
    canjmp = 1;
    for(; ;)
        pause();
}

  程序运行结果以下:

  (1)用sigsetjmp和siglongjmp函数对的运行结果:

    

  (2)用setjmp和longjmp函数对的运行结果:

    

  对比以上两个运行结果能够看到,sigsetjmp和siglongjmp函数对确实很好地保护了信号mask现场,不会由于jmp的动做就致使某些信号mask位没有被恢复过来。(另,在我运行的系统上,若是直接用signal还不太行,必须用sigaction函数才能够得到上述的结果;缘由就是系统的signal没有实现同类信号屏蔽

 


10.18 system Function

  这个部分主要说system这个函数与signal相关的内容:POSIX.1标准要求system必须忽略SIGINT和SIGQUIT信号,屏蔽SIGCHLD信号。

  下面用正反两个例子来讲明为何有上述的要求。

  例子一

#include <unistd.h> 
#include <signal.h>
#include <stdio.h> 
#include <errno.h> 

int system(const char *cmdstring)    /* version without signal handling */
{
    pid_t    pid;
    int        status;
    if (cmdstring == NULL)
        return(1);        /* always a command processor with UNIX */

    if ((pid = fork()) < 0) {
        status = -1;    /* probably out of processes */
    } else if (pid == 0) {                /* child */
        execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
        _exit(127);        /* execl error */
    } else {                            /* parent */
        while (waitpid(pid, &status, 0) < 0) {
            if (errno != EINTR) {
                status = -1; /* error other than EINTR from waitpid() */
                break;
            }
        }
    }
    return(status);
}
static void sig_int(int signo)
{
    printf("caught SIGINT\n");
}

static void sig_chld(int signo)
{
    printf("caught SIGCHILD\n");
}

static void sig_usr1(int signo)
{
    printf("caught SIGUSR1\n");
}
int main()
{
    signal(SIGINT, sig_int);
    signal(SIGCHLD, sig_chld);
    signal(SIGUSR1, sig_usr1);
    system("/bin/ed");
}

  程序运行结果以下:

  

  分析以下:

  -------------插播一段ed的背景知识-------------

  首先须要补充一下/bin/ed这个程序有关信号的特色:

    (1)ed捕获SIGINT和SIGQUIT信号

    (2)对于SIGINT信号:prints a question mask

    (3)对于SIGQUIT信号的处理方式:ignore

  为了验证ed的特性,我作了以下的试验:

  

  两个窗口是tmux开的,左侧是调用/bin/ed的窗口;右侧的是发送signal用的(另,不知道tmux的,能够参考个人这篇blog

  能够看到开启ed程序后,向其发送SIGINT信号,真的再终端反馈输出一个?;向其发送SIGQUIT信号,并么有什么反应。

  ------------------------插播结束-------------------------

  如今开始切入正题。

  先分析运行结果

  (参考http://blog.csdn.net/windeal3203/article/details/39049291http://blog.csdn.net/ctthuangcheng/article/details/9258715):

  (1)终端输入ctrl+c,前台进程都会受到这个信号;这里的前台进程包括ed shell a.out三个进程(其中shell自动忽略SIGINT信号,不讨论

  (2)ed在收到SIGINT后,天然会输出一个?;而a.out中注册了SIGINT的处理函数,所以也会触发信号处理函数

  (3)输入q以后,ed退出,发送一个SIGCHLD信号;因为a.out是ed的父进程(看红框中的pid和ppid),因此天然收到SIGCHLD信号,并触发信号处理函数

  请注意,上述的system函数是去除了signal设定的阉割版,并无符合POSIX.1的标准

  咱们须要一直记着:这个/bin/ed是由system函数调用的,这样带来的问题有两个:

  (1)因为终端正在运行的是/bin/ed程序,输入的ctrl+c的想法是给ed的,并非给a.out的,a.out错误接收到了发给ed的SIGINT了;这个时候若是a.out还有其余的任务要执行,而且没有处理SIGINT的机制,就被强制关闭了。

  (2)当ed执行完毕退出的时候,至关于child process结束,所以会给parent process的a.out发送一个SIGCHLD信号;其实这个ed一旦由system函数执行上,就跟a.out没有太大关系了,a.out错误接收了发给ed的SIGCHLD了,认为是本身的进程执行完毕了。

  我我的理解,system function虽说某种程度上方便了启动某些程序;但经过上述的分析,system Function也是一个挺别扭的事情。要想知作别扭的缘由,须要参考一下system的实现(不考虑signal版):

int system(const char *cmdstring)    /* version without signal handling */
{
    pid_t    pid;
    int        status;
    if (cmdstring == NULL)
        return(1);        /* always a command processor with UNIX */

    if ((pid = fork()) < 0) {
        status = -1;    /* probably out of processes */
    } else if (pid == 0) {                /* child */
        execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
        _exit(127);        /* execl error */
    } else {                            /* parent */
        while (waitpid(pid, &status, 0) < 0) {
            if (errno != EINTR) {
                status = -1; /* error other than EINTR from waitpid() */
                break;
            }
        }
    }
    return(status);
}

  (1)显然system的实现利用fork和exec两个重要的函数:fork就是分叉生成一个child process;exec的做用是让child process用shell运行程序;能够说system利用了fork和exec实现了极大的代码便利

  (2)可是fork和exec必然会对parent process有影响,好比上面提到的信号问题。

  上述两个方面(1)简化了工做,(2)复杂了工做,因此说system Function是个别扭的函数。

  

  例子二

  给出另外一个system实现(在原书的代码上作了一些修改,主要是方便显示),代码以下:

#include <sys/wait.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h> 

static void sig_int(int signo)
{
    printf("caught SIGINT\n");
}

static void sig_chld(int signo)
{
    printf("caught SIGCHILD\n");
}

int i_system(const char *cmdstring)    /* with appropriate signal handling */
{
    pid_t                pid;
    int                    status;
    struct sigaction    ignore, saveintr, savequit;
    sigset_t            chldmask, savemask;

    if (cmdstring == NULL)
        return(1);        /* always a command processor with UNIX */

    ignore.sa_handler = SIG_IGN;    /* ignore SIGINT and SIGQUIT */
    sigemptyset(&ignore.sa_mask);
    ignore.sa_flags = 0;
    if (sigaction(SIGINT, &ignore, &saveintr) < 0)
        return(-1);
    if (sigaction(SIGQUIT, &ignore, &savequit) < 0)
        return(-1);
    
    sigemptyset(&chldmask);            // now block SIGCHLD
    sigaddset(&chldmask, SIGCHLD);
    if (sigprocmask(SIG_BLOCK, &chldmask, &savemask) < 0)
        return(-1);

    if ((pid = fork()) < 0) {
        status = -1;    /* probably out of processes */
    } else if (pid == 0) {            /* child */
        /* restore previous signal actions & reset signal mask */
        sigaction(SIGINT, &saveintr, NULL);
        sigaction(SIGQUIT, &savequit, NULL);
        sigprocmask(SIG_SETMASK, &savemask, NULL);

        execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
        _exit(127);        /* exec error */
    } else {
        /* parent */
        while (waitpid(pid, &status, 0) < 0)
            if (errno != EINTR) {
                printf("error\n");
                status = -1; /* error other than EINTR from waitpid() */
                break;
            }       
    }
    printf("ed ends\n");
    /* restore previous signal actions & reset signal mask */
    if (sigaction(SIGINT, &saveintr, NULL) < 0)
        return(-1);
    if (sigaction(SIGQUIT, &savequit, NULL) < 0)
        return(-1);
    
    if (sigprocmask(SIG_SETMASK, &savemask, NULL) < 0)
        return(-1);
    
    printf("return from system\n");
    return(status);
}
int main()
{
    signal(SIGINT, sig_int);
    signal(SIGCHLD, sig_chld);
    i_system("/bin/ed");
    printf("after ed\n");
}

  代码执行结果:

  

  结果分析:

  (1)当终端输入q以后,ed进程结束,并向父进程发送SIGCHLD信号  

  (2)可是因为i_system执行过程当中屏蔽了SIGCHLD信号,所以ed结束后先被主进程waitpid收尸,这个时候父进程a.out是不会收到SIGCHLD信号的

  (3)最后,当全部与ed相关的内容都处理完了以后,最后一个sigprocmask放开SIGCHLD信号的阻塞

  (4)以前因为ed进程结束带来的SIGCHLD信号立刻被处理了,触发sig_chld信号处理函数

  能够看到,因为system忽略了SIGINT和SIGQUIT信号,在system的执行过程当中,不会因为终端发出ctrl+c就会影响父进程a.out的运行;而且因为屏蔽了SIGCHLD信号,不会使得父进程误收到SIGCHLD信号;只有当所system中执行的全部内容都结束后,才会释放对SIGCHLD的阻塞。

  system这个函数考虑的问题太多,实际用到的时候要把与signal相关的东西考虑清楚。

相关文章
相关标签/搜索