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

从0开始Linux(34)——进程信号(3)信号保存

欢迎来到博主的专栏:从0开始Linux
博主ID:代码小豪

文章目录

    • 信号保存
      • 信号的补充概念
      • 从内核解析进程信号
      • 修改block表

在前面的章节中我们提到,一个进程,在收到信号后会有不同的行为:大致可分为(1)默认行为(2)忽略(3)自定义行为。

  • 默认行为:如果我们没有使用signal函数修改进程对一个信号的行为,那么它采用的就是默认行为。如果想要将进程对于某信号的重新改为默认,则使用系统调用signal(signo,SIG_DEF)
  • 忽略:忽略也是进程对于信号做出的行为,比如bash进程就会忽略SIGINT的操作,如果想要让进程忽略某个信号,则使用signal(signo,SIG_IGN)
  • 自定义行为:如果我们想让进程对signo的信号做出的行为为handle函数,则使用signal(signo,handle)。

在前面的文章中,我们了解到,进程的信号本质上不是发送给进程的,而是发送给操作系统的,操作系统收到信号后,会将对应的信号写进进程的PCB有关映射信号的位图当中。因此与其说是向进程发送信号,更像是系统向进程PCB写入信号。

那么我么来思考第一件事,那就是进程一旦被写入了信号,操作系统就会让进程执行对应行为吗?当然不是了。因为操作系统需要做的工作太多,比如管理进程的调度,管理文件,管理内存等,所以并不是立马就能让进程执行的。由于写入进程信号,到进程执行行为之间存在时间差,所以要将进程信号保存下来,这个操作就是信号保存。

信号保存

信号的补充概念

  • 进程执行信号对应行为时,叫做信号递达
  • 进程在被OS写入信号,到执行信号对应行为的过程,叫做信号未决(pending)
  • 进程可以选择屏蔽(block)某个信号
  • 如果进程屏蔽了某个信号,在该信号发生后,会一直处于未决(pending)状态,直到解除对该信号的屏蔽,进程才会信号递达
  • 屏蔽信号和忽略信号是不一样的,忽略信号是进程处理信号的一种行为。

从内核解析进程信号

那么进程信号的原理是什么呢?我们从内核当中进行分析。在进程PCB中,存在三个与信号相关的数据结构,我们称其为:block表,pending表,handler表。其中,block表与信号屏蔽有关,pending表与信号未决有关,handle表与信号递达有关。(博主并不喜欢信号递达这个名词,更喜欢称之为信号行为,纯属个人观点)。

在这里插入图片描述
其中block表和pending表,我们看做是一个位图(实际上和位图作用类似,但是结构上不同,因此将位图作为逻辑结构来讲述。)在block表中,信号在位图中的位置,就是信号值。而位上的数据,表示对该信号是否进行屏蔽,0表示否,1表示真。比如如果我们将SIG_INT进行屏蔽,那么SIG_INT的信号值为2,那么在block表中的第二个位置被写入1。

而pending表则表示有哪些信号处于pending状态,在pending表中,表中的位置表示信号值,而表中的数据表示该信号是否处于pending状态,0表示否,1表示真。比如SIG_INT处于pending状态,那么在pending表的第二个位置被写入1。

而handler表是一个函数指针数组,其中数组下标表示信号值,而保存的内容,是进程对于该信号值对应的行为。比如当进程收到SIG_INT信号时,就会执行handler表中的第二个行为,即SIG_DEF。

在这里插入图片描述
那么一个完成的过程是怎样的呢?首先是OS收到对该进程信号,比如SIG_INT,那么OS就会找到该进程的PCB中的pending表,将对应位置的信号状态设为1。那么接下来就是要让进程执行对应handle了。首先OS会对比block表中的信号是否被屏蔽,如果没有被屏蔽,就执行对应的行为,如果被屏蔽,就一直让信号处于pending状态。直到屏蔽被取消。

那么我们前面以及后面要学到的与进程信号先关的系统调用,本质上都是对这三个表的修改。比如kill函数,是对pending表的数据进行写入,比如signal()函数,是对handler表中的行为,进行修改。那么如果我们要修改block表,又该怎么做呢?

修改block表

首先,block表的系统调用,需要我们传入sigset_t类型的参数,那么这个sigset_t是何方神圣呢?听我娓娓道来。

这个sigset_t是glibc内置的数据类型,定义在头文件<signal.h>中,其定义如下:

