当前位置: 首页 > article >正文

【Linux】--- 信号阻塞、信号捕捉

【Linux】--- 信号阻塞、信号捕捉

  • 一、信号阻塞
    • 1、信号集
    • 2、操作信号集
    • 3、sigprocmask
    • 4、sigpending
    • 5、sigaction
  • 二、信号捕捉
    • 1、用户态与内核态
    • 2、信号捕捉的时机

一、信号阻塞

1、信号集

我们主要讨论的是非实时信号,当一个进程收到非实时信号后,不是立马处理的,而是会挑选合适的时候进行处理,那么既然信号会被延时处理,就要有一个机制来保存进程之前收到的信号。
在了解这个保存信号的机制前,我们先了解一些信号的相关概念:

  • 信号递达(Delivery):进程处理信号的过程称为递达,递达可以是执行默认处理函数,或者执行自定义的信号处理函数,忽略信号ign也是一种处理信号的方式,也算递达
  • 信号未决(Pending):当进程收到一个信号,但是还没有处理这个信号,称为未决
  • 信号阻塞(Block):当一个信号被阻塞,就会一直保留在未决状态,不会执行任何处理函数

注意忽略信号ign与信号阻塞不同,忽略信号是一种递达方式,即进程处理这个信号的方式就是忽略它;而阻塞则是相当于进程根本收不到这个信号,信号被阻挡了。

对应未决,阻塞,递达三个信号的状态,Linux内核中,进程的PCB维护了三张表pending,block,handler:

在这里插入图片描述
pending:该表的本质是一个位图,也称为未决信号集。当进程接收到一个信号,会把对应的比特位修改为1,表示进程已经接收到该信号

block:该表的本质是一个位图,也称为阻塞信号集。当进程收到信号,在pending中把对应的位修改1,此时就要经过block,如果block中对应的位为1,表示该信号被阻塞,不会被递达,penidng上的该位一直保持为1如果block中对应的位为0,表示该信号未被阻塞,进程挑选合适的时候递达该信号。

handler:该表本质是一个函数指针数组,指向信号的处理函数。如果时机合适,进程会检测pending表和block表,然后检测出已经接收到的信号,若该信号未被阻塞,执行对应信号的处理函数,并把pending中的该位变回0,表示该信号已经处理完了。

以上表还有以下特性:

  1. 当用户通过signal修改信号的默认处理方式,其实就是在修改这个handler内部的函数指针。
  2. 如果连续收到多个相同的非实时信号,此时pending位图只会记录一次,如果是实时信号,则会把收到的所有信号放进队列中,每个信号都会被处理。

2、操作信号集

简单了解了这三张表后,我们又要如何操纵这三种表呢?

对于handler表来说,其实就是通过signal函数来修改内部的处理函数,而对于block和pending表,有另外一套系统调用来处理。

block和pending表它们都叫做信号集,本质都是一张位图,要做的无非就是修改某一个位是0还是1,因此这两个表的操作是一样的。

操作这两个信号集,都依赖一个类型sigset_t,其包含在<signal.h>中,Linux中该类型的源码如下:

typedef struct
{
  unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;//注意这前面有两个下划线


typedef __sigset_t sigset_t;//这次typedef后,前面没有下划线了

也就是说,sigset_t本质是一个结构体,结构体内部只有一个成员,且该成员是一个数组。这个数组就是用于存储位图的工具。从宏观上看,你可以理解为sigset_t就是一个位图,不过这不太严谨。

想要操作这张信号集,需要通过以下五个函数,这些函数也需要头文件<signal.h>,函数原型如下:

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);

  • int sigemptyset(sigset_t *set);
  • int sigfillset(sigset_t *set);
  • int sigaddset(sigset_t *set, int signum);
  • int sigdelset(sigset_t *set, int signum);
  • int sigismember(const sigset_t *set, int signum);

前四个函数的返回值都是:如果成功返回0,失败返回-1。

也就是说,我们可以通过以上函数,来操作信号集这个位图,但要注意,我们通过这个函数操作的信号集,既不是block也不是pending,它目前只是一个进程中的变量而已。

