【Linux】进程信号 --- 信号保存

在这里插入图片描述

👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:Linux
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍


一、再次认识信号

1.1 为什么要进行信号保存

在入门篇提到过:进程收到信号之后,可能不会立即被处理,因为进程可能正在做重要的事,需要等到合适的时间再处理。就比方说:你收到外卖小哥的外卖到了的提醒消息(进程收到信号),但你正在打LOL(更重要的事),晚点下去拿(保存信号),当打完之后再下去(处理信号)。因此,从信号产生到信号处理这段期间,需要将信号保存起来,那如何保存呢?位图!(具体看1.2

  • 对于普通信号,它用的是位图,只要收到就会先保存,但是如果这个信号还没处理,又来个信号,那么就只记得最近一次的信号
  • 而对于实时信号。只要发送了,就要立即处理,哪怕此时进程在忙。它的用的是队列。
  • 关于进程信号,我们重点关心普通信号即可。

1.2 信号如何被保存

信号列表

对应的1~31号信号我们称为普通信号,是不是一个 int整型(32bit 就足以表示所有普通信号的产生信息了。

对于普通的信号处理而言,进程主要关心自己是否有信号以及收到了哪个具体的信号。并且,这个信号是由操作系统发送给进程的进程控制块(task_struct。所以结构体task_struct内一定维护类似于int signal字段。

如果给进程发的是一号信号,那么则将bit位的第一位给置为1(注意这里有第0位,表示没有收到信号),后面以此类推。所以描述一个信号,用比特位的位置来表示,即 普通信号是用位图来管理信号

总结:

  1. 比特位的内容是0还是1,表明是否收到信号。
  2. 比特位的位置(第几个),表示信号的编号。
  3. 所谓的“发信号”,本质就是操作系统(管理者)去修改task_struct的信号位图对应的比特位。也就是写信号

说明:在后面我们会说,信号是被保存在pending表中的

1.3 信号其他相关常见概念

在这里插入图片描述

  • 信号产生(Produce:由四种不同的方式发出信号。(详情见
  • 信号未决(Pending:信号从产生到处理的中间状态。(信号保存)
  • 信号递达(Delivery:进程收到信号后,对信号的处理动作。(信号处理)

除此之外,进程还允许阻塞(屏蔽)某些信号,我们称之为 信号阻塞这意味着进程暂时不接收阻塞的信号,使其保持在未决信号集合中,只有在解除阻塞后,信号才会被递送到信号处理程序中。注意:信号阻塞是一种手段,可以发生在 信号处理 前的任意时段

注意区分阻塞和忽略(信号处理动作):忽略是真的什么都不做,相当于“已读不回”;而阻塞是信号还没到处理阶段,它可能会被处理,可能不会被处理,相当于“未读”,是一种状态。

二、在内核中的表示

在这里插入图片描述

在操作系统中,有三张表分别是block表、pending表、handler表。共同构成了操作系统内核管理信号的机制。

  • block表:也称信号阻塞表(位图)。它主要用于记录信号有没有被阻塞。如果某信号(比特位的位置)被设置成1,表示信号被阻塞;如果某信号被设置成0,表示信号没有被阻塞。
  • pending表:也称未决信号表(位图)。它主要用于记录已经向进程发送但尚未被处理的信号(信号保存)。如果信号(比特位的位置)对应的比特位是1,则表示该信号是pending的,即信号产生但还未被处理。当进程的信号处理函数还没有准备好处理信号时,信号会保持在pending表中。一旦信号的处理函数准备好,内核会从pending表中选择一个信号交付给进程。注意:如果这个信号还没处理,又来个信号,那么就只记得最近一次的信号
  • handler表:也称信号处理程序表(函数指针数组)该表存储着信号[1,31]的系统默认处理动作的函数指针(函数地址);如果用户自定设定了方法(如singal函数自定义处理方式),则会将该方法的地址填入到信号处理程序表中。当进程接收到一个信号时,内核会查找该信号对应的处理函数,并执行该处理函数来响应信号事件。
  • 信号它的一切操作都是围绕这三张表!!!

处理信号有三种方式:忽略SIG_IGN、系统默认动作SIG_DEL、用户自定义。用户自定义在【信号产生】已经用很多次了,这次来见见忽略和系统默认动作。

  • Linux操作系统对于忽略和默认动作的定义

在这里插入图片描述

默认动作就是将0强转为函数指针类型,忽略动作则是将1强转为函数指针类型,分别对应handler 表中的01下标位置

我们可以使用代码来看看效果

  • 忽略动作SIG_IGN

在这里插入图片描述

【程序结果】

在这里插入图片描述

  • 系统默认动作SIG_DEL

在这里插入图片描述

【程序结果】

在这里插入图片描述

三、操作系统中位图的数据类型

3.1 sigset_t — 信号集类型

无论是block表还是pending表,它们都是位图结构,同时也是内核的数据结构。由于操作系统不相信任何用户,它不允许用户直接修改这两张表。所以要修改这些表,操作系统一定提供了一系列的系统调用接口。

在内核中,操作系统将信号操作所需要的位图结构封装成了一个结构体类型__sigset_t,在用户层,我们可以直接使用sigset_t类型。

在这里插入图片描述

sigset_t称为信号集类型,这个类型可以表示每个信号的状态。

  • 在阻塞信号集block表中的含义是该信号是否被阻塞。
  • 而在未决信号集pending表中的含义是该信号是否处于未决状态。

至于这个类型内部如何存储这些比特位则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如直接打印sigset_t变量是没有意义的!

3.2 信号集操作函数

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • sigemptyset函数:初始化set所指向的信号集,将所有信号的对应比特位清零,表示该信号集不包含任何有效信号。
  • sigfillset函数:初始化set所指向的信号集,将所有信号的对应bit设置成1,表示该信号集的有效信号包括系统支持的所有信号。
  • sigaddset函数:向指定的信号集中添加特定的信号,设置1
  • sigdelset函数,向指定的信号集中去掉特定的信号,设置0
  • 以上四个函数都是成功返回0,出错返回-1
  • sigismember函数:判断一个信号集的有效信号中是否包含某种信号。若包含则返回1,不包含则返回0

注意:在使用sigset_ t类型的变量之前,一定要调用sigemptysetsigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddsetsigdelset在该信号集中添加或删除某种有效信号。

四、系统调用接口 — sigprocmask

sigprocmask函数用来对block表进行操作(阻塞信号)。函数原型如下:

#include <signal>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数解释:

  • how参数:指定操作的类型,可以是以下值之一:

    • SIG_BLOCK:将 set中的信号添加到当前进程的block表中(mask|=set)。
    • SIG_UNBLOCK:从当前进程的block表中移除set中的信号(mask&=~set)。
    • SIG_SETMASK:设置当前进程的block表为 set 中的值(mask==set)。
  • set 参数:就是一个信号集,主要从此信号集中获取屏蔽信号信息

  • oldset 参数:指向 sigset_t 类型的指针,用于存储之前的block表。如果不需要获取旧的block表,可以将 oldset 设为 nullptr

  • 返回值:若成功则为0,若出错则为-1

五、系统调用接口 — sigpending

sigpending函数用来获取当前进程中的未决信号集pengding表,为了做检查。函数原型如下:

#include <signal.h>
int sigpending(sigset_t *set);

参数说明:

  • 参数:待获取的未决信号集
  • 返回值:成功返回0,失败返回 -1并将错误码设置

如何根据打印 pending

  1. 使用函数sigismember判断当前信号集中是否存在该信号,如果存在,输出1,否则输出0
  2. 如此重复,将 31 个信号全部判断打印输出即可

【代码】

代码逻辑:循环打印pending表,阻塞2号信号。若进程没有收到2号信号,那么位图一定是全0;当进程收到2号信号时,位图的低2位比特位由01

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

int main()
{
    // 在用户层定义sigset_t变量
    sigset_t sset;
    // 初始化信号集,将所有信号的对应比特位清零
    sigemptyset(&sset);
    // 将信号集sset添加特定的信号,设置1
    sigaddset(&sset, 2);
    // sigprocmask函数用来对block表进行操作(阻塞信号)
    sigset_t oldset;
    sigemptyset(&oldset);
    sigprocmask(SIG_SETMASK, &sset, &oldset);

    // 重复打印当前进程的pending表。
    // 虽然我们阻塞了2号信号,但是只有没产生信号,pending表一定是全0
    sigset_t pending_t; // 获取pending表
    while (true)
    {
        // sigpending函数用来获取当前进程中的未决信号集pending表
        int n = sigpending(&pending_t);
        if (n < 0) continue; 
        // 打印
        // 判断当前信号集中是否存在该信号,如果存在,输出1,否则输出0
        for (int i = 31; i >= 1; i--)
        {
            if (sigismember(&pending_t, i))
            {
                cout << "1";
            }
            else
            {
                cout << "0";
            }
        }
        cout << endl << endl;
             
        sleep(1);
    }
    return 0;
}

【程序结果】

在这里插入图片描述

我们再看下面的代码,这段代码在上面的基础上加了解除2号信号阻塞,那么当收到2号信号后,由于解除了阻塞,对应的比特位由1变为0

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

int main()
{
    // 在用户层定义sigset_t变量
    sigset_t sset;
    // 初始化信号集,将所有信号的对应比特位清零
    sigemptyset(&sset);
    // 将信号集sset添加特定的信号,设置1
    sigaddset(&sset, 2);
    // sigprocmask函数用来对block表进行操作(阻塞信号)
    sigset_t oldset;
    sigemptyset(&oldset);
    sigprocmask(SIG_SETMASK, &sset, &oldset);

    // 重复打印当前进程的pending表。
    // 虽然我们阻塞了2号信号,但是只有没产生信号,pending表一定是全0
    sigset_t pending_t; // 获取pending表
    int cnt = 0;
    while (true)
    {
        // sigpending函数用来获取当前进程中的未决信号集pending表
        int n = sigpending(&pending_t);
        if (n < 0) continue; 
        // 打印
        // 判断当前信号集中是否存在该信号,如果存在,输出1,否则输出0
        for (int i = 31; i >= 1; i--)
        {
            if (sigismember(&pending_t, i))
            {
                cout << "1";
            }
            else
            {
                cout << "0";
            }
        }
        cout << endl << endl;
             
        sleep(1);
        // 解除阻塞
        cnt++;
        if (cnt == 6)
        {
        	cout << "已解除2号信号阻塞" << endl;
            sigprocmask(SIG_SETMASK, &oldset, nullptr);
        }
    }
    return 0;
}

【程序结果】

在这里插入图片描述

通过以上结果:2 号信号产生后,当前进程的 pending 表中的 2 号信号位被置为 1,表示该信号属于未决状态,并且在六秒之后,阻塞结束,信号递达,进程终止。

哎?奇怪?当阻塞解除后,信号递达,应该看见 pending 表中对应位置的值由 1 变为 0,但为什么没有看到?

这是因为当阻塞解除后信号递达,而2号信号的默认执行动作为终止进程,进程都终止了,当然看不到。

解决方法:自定义2号信号的处理动作动作(别急着退出进程)

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void handler(int signum)
{
    cout << "解除" << signum << "号信号的阻塞 " << endl;
    // 最终不退出进程 没加exit
}

int main()
{
    // 捕捉2号信号
    signal(2, handler);

    // 在用户层定义sigset_t变量
    sigset_t sset;
    // 初始化信号集,将所有信号的对应比特位清零
    sigemptyset(&sset);
    // 将信号集sset添加特定的信号,设置1
    sigaddset(&sset, 2);
    // sigprocmask函数用来对block表进行操作(阻塞信号)
    sigset_t oldset;
    sigemptyset(&oldset);
    sigprocmask(SIG_SETMASK, &sset, &oldset);

    // 重复打印当前进程的pending表。
    // 虽然我们阻塞了2号信号,但是只有没产生信号,pending表一定是全0
    sigset_t pending_t; // 获取pending表
    int cnt = 0;
    while (true)
    {
        // sigpending函数用来获取当前进程中的未决信号集pending表
        int n = sigpending(&pending_t);
        if (n < 0)
            continue;
        // 打印
        // 判断当前信号集中是否存在该信号,如果存在,输出1,否则输出0
        for (int i = 31; i >= 1; i--)
        {
            if (sigismember(&pending_t, i))
            {
                cout << "1";
            }
            else
            {
                cout << "0";
            }
        }
        cout << endl
             << endl;

        sleep(1);
        // 解除阻塞
        cnt++;
        if (cnt == 4)
        {
            sigprocmask(SIG_SETMASK, &oldset, nullptr);
        }
    }
    return 0;
}

【程序结果】

在这里插入图片描述

那如果将所有的信号全部屏蔽掉,那是不是信号就不会被递达(处理)了?

我们能想到的,操作系统的设计者肯定考虑到了,肯定有一些信号是无法被屏蔽的

9号和19号不可被屏蔽,也不可被捕捉。我们可以来验证

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

int main()
{
	cout << "my pid is " << getpid() << endl;
	sleep(3);
	
    sigset_t sset, oldset;
    sigemptyset(&sset);
    sigemptyset(&oldset);
    for (int i = 1; i <= 31; i++)
    {
        sigaddset(&sset, i);
    }
    // 将所有的信号屏蔽
    sigprocmask(SIG_SETMASK, &sset, &oldset);
    sigset_t pending_t;
    while (true)
    {
        int n = sigpending(&pending_t);
        if (n < 0)
            continue;
        for (int signo = 31; signo >= 1; signo--)
        {
            cout << sigismember(&pending_t, signo);
        }
        cout << endl
             << endl;
        sleep(1);
    }

    return 0;
}

代码如上,大家可以通过kill -num <pid>命令自行去试。

六、总结

在这里插入图片描述

七、相关代码

本篇博客的相关代码:点击跳转

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值