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

Linux进程信号保存(25)

文章目录

  • 前言
  • 一、重识信号
    • 概念
    • 感性理解
    • 在内核中的表示
    • sigset_t 信号集
  • 二、信号集操作函数
    • 增删改查
    • sigprocmask
      • 示例程序一:
      • 示例程序二:
    • sigpending
  • 总结


前言

继续,继续!

信号从产生到执行,并不会被立即处理,这就意味着需要一种 “方式” 记录信号是否产生,对于 31 个普通信号来说,一个 int 整型就足以表示所有普通信号的产生信息了;信号还有可能被 “阻塞”,对于这种多状态、多结果的事物,操作系统会将其进行描述、组织、管理,这一过程称为 信号保存 阶段

你会越来越发现,先描述,再组织,这真是一个经典的概念


一、重识信号

概念

  信号 传递过程:信号产生 -> 信号未决 -> 信号递达

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

  在这三种过程之前,均有可能出现 信号阻塞 的情况

信号阻塞(Block):使信号传递 “停滞”,无论是否产生,都无法进行处理

在这里插入图片描述
信号递达后的三种处理方式:

  • SIG_DFL 默认处理动作,大多数信号最终都是终止进程
  • SIG_IGN 忽略动作,即进程收到信号后,不做任何处理动作
  • handler 用户自定义的信号执行动作

请注意!

  • 信号阻塞 是一种手段,可以发生在 信号处理 前的任意时段
  • 信号阻塞 与 忽略动作 不一样,虽然二者的效果差不多:什么都不干,但前者是 干不了,后者则是 不干了,需要注意区分

感性理解

  我们来打一个比方,有助于你的理解

我最爱打比方了

现在我们把信号传递比作是网上购物

  • 信号产生:在某某购物平台上下达了订单
  • 信号未决:订单下达后,快递的运输过程
  • 信号递达:快递到达驿站后,你对于快递的处理动作
  • 信号阻塞:快递运输过程中堵车了

  只要你下单了,你的手机上肯定会有 物流信息(未决信息已记录),当 快递送达后(信号递达),物流记录 不再更新

  而 堵车 是一件不可预料的事情,也就是说:在下单后,快递可能一会儿送达(没有阻塞),可能五天送达(阻塞 -> 解除阻塞),有可能永不送达,因为快递可能永远堵车(阻塞)

  堵车也有可能在你下单前发生(信号产生前阻塞)

在内核中的表示

  对于传递中的信号来说,需要存在三种状态表达:

  1. 信号是否阻塞
  2. 信号是否未决
  3. 信号递达时的执行动作

  在内核中,每个进程都需要维护这三张与信号状态有关的表:block 表、pending 表、handler 表

在这里插入图片描述
所谓的 block 表 和 pending 表 其实就是 位图结构

一个 整型 int 就可以表示 31 个普通信号(实时信号这里不讨论)

  • 比如 1 号信号就是位图中的 0 位置处,0 表示 未被阻塞/未产生未决,1 则表示 阻塞/未决
  • 对于信号的状态修改,其实就是修改 位图 中对应位置的值(0/1)
  • 对于多次产生的信号,只会记录一次信息(实时信号则会将冗余的信号通过队列组织)

如何记录信号已产生 -> 未决表中对应比特位置置为 1 ?
  假设已经获取到了信号的 pending 表
  只需要进行位运算即可:pending |= (1 << (signo - 1))
  其中的 signo 表示信号编号,-1 是因为信号编号从 1 开始,需要进行偏移
  如果想要取消 未决 状态也很简单:pending &= (~(1 << (signo - 1)))
  至于 阻塞 block 表,与 pending 表 一模一样

我们再来看上图,三个信号 SIGHUP、SIGINT、SIGQUIT 的解读如下:

  1. SIGHUP 信号未被阻塞,未产生,一旦产生了该信号,pending 表对应的位置置为 1,当信号递达后,执行动作为默认
  2. SIGINT 信号被阻塞,已产生,pending 表中有记录,此时信号处于阻塞状态,无法递达,一旦解除阻塞状态,信号递达后,执行动作为忽略该信号
  3. SIGQUIT 信号被阻塞,未产生,即使产生了,也无法递达,除非解除阻塞状态,执行动作为自定义

阻塞 block 与 未决 pending 之间并没很强的关联性,阻塞不过是信号未决的延缓剂

