Linux 信号 (下)

来源:互联网 发布:文字美图软件 编辑:程序博客网 时间:2024/05/02 04:25

一、可重入函数

当捕捉到信号时 ,不论进程的主控制流程当前执行到哪儿 ,都会先跳到信号处理函数中执行 ,

信号处理函数返回后再继续执行主控制流程。信号处理函数是一个单独的控制流程 ,因为它

和主控制流程是异步的 ,二者不存在调用和被调用的关系 ,并且使用不同的堆栈空间。引入了

信号处理函数使得一个进程具有多个控制流程 ,如果这些控制流程访问相同的全局资源 (全局

变量、硬件资源等 ),就有可能出现冲突 ,如下面的例子所示。

如下是不可重入的函数:

wKiom1eaAhyhgNpSAAFw2qI5zvU845.png

main函数调用insert 函数向一个链表 head中插入节点 node1,插入操作分为两步 ,刚做完

第一步的 时候 ,因为硬件中断使进程切换到内核 ,再次回用户态之前检查到有信号待处理 ,

是切换 到sighandler函数 ,sighandler也调用 insert函数向同一个链表 head中插入节

node2,插入操作的 两步都做完之后从 sighandler返回内核态 ,再次回到用户态就从

main函数调用的insert 函数中继续 往下执行 ,先前做第一步之后被打断 ,现在继续做完第二

步。结果是 ,main函数和 sighandler先后 向链表中插入两个节点 ,而最后只有一个节点真

正插入链表中了。

在一个时间段内,有多个执行流进入同一个函数中,叫作 重入。如果函数执行没有问题,允许重入,则叫做 可重入函数。否则,叫做不可重入函数。

参考:

函数被不同的控制流程调用 , 有可能在第一次调用还没返回时就再次

进入该函 数 , 这称为,例如 insert 函数访问一个全局链表 , 有可能因为重入而造成错乱 , 像这样

的函数称为 不可重入函数 , 反之 ,如果一个函数只访问自己的局部变量或参数 , 则称为可重入(Reentrant) 函数

如果一个函数符合以下条件之一则是不可重入的 :

调用了 mallocfree ,因为 malloc也是用全局链表来管理堆的。

调用了标准 I/O库函数。标准 I/O库的很多实现都以不可重入的方式使用全局数据结构。

SUS规定有些系统函数必须以线程安全的方式实现 ,这里就不列了。

二、volatile 限定符应用:

情景1: int done没有加volatile 且编译不优化 收到信号后正常结束

代码:

     #include <stdio.h>

     #include <signal.h>

    int done = 0; // 【没有加volatile】

void handler(int sig) // 用信号捕捉在另外一个执行流修改 done的值

{

    printf("catch a sig: %d\n", sig);

    done = 1;

}

int main()

{

    signal(2, handler);

    while (!done);

    printf("haha , you should run here");

    return 0;

}

执行:

[bozi@localhost test_volatile]$ makegcc -o volatile_test volatile_test.c // 【不加优化编译】[bozi@localhost test_volatile]$ ./volatile_test^Ccatch a sig: 2haha , you should run here[bozi@localhost test_volatile]$ ^C 【收到信号后正常结束 】

情景2: int done 不加volatile 但是 编译加优化 收到信号后I 在捕捉函数中修改值 但在main中值没有变 一直死循环

运行:

   

  [bozi@localhost test_volatile]$ make     gcc -o volatile_test volatile_test.c -O3    【编译加优化 O3(大写字母O不是数字零)】一直死循环     [bozi@localhost test_volatile]$ ./volatile_test^Ccatch a sig: 2^Ccatch a sig: 2^Ccatch a sig: 2^Ccatch a sig: 2^Ccatch a sig: 2

原因:

     main函数 和 catch是两个执行流,可以并发执行,对于编译器而言,他无法发现程序中存在的多种执行流,他只是看到main函数中的变量done,而

他对done优化后,使得done每次读取都是从寄存器读取,二另外的执行流catch在修改了内存中done的值后,main函数里面还是寄存器的值,所以修改和读取的值是不同的 ,情景三在加了volatile后,告诉编译器,不要对done变量进行优化,编译时加的-O3优化级别对done变量是无效的,使得每次读取都是从内存而不是寄存器,这样就能保证,两个执行流访问的变量值 是一样的。

 

