从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_BLOCK | set保存的是我们希望屏蔽的信号,系统会在block表中屏蔽这些信号 |
SIG_UNBLOCK | set保存的是我们希望解除屏蔽的信号,系统会在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表。达到屏蔽信号的作用。