35 信号处理

什么时候捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,称为捕捉信号,由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:用户程序注册了SIGQUIT信号的处理函数sighandler。当前正在执行main函数,这时发生中断或异常切换到内核态,在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。内核决定返回用户态后不是回复main函数的上下文 继续执行,而是执行sighandler函数,sighandler和main函数使用不同的栈空间,它们之间不存在调用和被调用的关系是两个独立的控制流程,sighandler函数返回后自动执行特殊的系统调用四个热突然再次进入内核态,如果没有新的信号递达,这次再返回用户态就是回复main函数的上下文继续执行了

什么时候处理

当进程从内核态返回用户态的时候,进行信号的检测和处理。内核态让进程可以访问os的代码和数据。例如调用系统调用时,操作系统会做身份切换,变成内核身份,cpu的int80中断,可以让用户态陷入内核态

在这里插入图片描述
在这里插入图片描述

整个信号处理过程类似一个8,中间的横线上面是用户,下面是内核,从用户代码开始,需要四个状态切换,中间的交点就是信号检测的时候。信号的处理函数是在用户态运行的,信号检测到后要切换到用户态处理信号,因为os不信任用户代码,如果有非法行为不能在内核执行,所以在用户态执行完后栈中的sigreturn返回到内核中,才知道主函数执行到哪了,返回用户态继续执行

就算进程中没有任何系统调用,库函数等,在时钟片到达,进程剥离cpu的时候也需要陷入内核才可以

3. 重新看地址空间

在这里插入图片描述

用户空间的地址有页表来映射,os空间也有内核的页表映射找到物理地址。用户页表有几个进程就要有几个,内核页表只需要有一份。因为每一个进程看到的3-4GB的内容都是一样的,和动态库一样。整个系统中,进程再怎么切换,3-4GB的内容是不变的。在进程视角中,调用系统方法,就是在自己的地址空间中进行,os视角,任何时刻,由进程执行os代码,可以随时执行

在这里插入图片描述

进程由os来推动运行,那os又是由谁来推动运行的
os本质是基于时钟中断的一个死循环。在硬件中,有一个时钟芯片,每隔很短的时间向计算机发送时钟中断,os收到后从执行pause停止,执行相应的中断任务,如进程的调度

计算机中的时间无论连不联网都是准确的,这是因为内部有一个一直运行的时钟芯片,关机的时候也在运行,计数器一直++,然后和上一次时间做计算得到现在的时间

如何判断内核权限

在这里插入图片描述

一个进程想访问os的内容是不被允许的,cpu中有cr3寄存器指向的进程页表,ecs寄存器中低两位的数值用来判断当前是用户态还是内核态,只有是内核态,才有资格访问os,想要修改这个状态,cpu提供了int80陷入内核的方法,可以改为内核态或用户态。除此之外,想访问内核仍有很多限制

捕捉函数

sigaction

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

函数可以读取和修改指定信号相关联的处理动作。调用成功则返回0,出错返回-1,signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作,act和oact指向sigaction及饿哦固体
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,复制为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用户自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void。可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用

当谋和信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动回复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这个信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。so_flags字段包含一些选项,设置为0,sa_sigaction是实时信号的处理函数

sigaction结构

在这里插入图片描述

主要关注第一个和第三个参数,第一个是自定义的函数。第三个参数当这个信号被屏蔽的时候还希望同时屏蔽其他信号,可以设置

测试这个函数的基本捕捉功能

#include <signal.h>
#include <stdio.h>
#include <cstring>
#include <unistd.h>
#include <iostream>

using namespace std;
void handler(int signo)
{
    printf("catch a signo:%d\n", signo);
}

int main()
{
    struct sigaction act;
    memset(&act, 0, sizeof(act));
    act.sa_handler = handler;
    sigaction(2, &act, nullptr);

    while (true)
    {
        printf("%d\n", getpid());
        sleep(1);
    }

    return 0;
}

在这里插入图片描述

当一个信号正在被处理时,这个信号会被阻塞,防止信号处理的嵌套调用。只有在处理完毕后才会返回继续检测。信号的pend位图是在什么时候修改的,关于这两个问题验证一下:

#include <signal.h>
#include <stdio.h>
#include <cstring>
#include <unistd.h>
#include <iostream>

