Linux 信号 (下)

一、可重入函数

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

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

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

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

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

如下是不可重入的函数:

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]$ make
gcc -o volatile_test volatile_test.c // 【不加优化编译】
[bozi@localhost test_volatile]$ ./volatile_test
^Ccatch a sig: 2
haha , 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]$ make
gcc -o volatile_test volatile_test.c -O3
[bozi@localhost test_volatile]$ ./volatile_test
^Ccatch a sig: 2
haha , 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_1
father
father do his work ...
child
father do his work ...
father do his work ...
father do his work ...
father do his work ...
 father catch sig 17exit code:1
exit sig:0
[bozi@localhost test_20160726]$ echo $?
0


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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值