typedef struct
{
  unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;

typedef __sigset_t sigset_t;

pending表和block表可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

有趣的是,在linux内核代码中,block表和pending表的数据类型都是sigset_t。
在这里插入图片描述

虽然pending表的类型是struct sigpending类型,但其实这个struct sigpending本质是对sigset_t进行的一个封装。
在这里插入图片描述
我们可以在程序当中直接定义一个sigset_t类型的变量,但是这个结构体毕竟不是我们自己写的。所以修改sigset_t类型的变量,我们要依靠标准库提供的函数。

int setemptyset(sigset_t* set);

int sigfillset(sigset_t* set);

int sigaddset(sigset_t* set,int signal);

int sigdelset(sigset_t* set,int signal);

int sigisnumber(const sigset_t* set,int signal);
  • sigemptyset()可以将传入函数的信号集set初始化成全0
  • sigfillset()将传入函数的信号集set全置为有效(可以理解成为1)
  • sigaddset() 将传入函数的信号集set当中的信号设为有效
  • sigdelset()将传入函数的信号集set的被启用的信号,设为无效
  • sigismember(),检测传入函数的信号集set的signal信号是否被启用。

前面提到了,修改pending表的系统调用可以使用kill,而修改handler表的系统调用是signal。那么修改block表应该使用哪些系统调用呢?

修改block表的系统调用是sigprocmask()。其函数原型如下:

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

它的参数有点复杂,我们一个一个讲起。

首先是how,how表示我们修改block表的方式,分为SIG_BLOCK,SIG_UNBLOCK,SIG_SETMASK。这三个选项对应的行为都不同。

第二个参数为set,这个set是一个sigset_t类型的指针,也就是我们前面所说的信号集。该参数是一个输入型参数,即我们将我们自定义出的信号集,以指针的方式传递给sigprocmask函数。该系统调用会用我们自定义的set,修改内核中的block表(block表也是sigset_t类型)。

第三个参数为oldset。也是一个信号集,但是它是一个输出型参数,将oldset传入该函数中,可以或得被修改前的block表的数据。

选项行为
SIG_BLOCKset保存的是我们希望屏蔽的信号,系统会在block表中屏蔽这些信号
SIG_UNBLOCKset保存的是我们希望解除屏蔽的信号,系统会在block表中解除屏蔽这些信号
SIG_SETMASK系统会将set的数据,替换掉block表中的数据。可以视为block=set

那么现在我们就来写如下的代码试试这些函数调用。

首先,我们要定义出自己的信号集。

sigset_t myblock,oldblock;//定义一个输入信号集的myblock,和一个输出信号集的oldblock
//初始化信号集
sigemptyset(&myblock);
sigemptyset(&oldblock);
//将信号集中的SIGINT 屏蔽
sigaddset(&myblock,SIGINT);

但是如果我们想要真的屏蔽掉SIGINT信号,光定义出自己的信号集是没用的,因为OS当中的block表并没有被修改。因此我们通过sigprocmask函数,将我们自定义的set传给操作系统,修改block表。

sigprocmask(SIG_BLOCK,&myblock,&oldblock);
//这个oldblock是输出型参数,其内容是被修改前的内核block表中的数据。
//目的是为了方便我们回溯之前的block表

通过sigprocmask函数调用,可以修改内核的block表。达到屏蔽信号的作用。


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

相关文章:

  • 面试手撕题积累
  • C++算法练习-day45——236.二叉树的最近公共祖先
  • hive的cascade使用解释
  • uniapp的renderjs使用
  • GitLab指定用户分配合并权限
  • Spring Boot 整合 ELK 全面指南:实现日志采集、分析与可视化
  • 泡泡玛特出海,如何走出舒适区
  • webrtc ios h264 硬编解码
  • Vue3+Typescript+Axios+.NetCore实现导出Excel文件功能
  • 并行区块链全解:执行原理、代表项目及技术发展周期
  • 深度学习:自然语言处理
  • JS-06-事件监听
  • MongoDB的SQL注入测试方法
  • ubuntu上安装redis
  • 【C++】7000字介绍map容器和set容器的功能和使用
  • Virtio on Linux
  • css:项目
  • PHP 常量
  • 云计算之kubernetes面试题
  • 基于spring boot开发的理财管理系统设计
  • 算法训练营day16(二叉树03:最大深度,最小深度,完全二叉树节点数量)
  • 湖北移动,以5G-A规模商用“换”出内需新活力
  • SSH远程命令实践:如何打包、压缩并传输服务器文件
  • shell-函数调用进阶即重定向
  • 租辆酷车小程序开发(二)—— 接入微服务GRPC
  • PHP获取安卓APK文件的信息(名称、版本、图标文件等)