信号在 产生 之前,可以将其 阻塞,信号在 产生 之后(未决),依然可以将其 阻塞

在这里插入图片描述
  至于 handler 表是一个 函数指针表,格式为:返回值为空,参数为 int 的函数

  可以看看 默认动作 SIG_DEL 和 忽略动作 SIG_IGN 的定义

/* Type of a signal handler.  */
typedef void (*__sighandler_t) (int);/* Fake signal functions.  */
#define SIG_ERR	((__sighandler_t) -1)		/* Error return.  */
#define SIG_DFL	((__sighandler_t) 0)		/* Default action.  */
#define SIG_IGN	((__sighandler_t) 1)		/* Ignore signal.  */

  默认动作就是将 0 强转为函数指针类型,忽略动作则是将 1 强转为函数指针类型,分别对应 handler 函数指针数组表中的 0、1 下标位置;除此之外,还有一个 错误 SIG_ERR 表示执行动作为 出错

简单讲述下这三个表,就是:

  1. block 表(位图结构)比特位的位置,表示哪一个信号;比特位的内容代表 是否 对应信号被阻塞
  2. pending 表(位图结构)比特位的位置,表示哪一个信号;比特位的内容代表 是否 收到该信号
  3. handler 表(函数指针数组)该数组的下标,表示信号编号;数组的特定下标的内容,表示该信号递达后的执行动作

sigset_t 信号集

  无论是 block 表 还是 pending 表,都是一个位图结构,依靠 除、余 完成操作,为了确保不同平台中位图操作的兼容性,将信号操作所需要的 位图 结构封装成了一个结构体类型,其中是一个 无符号长整型数组

/* A `sigset_t' has a bit for each signal.  */# define _SIGSET_NWORDS	(1024 / (8 * sizeof (unsigned long int)))
typedef struct{unsigned long int __val[_SIGSET_NWORDS];} __sigset_t;#endif

  注:_SIGSET_NWORDS 大小为 32,所以这是一个可以包含 32 个 无符号长整型 的数组,而每个 无符号长整型 大小为 4 字节,即 32 比特,至多可以使用 1024 个比特位

  sigset_t 是信号集,其中既可以表示 block 表信息,也可以表示 pending 表信息,可以通过信号集操作函数进行获取对应的信号集信息;信号集 的主要功能是表示每个信号的 “有效” 或 “无效” 状态

block 表 通过信号集称为 阻塞信号集或信号屏蔽字(屏蔽表示阻塞),pending 表 通过信号集中称为 未决信号集

假设现在要获取第 127 个比特位

  • 首先定位数组下标(对哪个数组操作):127 / (8 * sizeof (unsigned long int)) = 3
  • 求余获取比特位(对哪个比特位操作):127 % (8 * sizeof (unsigned long int)) = 31
  • 对比特位进行操作即可
  • 假设待操作对象为 XXX
  • 置 1:XXX._val[3] |= (1 << 31)
  • 置 0:XXX._val[3] &= (~(1 << 31))

其实很好理解,对吧!

所以可以仅凭 sigset_t 信号集,对 1024 个比特位进行任意操作

二、信号集操作函数

  对于 信号 的 产生或阻塞 其实就是对 block 和 pending 两张表的 增删改查

增删改查

  对于位图的增删改查是这样操作的:

  • 增:| 操作,将比特位置为 1
  • 删:& 操作,将比特位置为 0
  • 改:| 或 & 操作,灵活变动
  • 查:判断指定比特位是否为 1 即可

  比特作为基本单位,不推荐让我们直接进行操作,操作系统也不同意,于是提供了一批 系统接口,用于对 信号集 进行操作

#include <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);	//查  

  这些函数都是 成功返回 0,失败返回 -1

  至于参数,非常简单,无非就是 待操作的信号集变量、待操作的比特位

  注意: 在创建 信号集 sigset_t 类型后,需要使用 sigemptyset 或 sigfillset 函数进行初始化,确保 信号集 是合法可用的

sigprocmask

  sigprocmask 函数可用用来对 block 表 进行操作

我们一开始只是在用户栈上对 block 进行操作,要用这个函数写进 内核 里面进去

在这里插入图片描述

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

返回值:成功返回 0,失败返回 -1 并将错误码设置