情景3: done前面加volatile修饰 这样两个执行流就访问的done的内容一样了, 结果同情景1 既不加volatile 也不编译优化

     volatile int done;

执行结果:

[bozi@localhost test_volatile]$ makegcc -o volatile_test volatile_test.c -O3[bozi@localhost test_volatile]$ ./volatile_test^Ccatch a sig: 2haha , you should run here[bozi@localhost test_volatile]$

三、竞态条件与sigsuspend函数

     由于异步事件在任何时候都有可能发生 (这里的异步事件指出现更高优 先级的进程 ),如果我们写程序时考虑不周密 ,就可能由于时序问题

而导致错误 ,这叫做 竞态条件 (Race Condition)。

修改之前sleep中的bug

 (1)找bug

代码:

#include<stdio.h>

#include <signal.h>

#include <stdlib.h>

void catch(int sig)

{

}

int my_sleep(int timeout)

{

    signal(SIGALRM, catch); // 用catch 是为了让pause出错返回

    alarm(timeout);      /////////////////////////////////////////////////////////////////////bug

    pause();                 ////////////////////////////////////////////////////////////////////////bug

    int ret = alarm(0);

    signal(SIGALRM, SIG_DFL); // 回复以前的行为 方便别人用时 和自己调用之前是一样的 而不是 变成catch信号后 调用用户自定义函数

    return ret;

}

int main()

{

    while (1)

    {

        printf("hello word\n");

        my_sleep(2);

    }

    return 0;

}

问题出在哪?

根本原因:系统的执行时序不一定是连续的执行,可能上一话执行完进程就切换出去,cpu执行另外一个进程,当这个进程

在此被切换进来时候,才执行下一条语句,这两条语句开起来连在一起,但执行起来,却相隔十万八千里。

上面的bug,就出现在alarm和pause之间。

例如 情景1 , 设置alarm闹铃后,执行完alarm进程切换出去,执行另外一个进程,当另外一个进程执行完, sleep进程有切入,但此时闹铃已经超时了,在执行另外一个程序的时候,闹铃已经响了,处于 未决 状态。在另外一个进程执行完,内核调度sleep进程, 闹铃信号 递达, 进入catch中进行信号捕捉处理,处理完有进入内核,然后再次从内核返回main中的my_sleep执行alarm的下一句pause();可是bug出来了,alarm已经处理,pause将一直阻塞,程序卡在哪儿。

主要原因就是:(1)无阻塞信号的情况:alarm与pause这两句之间有间隙,不是原子操作。 只要有一次进程切换,就可能会多一次信号的处理,这样就可能让后面的等待信号的语句一直 等待,程序阻塞,出现bug。(2) 如果在之前阻塞信号,那么在pause之前要解除阻塞,由于解除阻塞与pause之间也是有间隙的,所以也有可能发生上面的情况。

这种bug不是调试可以出来的, 例如在服务器有很多进程时,进程切换频繁,这样这种bug发生的概率就会增大。

解决方案:

解除信号屏蔽 ”和 “挂起等待信号 ”这两步能合并成一个原子操作就好了 ,这正是 sigsuspend函数的功 能。 sigsuspend包含了pause的挂起等待功能 ,同时解决了竞态条件的问题 ,在对时序要求严格的场合下都应该调用 sigsuspend而不是 pause。

     函数:

       #include <signal.h>        int sigsuspend(const sigset_t *mask);

          

sigsuspend没有成功返回值,只有执行了一个信号处理函数之后sigsuspend才返回, 返回值为-1,errno设置为 EINTR调用sigsuspend ,进程的信号屏蔽字由 sigmask参数指定 ,可以通过指定 sigmask来临时解除对某 个信号的屏蔽,然后挂起等待 , sigsuspend返回时 ,进程的信号屏蔽字恢复为原来的值,如果原来对该信号是屏蔽的 , sigsuspend返回后仍然是屏蔽的。