那么我们接下来要做的,就是把我们自己创建并设置的信号集,与block和pending交互。

3、sigprocmask

sigprocmask函数用于读取或者更改进程的block,需要头文件<signal.h>,函数原型如下:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数:
在这里插入图片描述

4、sigpending

sigpending函数用于读取进程的pending,需要头文件<signal.h>,函数原型如下:

int sigpending(sigset_t *set);

参数:

set:输出型参数,将pending传入到set中

接下来我们综合以上的所有接口,进行几个实验:

1、证明block确实可以阻塞信号,信号确实保存在pending中

int main()
{
    sigset_t set;

    sigemptyset(&set);
    sigaddset(&set, 2);
    sigprocmask(SIG_BLOCK, &set, nullptr);

    while (true)
    {
        sigset_t pending_set;
        sigpending(&pending_set);

        for (int i = 31; i > 0; i--)
        {
            if (sigismember(&pending_set, i))
                cout << "1";
            else
                cout << "0";
        }
        cout << endl;

        sleep(1);
    }

    return 0;
}

首先定义了一个sigset_t类型的变量set,由于不清楚这个变量会被初始化为什么样子所以要用sigemptyset将其全部初始化为0;

此处我们用(2) SIGINT做检测,先通过sigaddset(&set, 2)把set中的第二位变为1,随后通过sigprocmask(SIG_BLOCK, &set, nullptr)将set添加到block中,由于我们并不想知道旧的block是什么样,所以第三个参数设为nullptr。

接着进程陷入一个死循环,循环体内通过sigpending获取当前进程的pending,随后通过一个for循环将这个pending输出,此处要注意:不能直接通过循环输出结构体内部的数组,必须通过sigismember检测一个位是0还是1。

这个pending是用于保存进程中的未决信号的,我们已经把(2) SIGINT阻塞了,如果预测没有错误的话,那么输入ctrl + C时,pending的第二位会变成1,这就说明我们已经接收到该信号了。但是block把(2) SIGNAL给阻塞了,导致其一直处于pending中,无法被递达,所以pending的第二位会一直是1。

输出结果:
在这里插入图片描述

2、检测是否所有信号可以被阻塞