using namespace std;
void printblock()
{
    sigset_t set, ost;
    sigprocmask(SIG_BLOCK, nullptr, &ost);
    printf("block: ");

    for (int i = 31; i >= 1; i--)
    {
        if (sigismember(&ost, i))
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}

void printpend()
{
    sigset_t set;
    sigpending(&set);
    printf("pend: ");
    for (int i = 31; i >= 1; i--)
    {
        if (sigismember(&set, i))
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}

void handler(int signo)
{
    printpend();  //打印pend
    printblock(); //打印block
    int n = 5;
    while (n > 0)
    {
        printf("catch a signo:%d\n", signo);
        sleep(1);
        n--;
    }
}

int main()
{
    struct sigaction act;
    memset(&act, 0, sizeof(act));
    act.sa_handler = handler;
    sigaction(2, &act, nullptr);

    while (true)
    {
        printf("%d\n", getpid());
        printblock();
        sleep(1);
    }

    return 0;
}

在这里插入图片描述在这里插入图片描述

结论
图1:当收到2号信号到达处理函数时,pend表已经将信号修改为无。block表修改为阻塞状态,当执行完后解除阻塞
图2:信号递达时,由于信号被os设置为阻塞,再次发送信号,pend表位1,但不会再递达,处于未决状态,递达完毕后才会再次递达,递达过程中只会保存一次信号

信号递达时可以让屏蔽多个信号,返回时自动解除

	struct sigaction act;
    memset(&act, 0, sizeof(act));
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 4);
    sigaddset(&act.sa_mask, 5);

可重入函数

在这里插入图片描述

上面是一个链表的操作,main函数在链表里插入了node1,在插入的过程中收到了信号,信号的处理动作是在相同位置插入node2节点,当插入完成后回到insert函数,又改变了头节点的指向,指到node1节点,完成了node1的插入。此时node2节点没有节点指向它,就变了内存泄露的一个节点

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回到用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler函数也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完后返回内核态,再次回到用户态从main函数调用的insert函数中继续往下执行,闲情做第一步之后杯打断,现在继续做完第二步。结果是,main函数和sighanler先后,向链表中插入两个节点,而最后只有一个节点真正插入链表中

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数。反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。为什么两个及控制流程调用同一个函数,访问它的同一个局部变量或参数不会造成错乱?
因为sighandler的函数和main调用的是两个栈空间,局部变量不会造成冲突

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

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都是不可重入的方式使用全局数据结构

volatile

下面的一个代码,main函数中对flag变量没有做改变,编译器识别后可能会将flag全局变量优化到寄存器中

#include <signal.h>
#include <stdio.h>
#include <cstring>
#include <unistd.h>
#include <iostream>

using namespace std;
void printblock()
{
    sigset_t set, ost;
    sigprocmask(SIG_BLOCK, nullptr, &ost);
    printf("block: ");

    for (int i = 31; i >= 1; i--)
    {
        if (sigismember(&ost, i))
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}

void printpend()
{
    sigset_t set;
    sigpending(&set);
    printf("pend: ");
    for (int i = 31; i >= 1; i--)
    {
        if (sigismember(&set, i))
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}
int flag = 0;
void handler(int signo)
{

    printf("catch a signo:%d\n", signo);
    flag = 1;
}


int main()
{


    signal(2, handler);
    //flag可能会优化到寄存器变量
    while (!flag);

    printf("process quit\n");
    return 0;
}

在这里插入图片描述
没有优化时,发送2号新号会退出
在这里插入图片描述

当我们编译时加入O1优化,进程就不能退出了
这时因为默认编译不做优化,O1优化后,os将这个变量优化到寄存器中。一般情况下,访问变量都要从内存中读取,寄存器变量后,发送信号,内存中的值修改了,但cpu只访问寄存器中的值,导致内存不可见了。register关键字就是建议优化为寄存器变量,只是建议,最终结果还是看具体情况

volatile关键字的作用:
保存内存的可见性,告知编译器,被修饰的关键字的变量,不允许被优化,对该变量的任何操作,都必须在真是的内存中进行

SIGCHLD信号

子进程在退出后,会向父进程发送SIGCHLD(17)信号

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

void handler(int signo)
{
    printf("catch a signo: %d", signo);
}

int main()
{
    signal(17, handler);
    pid_t id = fork();
    if (id == 0)
    {
        sleep(5);
    }
    while (true)
    {
        sleep(1);
    }
}

在这里插入图片描述

等待的好处
信号的方式还是要调用wait/waitpid这样的接口
1.获取子进程的退出状态,释放子进程的僵尸
2.虽然不知道父子谁先运行,但一定是父进程最后退出

如果有多个进程需要回收,当回收第一个进程的时候,会把这个信号屏蔽掉,这时如果好几个进程都发了信号,就会得不到回收,还是僵尸进程,这种情况可以通过判断wait返回值,只要有需要回收的就一直回收

void handler(int signo)
{
    pid_t rid;
    while (rid = waitpid(-1, nullptr, 0) > 0)
    {
        printf("catch a signo: %d", signo);    
    }
   
}

阻塞方式如果回收一半,就会一直卡在判断里,所以采取非阻塞方式等待可以回收多个进程

当父进程不关心子进程的结果,可以让子进程自动清理,不需要通知父进程,可以将信号处理方式设置为忽略。只对linux有用

signal(17, SIG_IGN);

17号的默认处理动作是忽略,我们又设置了忽略,为什么这样就可以了。17号的默认是它对信号的处理是默认方式,默认方式忽略执行。而设置信号的处理方式为忽略,是忽略子进程的处理方式

进程第一章用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞的查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了,采用第二种方式,父进程在处理自己的工作的同时还要记得是不是的轮询一下,程序实现复杂

其实,子进程在终止时会给父进程发sigchld信号,该信号的默认处理动作是忽略,父进程可以自定义SIGHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait,清理子进程即可

事实上,由于UNIX的历史原因,要想不产生僵尸进程还有另外一种方法,父进程调用sigaction将sigchold的处理动作设置为SIG_IGN没这样fork出来的紫禁城在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程,系统默认的忽略动作和用户用sigaction函数自定义的忽略,通产是没有区别的,但这是一个特例。此方法对于linux可用,不保证在其他UNIX系统上都可用

  • 30
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值