改进代码:

    

 #include<stdio.h>#include <signal.h>#include <stdlib.h>void catch(int sig){}int my_sleep(int timeout){//    signal(SIGALRM, catch); // 用catch 是为了让pause出错返回//    alarm(timeout);////    pause();//    int ret = alarm(0);//    signal(SIGALRM, SIG_DFL); // 回复以前的行为 方便别人用时 和自己调用之前是一样的 而不是 变成catch信号后 调用用户自定义函数//    return ret;    struct sigaction newact, oldact;    sigset_t newmask, oldmask, suspmask;    unsigned int ret;    newact.sa_handler = catch;    sigemptyset(&newact.sa_mask);    newact.sa_flags = 0;    sigaction(SIGALRM, &newact, &oldact); // 注册信号捕获自定义方法    sigemptyset(&newmask);    sigaddset(&newmask, SIGALRM);    sigprocmask(SIG_BLOCK, &newmask, &oldmask); // 加信号屏蔽字    alarm(timeout); // 生成闹钟    suspmask = oldmask;    sigdelset(&suspmask,SIGALRM); // 用于sigsuspend 删除SIGALRM    sigsuspend(&suspmask);// 执行完后 SIGALRM 的信号屏蔽字又会恢复到    // sigprocmask 加SIG_BLOCK的状态 所以在    // 下面 用 sigprocmask 恢复成不加block的状态    ret = alarm(0);// 清除闹钟    sigaction(SIGALRM,&oldact, NULL);// 恢复 信号之前的处理方法    sigprocmask(SIG_SETMASK, &oldmask, NULL); // 取消信号的阻塞    return ret;// sleep未执行时间}int main(){    while (1)    {        printf("hello word\n");        my_sleep(2);    }    return 0;}

四、SIGCHLD信号

3 种方式 解决僵尸进程

方式一:wait 阻塞式的 死死地等 就跟烧开水一样,一直等到水烧开,自己啥都不做

方式二:waitpid 非阻塞方式  轮询 过一段时间 询问询问一下子进程 就跟烧开水一样,不过是每隔一定时间看一下水烧开没,期间自己做自己的事情

方式三: SIGCHLD信号      这个人性化,水壶有烧开水报警功能,水开了自己就响了(发信号),期间人做自己想做的事情

所以 主进程只要 捕获这个信号就可以了

子进程退出,是会向父进程发送SIGCHLD信号的

wait和waitpid得到的子进程退出状态state包含 退出码(return exit的值 用这判断子进程 【执行结果是否正确】state次低8位) 和 退出信号(不是SIGCHLD信号 是别的进程发送给子进程的信号 如果没有就表示子进程正常退出【所有用这个查看子进程是否 正常退出】state低8位)

例子:

#include<stdio.h>#include<signal.h>#include<wait.h>#include <stdlib.h>void catch(int sig){    printf("father catch sig %d", sig);        int status = 0;          //if (ret > 0)        // 用while 因为多个子进程同时退出 父进程只接收 一次信号        // 如果用if 这一次waitpid 成功 只是回收一个子进程        // 用while 可以防止遗漏        while (waitpid(-1, &status, WNOHANG ) > 0) // -1 等待任意一个子进程          {              printf("exit code:%d\n", (status>>8)&0xff);              printf("exit sig:%d\n", (status)&0xff);          }        exit(0);}int main(){    signal(SIGCHLD, catch);    pid_t id = fork();    if (id == 0)    {        // child        //        printf("child\n ");        sleep(5);        exit(1);    }    else    {        //father        printf("father\n");    //    int status = 0;    //    pid_t ret = waitpid(id, &status, 0);    //    if (ret > 0)    //    {    //        printf("exit code:%d\n", (status>>8)&0xff);    //        printf("exit sig:%d\n", (status)&0xff);    //    }        while (1)        {            printf("father do his work ...\n");            sleep(1);        }        return 2;    }    return 0;}

运行结果:

[bozi@localhost test_20160726]$ ./test_SIGCHLD_1fatherfather do his work ...childfather do his work ...father do his work ...father do his work ...father do his work ... father catch sig 17exit code:1exit sig:0[bozi@localhost test_20160726]$ echo $?0


本文出自 “城市猎人” 博客,请务必保留此出处http://alick.blog.51cto.com/10786574/1831418

0 0