参数1:对 屏蔽信号集 的操作

  • SIG_BLOCK 希望添加至当前进程 block 表 中阻塞信号,从 set 信号集中获取,相当于 mask |= set
  • SIG_UNBLOCK 解除阻塞状态,也是从 set 信号集中获取,相当于 mask &= (~set)
  • SIG_SETMASK 设置当前进程的 block 表为 set 信号集中的 block 表,相当于 mask = set

参数2:就是一个信号集,主要从此信号集中获取屏蔽信号信息
参数3:也是一个信号集,保存进程中原来的 block 表(相当于给你操作后,反悔的机会)

  这个函数就是 参数 1 比较有讲究,主打的就是一个 从 set 信号集 中获取阻塞信号相关信息,然后对进程中的 block 表进行操作,并且有三种不同的操作方式

来几个小程序验证一下:

示例程序一:

将 2 号信号阻塞,尝试通过 键盘键入 发出 2 信号

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;int main()
{//创建信号集sigset_t set, oset;//初始化信号集sigemptyset(&set);sigemptyset(&oset);//阻塞2号信号sigaddset(&set, 2);	//2 号信号被记录//设置当前进程的 block 表sigprocmask(SIG_BLOCK, &set, &oset);//死循环while(true){cout << "我是一个进程,我正在运行" << endl;sleep(1);}return 0;
}

在这里插入图片描述
显然,当 2 号信号被阻塞后,是 无法被递达 的,进程也就无法终止了

示例程序二:

在程序运行五秒后,解除阻塞状态

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;int main()
{// 创建信号集sigset_t set, oset;// 初始化信号集sigemptyset(&set);sigemptyset(&oset);// 阻塞2号信号sigaddset(&set, 2);	//2号信号被记录// 设置当前进程的 屏蔽信号集sigprocmask(SIG_BLOCK, &set, &oset);// 死循环int n = 0;while (true){if (n == 5){// 采用 SIG_SETMASK 的方式,覆盖进程的 block 表sigprocmask(SIG_SETMASK, &oset, nullptr); // 不接收进程的 block 表}cout << "我是一个进程,我正在运行" << endl;n++;sleep(1);}return 0;
}

在这里插入图片描述
  在 2 号信号发出、程序运行五秒解除阻塞后,信号才被递达,进程被终止

如何证明信号已经抵达呢?

  • 当 n == 5 时,解除阻塞状态,程序立马结束
  • 并只打印了 五条 语句,证明在第六秒时,程序就被终止了
  • 至于如何进一步证明,需要借助 未决信号表

sigpending

  这个函数很简单,获取当前进程中的 未决信号集

在这里插入图片描述

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

返回值:成功返回 0,失败返回 -1 并将错误码设置

参数:待获取的 未决信号集

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

#include <iostream>
#include <cassert>
#include <unistd.h>
#include <signal.h>
using namespace std;static void DisplayPending(const sigset_t& pending)
{//打印 pending 表cout << "当前进程的 pending 表为: ";int i = 1;while(i < 32){if(sigismember(&pending, i))cout << "1";elsecout << "0";i++;}cout << endl;
}int main()
{// 创建信号集sigset_t set, oset;// 初始化信号集sigemptyset(&set);sigemptyset(&oset);// 阻塞2号信号sigaddset(&set, 2);	//记录 2 号信号// 设置当前进程的 屏蔽信号集sigprocmask(SIG_BLOCK, &set, &oset);// 死循环int n = 0;while (true){if (n == 5){// 采用 SIG_SETMASK 的方式,覆盖进程的 block 表sigprocmask(SIG_SETMASK, &oset, nullptr);   // 不接收进程的 block 表}									//获取进程的 未决信号集sigset_t pending;sigemptyset(&pending);int ret = sigpending(&pending);assert(ret == 0);(void)ret;    //欺骗编译器,避免 release 模式中出错DisplayPending(pending);n++;sleep(1);}return 0;
}

在这里插入图片描述
  结果:当 2 号信号发出后,当前进程的 pending 表中的 2 号信号位被置为 1,表示该信号属于 未决 状态,并且在五秒之后,阻塞结束,信号递达,进程终止

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

很简单,因为当前 2 号信号的执行动作为终止进程,进程都终止了,当然看不到

  那我们现在改一下程序,给 2 号信号先注册一个新的自定义动作(而不是急着退出进程)