void showSet(sigset_t *pset)
{
    for (int i = 31; i > 0; i--)
    {
        if (sigismember(pset, i))
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}

int main()
{
    sigset_t set, old_set;

    sigemptyset(&old_set);
    sigfillset(&set);
    sigprocmask(SIG_BLOCK, &set, &old_set);

    cout << "old_set: ";
    showSet(&old_set);

    sigprocmask(SIG_BLOCK, &set, &old_set);

    cout << "new_set: ";
    showSet(&old_set);
    
    return 0;
}

以上代码中,我将输出信号集的各个比特位的功能,封装为了一个函数showSet,然后定义了两个信号集set和old_set。我们的目的是:将set的所有位变成1,然后添加到block中,在将block提取到old_block中。最后观察old_block,就可以知道是否block被成功设置为了全1。

首先通过sigemptyset把old_set设为全0,通过sigfillset把set设为全1。接着通过sigprocmask(SIG_BLOCK, &set, &old_set);把set添加到block中,把初始的block添加到old_block中,随后通过showSet输出old_set。

此处要注意,第一次old_set提取到的,不是设置后的block而是设置前的block,也就是说现在old_set拿到的是block的默认值。

现在我们要拿到block被添加了全1后的值,所以要再进行一次sigprocmask(SIG_BLOCK, &set, &old_set);,这次拿到的是上一次的block的值,也就是添加了全1后的值,再通过showSet输出。

以上代码我们输出了两次old_set,此处再强调一遍:第一次输出的是block的默认值,第二次输出的是全1设置后的block。

输出结果:

在这里插入图片描述

  • 第一次输出为全0,说明block的默认值是全0,不阻塞任何信号

为什么第二次输出时,我们明明把全1的set添加到block中,却不是全1呢?

不妨设想:如果一个进程将所有信号都阻塞了,那么我们就无法通过任何信号杀死它,出现一个不死的进程,如果这个进程是一个病毒,那么就会带来大麻烦。因此操作系统设计的时候,就应该避免这个情况,所以有一些信号不允许被阻塞!

通过实验现象可以看出来:信号(9) SIGKIILL和(19) SIGSTOP不允许被阻塞。

3、信号被递达时,block表和pending表是什么状态

void showSet(sigset_t *pset)
{
    for (int i = 31; i > 0; i--)
    {
        if (sigismember(pset, i))
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}

void handler(int sig)
{
    cout << "get sig:" << sig << endl;

	sigset_t pending_set;
	sigemptyset(&pending_set);
    sigpending(&pending_set);
    cout << "pending: ";
    showSet(&pending_set);
    
    sigset_t tmp;
	sigemptyset(&tmp);
	
    sigset_t block_set;
	sigemptyset(&block_set);
    sigprocmask(SIG_BLOCK, &tmp, &block_set);
    cout << "block:   ";
    showSet(&block_set);
    
    exit(0);
}

int main()
{
    signal(2, handler);

    while (true);

    return 0;
}

以上代码,先通过signal(2, handler);设置信号(2) SIGINT的处理函数,随后进程陷入while的死循环。

在handler中,会先通过sigpending(&pending_set);获取当前的pending,随后输出这个pending。再通过sigprocmask(SIG_BLOCK, &tmp, &block_set);,获取当前的block,存到block_set中。

输出结果:
在这里插入图片描述
我们通过ctrl + C给进程发送(2) SIGINT,毫无疑问pending的第二位会被设置为1。

进入handler后,发现pending的第二位为0,我们明明发送信号把第二位设置为1,为什么输出的时候发现是0呢?

这说明在处理handler前就已经把pending变回0了!也就是先清除信号,后递达。

另外的,我们发现block的第二位变成了1,我们明明没有阻塞(2) SIGINT,为什么显示block的第二位是1呢?

这是为了防止信号在handler中自己触发自己,导致信号的嵌套调用,比如这样:

void handler(int sig)
{
	kill(getpid(), 2);
}

上面这个handler中,再次触发了信号,这样会造成无限递归,因此操作系统在处理信号的时候,把对应的位阻塞,防止无限递归。

通过以上三个实验,我们熟悉了信号相关的接口,并证明了一些知识点:

1、信号确实是被保存在pending中的,block确实可以阻塞信号递达
2、block的默认值为全0,不阻塞任何信号,信号(9) SIGKIILL和(19) SIGSTOP不允许被阻塞
3、信号是先清除,后递达的
4、操作系统在处理信号的时候,把对应的位阻塞,防止无限递归

5、sigaction

了解前面的接口后,我们来讲解本博客的最后一个接口sigaction。

刚刚我们说:操作系统在处理信号的时候,把对应的位阻塞,防止无限递归。

操作系统提供了sigaction接口,可以让我们在处理信号的时候,自定义要阻塞哪些信号=!

sigaction用于设置信号处理的自定义函数和block,需要头文件<signal.h>,函数原型如下:

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数:

第一个参数signum用于指定该处理函数作用于哪一个信号。第二个参数和第三个参数的类型都是struct sigaction,接下来我简单讲解以下这个结构体:

该结构体用于描述信号的处理方式,定义如下:

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};

在这里插入图片描述
第二个参数act用于传入信号处理方式,oldact用于接收老的信号处理方式。

示例:

void showSet(sigset_t *pset)
{
    for (int i = 31; i > 0; i--)
    {
        if (sigismember(pset, i))
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}

void handler(int sig)
{
    cout << "get sig:" << sig << endl;

    sigset_t block_set;
    sigset_t tmp;
    sigemptyset(&block_set);
    sigprocmask(SIG_BLOCK, &tmp, &block_set);
    cout << "block:   ";
    showSet(&block_set);

    exit(0);
}

int main()
{
    struct sigaction act;
    act.sa_handler = handler;
    act.sa_flags = 0;

    sigemptyset(&act.sa_mask);
    
    for (int i = 1; i <= 5; i++)
        sigaddset(&act.sa_mask, i);

    sigaction(2, &act, nullptr);

    while (true);

    return 0;
}

以上示例中,先定义了struct sigaction act,用于传入信号处理方式。其sa_handler 用于传入处理函数handler,sa_flags 设为0即可。然后通过sigemptyset把sa_mask全部位变成0,再通过一个for循环把前五位变成1。

最后通过sigaction(2, &act, nullptr),设置2号信号的处理方式。在handler内部,获取并输出当前的block信号集。

按照预期,在处理handler的时候,block的前5位会被设置成1。

输出结果:
在这里插入图片描述

二、信号捕捉

之前讲解信号的递达时,我一直说:在合适的时候,操作系统会处理信号,那么问题来了,到底什么时候才是合适的时候?也就是说,到底什么时候操作系统会去处理pending中的信号呢?

为了解决这个问题,我们要先了解操作系统的用户态与内核态。

1、用户态与内核态

Linux 操作系统是一个多用户、多任务的操作系统,为了安全性和资源管理,它将系统划分为 用户态 和 内核态 两种运行模式。

每个进程都有自己独立的进程地址空间:

在这里插入图片描述
在这里插入图片描述
进程从用户态切换到内核态主要有以下几种情况:
在这里插入图片描述
在这里插入图片描述

2、信号捕捉的时机

讲了这么多用户态与内核态,那么这和信号有什么关系?

在从内核态返回用户态之前,操作系统会处理信号

执行过程大致如下图:
在这里插入图片描述
在这里插入图片描述
如果信号的处理方式是默认处理方式,此时直接在内核态执行代码,主要有两个原因:

  1. 信号的默认处理方式,是操作系统自己提供的,因此不会有安全性问题,可以直接以内核态的高级权限执行
  2. 大部分信号的默认处理方式,是直接杀掉当前进程,杀掉进程的行为,需要内核态的权限,因此直接在内核态就可以杀掉这个进程

在这里插入图片描述

也就是说,陷入内核态之后,只有内核态知道之前的用户态执行到哪里,所以E状态下不能直接跳转回原来执行的地方,必须先回到内核态,去找到原先执行的位置,在返回用户态。

每一次在从内核态返回用户态之前,操作系统都会处理信号

这句话是什么意思呢?我们先前说在从内核态返回用户态之前,操作系统会处理信号,这句话完全没有问题的。但我在此要额外强调一个每一次。

再次看到下图:
在这里插入图片描述
请问上图中,发生了几次内核态返回用户态?

一共发生了两次,也就是我标红的这两个箭头C->E,F->A,这两个时候都会检测并处理信号。

比如说某一次在E状态下处理完毕一个信号后,回到F,再准备回到A的时候,操作系统就会再做一次检测,检测还有没有要处理的信号,如果有,继续处理。


http://www.kler.cn/a/550378.html

相关文章:

  • TransCNN模型详解
  • JUC并发二
  • 健康的生活方式小结---秋浦四郎
  • Micropython RPI-PICO 随记-LVGL实战3 综合调试
  • Java中CompletableFuture异步工具类
  • 微信云开发小程序音频播放踩坑记录 - 从熄屏播放到iOS静音
  • 碰一碰发视频@技术原理与实现开发步骤
  • 在docker中部署fastdfs一些思考
  • 2步破解官方sublime4最新版本 4192
  • Dest1ny漏洞库: 美团代付微信小程序系统任意文件读取漏洞
  • 基于 Python typing 模块的类型标注
  • 力扣hot100_矩阵_python版本
  • ORB-SLAM3的源码学习:TwoViewReconstruction通过两幅图像来实现重建
  • 2024Selenium自动化常见问题及解决方式!
  • 【云原生】最新版Kubernetes集群基于Containerd部署
  • STM32 PWM脉冲宽度调制介绍
  • 又是阿里云npm install报错:ENOENT: no such file or directory, open ‘/root/package.json‘
  • Kubernetes控制平面组件:etcd常用配置参数
  • 抢占川南数字枢纽高地:树莓集团将翠屏区位优势转为产业胜势
  • JavaScript数组-数组的概念