#include <iostream>
#include <cassert>
#include <unistd.h>
#include <signal.h>
using namespace std;static void handler(int signo)
{cout << signo << " 号信号确实递达了" << endl;//最终不退出进程
}static void DisplayPending(const sigset_t pending)
{// 打印 pending 表cout << "当前进程的 pending 表为: ";int i = 1;while (i < 32){if (sigismember(&pending, i))cout << "1";elsecout << "0";i++;}cout << endl;
}int main()
{// 更改 2 号信号的执行动作signal(2, handler);// 创建信号集sigset_t set, oset;// 初始化信号集sigemptyset(&set);sigemptyset(&oset);// 阻塞2号信号sigaddset(&set, 2);	//记录 2 号信号// 设置当前进程的 屏蔽信号集sigprocmask(SIG_BLOCK, &set, &oset);// 死循环int n = 0;while (true){if (n == 5){// 采用 SIG_SETMASK 的方式,覆盖进程的 block 表sigprocmask(SIG_SETMASK, &oset, nullptr); // 不接收进程的 block 表}// 获取进程的 未决信号集sigset_t pending;sigemptyset(&pending);int ret = sigpending(&pending);assert(ret == 0);(void)ret; // 欺骗编译器,避免 release 模式中出错DisplayPending(pending);n++;sleep(1);}return 0;
}

在这里插入图片描述

显然,这就是我们想要的最终结果

  先将信号 阻塞,信号发出后,无法 递达,始终属于 未决 状态,当阻塞解除后,信号可以 递达,信号处理之后, 未决表 中不再保存信号相关信息,因为已经处理了

综上,信号在发出后,在处理前,都是保存在 未决表 中的

  • 针对信号的 增删改查 都需要通过 系统调用 来完成,不能擅自使用位运算
  • sigprocmask、sigpending 这两个函数的参数都是 信号集,前者是 屏蔽信号集,后者是 未决信号集
  • 在对 信号集 进行增删改查前,一定要先初始化
  • 信号在被解除 阻塞状态 后,很快就会 递达 了
  • 关于信号何时递达、以及递达后的处理动作,在下一篇文章中揭晓

总结

  那递达信号之后又会发生什么呢?其原理又如何?
  别急,我们下篇见分晓!

http://www.xdnf.cn/news/5872.html

相关文章:

  • 大数据——解决Matplotlib 字体不足问题(Linux\mac\windows)
  • 基于javaweb的SpringBoot酒店管理系统设计与实现(源码+文档+部署讲解)
  • 《数据库原理》部分习题解析
  • quickbi单个空间限制1000数据集引发的企业在使用过程中的思考和反思及建议。
  • 用AI制作黑神话悟空质感教程,3D西游记裸眼效果,西游人物跳出书本
  • 【Linux】进程通信 管道
  • MySQL三种模糊查询方式:​​LIKE、REGEXP和FULLTEXT
  • 携固态电池、新形态钢壳叠片电池等产品 豪鹏科技将亮相CIBF 2025
  • GPT( Generative Pre-trained Transformer )模型:基于Transformer
  • 银行营销风控环节如何实现数字化升级?
  • 南方科技大学Science! 自由基不对称催化新突破 | 乐研试剂
  • 人事管理系统8
  • Redis 主从复制的实现原理是什么?
  • 【Qt】pro工程文件转CMakeLists文件
  • 自动化测试基础知识详解
  • 无人机避障——如何利用MinumSnap进行对速度、加速度进行优化的轨迹生成(附C++python代码)
  • 如何通过 Windows 图形界面找到 WSL 主目录
  • 【Ansys Icepak】带翅片的散热器
  • C++23 views::zip 和 views::zip_transform (P2321R2) 深入解析
  • 嵌入式开发中 C++ 跨平台开发经验与解决方案
  • DAY 24 元组和OS模块
  • 思极地图使用
  • 《算法导论(第4版)》阅读笔记:p39-p48
  • 基于STM32、HAL库的ADAU1701JSTZ音频接口芯片驱动程序设计
  • 【23种设计模式】模式背后运用的技术对照
  • 【Android】下拉刷新组件Swiperefreshlayout
  • 将 swagger 接口导入 apifox 查看及调试
  • android 权限配置
  • ThingsBoard(TODO)
  • 无人机失联保护模块技术解析!