【Linux】信号2——信号的保存

掘根 2024-08-05 11:37:05 阅读 64

 1.信号的保存

为什么信号要被保存

因为信号不会立即处理,所以要保存起来。

我们看到的普通信号编号是1到31的

对于普通信号而言,对普通进程而言,自己有还是没有,收到哪个信号

给进程发信号,实际上是进程的task_struct发信号

实际上进程的task_struct里面有一个整数,操作系统把它当成二进制序列来用,一共32位,每一位代表1个信号,一共31个普通信号,只用它的31位比特位来一一对应即可

信号是如何记录的?

        实际上,当一个进程接收到某种信号后,该信号是被记录在该进程的进程控制块 task_struct 当中的。

        你买了一个快递,于我而言,我当然知道寄来的是什么,而快递员是男是女,多大年纪这并不重要,重要的是快递是否到了,里面的东西是否完整无损。所以对进程来说,最重要的无非就是 “是否有信号” + “是谁”。

        操作系统提供了 31 个普通信号,所以我们采用位图来保存信号,也就是说在 task_struct 结构中只要写上一个 unsigned int signals; (00000000 … 00000000) 这样一个字段即可比特位的位置代表是哪一个信号,比特位的内容用 0 1 来代表是否。

其中比特位的位置代表信号的编号,而比特位的内容就代表是否收到对应信号,比如第6个比特位是1就表明收到了6号信号。

信号是如何发送的?

        发送信号的本质就是写对应进程 task_struct 信号位图。因为 OS 是系统资源的管理者,所以把数据写到 task_struct 中只有 OS 有资格、有义务。所以,信号是操作系统发送的,通过修改对应进程的信号位图(0 -> 1)完成信号的发送,再朴素点说就是信号不是 OS 发送的,而是写的。

为什么是操作系统来发送信号? 

操作系统是进程的管理者,只有它有资格去修改进程的task_struct 

操作系统为什么不直接执行信号的内容,而让进程去干?

操作系统还要考虑别的进程,可能别的进程更重要,但是操作系统不知道,贸然让操作系统来干,可能会对其他进程产生影响

信号是如何产生的?

 接下来再看信号的产生(kill,键盘),不管信号是如何产生的,最后都一定要经过 OS,再到进程。kill 当然是命令,是在 bash 上的,也就是在系统调用之上,所以 kill 的底层一定使用了操作系统某种接口来完成像目标进程写信号的过程。键盘是一种硬件,它所产生的各种组合键会产生各种不同的数据,OS 作为硬件的管理者,键盘上所获得的各种数据,一定是先被 OS 拿到。所以,虽然信号的产生五花八门,但归根结底所有信号的产生后都是间接或直接由 OS 拿到后向目标进程发信号。

        一个进程收到信号,本质就是该进程内的信号位图被修改了,也就是该进程的数据被修改了,而只有操作系统才有资格修改进程的数据,因为操作系统是进程的管理者。也就是说,信号的产生本质上就是操作系统直接去修改目标进程的task_struct中的信号位图。

注意: 信号只能由操作系统发送,但信号发送的方式有多种。

信号是如何被处理的

有3种方法

执行该信号的默认处理动作。自定义信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。忽略该信号。

1.1. 信号其他相关常见概念

在开始内容之前,先介绍一些信号的专业名词:

实际执行信号的处理动作称为信号递达(Delivery)信号从产生到递达之间的状态,称为信号未决(Pending)(就是收到信号,但没有执行信号对应的动作)进程可以选择阻塞(Block)某个信号,阻塞的信号就是收到信号,但是一直处于未决状态。忽略信号也是一种递达动作。未决就是未决,阻塞就是阻塞。没有收到信号时,依然可以对没有收到的信号阻塞(收到信号后直接就是未决信号)

1.2.信号在内核的表示

        Linux内核的进程控制块PCB是一个结构体task_struct,除了包含进程id、状态、工作目录、用户id、组id、文件描述符表、还包含了信号相关的信息,主要指阻塞信号集(下面的block)和未决信号集(下面的pending)。

信号在内核中的表示示意图如下:

        每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。

实际执行信号的处理动作称为信号递达(Delivery)信号从产生到递达之间的状态,称为信号未决(Pending)(就是收到信号,但没有执行信号对应的动作)

阻塞信号集(上面的block):

        阻塞信号集,就是对信号进行阻塞或屏蔽设置的一个32位信号屏蔽字,同样每一位对应一个信号,如果某一位设置为1,那么该位对应的信号将被屏蔽,该信号会被延后处理,此时如果信号产生,那么未决信号集中对应的位置1,一直到该信号被解除屏蔽的时候(也就是阻塞信号集中对应位置0),才会去处理该信号,处理完信号,未决信号集中对应位反转回0。

        也叫信号屏蔽字,将某些信号加入集合,对他们设置屏蔽,当屏蔽某个信号后,如果再收到该信号,该信号的处理将推后至解除屏蔽后。(也就是说,信号一旦被阻塞,就不能递达)

未决信号集(上面的pending):

        未决信号集就是当前进程未处理的信号的集合,未决信号集实际上是一个32位数,该字的每一个位对应一个信号,如果该位为1表示信号还未被处理,如果改为置为0,表示信号已经被处理或者没有传递该信号。

信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态;当信号被处理对应位翻转回为0,这一时刻往往非常短暂。信号产生后由于某些原因主要是阻塞不能抵达,这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。

而阻塞信号集会影响到未决信号集

        比如说我在阻塞信号集中将2号信号为置为1,也就是将2号信号屏蔽,那么未决信号集中2号信号对应的位就会变为1(未决状态),一直阻塞在这种状态。(也就是说,信号一旦被阻塞,就不能递达,一直处于未决状态)

 

        内核通过读取未决信号集来判断信号是否应被处理,信号屏蔽字mask可以影响未决信号集,而我们可以在应用程序中自定义set来改变mask来达到屏蔽指定信号的目的。

回到这个图

在上图中,

 SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会在改变处理动作之后再接触阻塞。SIGQUIT信号未产生过,但一旦产生SIGQUIT信号,该信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前,这种信号产生过多次,POSIX.1允许系统递达该信号一次或多次。Linux是这样实现的:普通信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里,但是我们这里只讨论普通信号,所以只计1次。

我们下来让大家见见handler里面的SIG_DFL和SIG_IGN

先来SIG_DFL

<code>#include<iostream>

#include<signal.h>

#include<unistd.h>

using namespace std;

int main()

{

signal(2, SIG_DFL); //实施2号信号的默认处理动作

while (1){

cout<<"I am running…… , pid:"<<getpid()<<endl;

sleep(1);

}

return 0;

}

好像跟它原来的差不多

我们再看看

<code>#include<iostream>

#include<signal.h>

#include<unistd.h>

using namespace std;

int main()

{

signal(2, SIG_IGN); //忽略2号信号

while (1){

cout<<"I am running…… , pid:"<<getpid()<<endl;

sleep(1);

}

return 0;

}

我们看到进程不理我的2号信号了

再看看第3种

<code>#include<iostream>

#include<signal.h>

#include<unistd.h>

using namespace std;

void sighandler(int signo)

{

cout<<"get a signo :"<<signo<<endl;

exit(1);

}

int main()

{

signal(2, sighandler); //将2号信号的默认处理动作修改为sighandler方法

while (1){

cout<<"I am running…… , pid:"<<getpid()<<endl;

sleep(1);

}

return 0;

}

总结一下:

在block位图中,比特位的位置代表某一个信号,比特位的内容代表该信号是否被阻塞。在pending位图中,比特位的位置代表某一个信号,比特位的内容代表该信号是否被处理。handler表本质上是一个函数指针数组,数组的下标代表某一个信号,数组的内容代表该信号递达时的处理动作,处理动作包括默认、忽略以及自定义。block、pending和handler这三张表的每一个位置是一一对应的。

我们对于信号最核心的理解就在上面了

1.3.信号集类型——sigset_t

        根据信号在内核中的表示方法,每个信号的未决标志只有一个比特位,非0即1,如果不记录该信号产生了多少次,那么阻塞标志也只有一个比特位。

因此,未决和阻塞标志可以用操作系统的数据类型sigset_t来存储。

        在我当前的云服务器中,sigset_t类型的定义如下:(不同操作系统实现sigset_t的方案可能不同)

<code>#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))

typedef struct

{

unsigned long int __val[_SIGSET_NWORDS];

} __sigset_t;

typedef __sigset_t sigset_t;

sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态。

在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞。在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

1.4.信号集操作函数

        sigset_t类型对于每种信号用一个bit表示“有效”或“无效”,至于这个类型内部如何存储这些bit则依赖于系统的实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

#include <signal.h>

typedef unsigned long sigset_t; /*信号集类型,其实就是一个32位的字*/

/*清空信号集,将某个信号集全部清0*/

int sigemptyset(sigset_t *set);

/*填充信号集,将某个信号集全部置1*/

int sigfillset(sigset_t *set);

/*将某个信号signum加入信号集set*/

int sigaddset(sigset_t *set, int signum);

/*将某个信号清出信号集,从信号集ste中删除信号signum,(其实就是本来某个屏蔽信号字中置1的位清0)*/

int sigdelset(sigset_t *set, int signum);

/*判断某个信号是否在信号集中,

返回值 在集合:1;不在:0;出错:-1 (其余四个函数成功返回0,失败返回-1)*/

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

函数解释:

sigemptyset函数:初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。sigfillset函数:初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。sigaddset函数:在set所指向的信号集中添加某种有效信号。sigdelset函数:在set所指向的信号集中删除某种有效信号。sigemptyset、sigfillset、sigaddset和sigdelset函数都是成功返回0,出错返回-1。sigismember函数:判断在set所指向的信号集中是否包含某种信号,若包含则返回1,不包含则返回0,调用失败返回-1。

注意: 在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号处于确定的状态。

例如,我们可以按照如下方式使用这些函数。

#include <stdio.h>

#include <signal.h>

int main()

{

sigset_t s; //用户空间定义的变量

sigemptyset(&s);

sigfillset(&s);

sigaddset(&s, SIGINT);

sigdelset(&s, SIGINT);

sigismember(&s, SIGINT);

return 0;

}

注意: 代码中定义的sigset_t类型的变量s,与我们平常定义的变量一样都是在用户空间定义的变量,所以后面我们用信号集操作函数对变量s的操作实际上只是对用户空间的变量s做了修改,并不会影响进程的任何行为。

        因此,我们还需要通过系统调用,才能将变量s的数据设置进操作系统。

阻塞信号集——sigprocmask

        sigprocmask函数可以用于读取或更改进程的信号屏蔽字(阻塞信号集,也就是block),该函数的函数原型如下:

设置阻塞或解除阻塞信号集,用来屏蔽信号或解除屏蔽,其本质是读取或修改进程的PCB中的信号屏蔽字。

需要注意的是,屏蔽信号只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢弃处理。

函数参数

how:

假设当前的信号屏蔽字(阻塞信号集,也就是block)为mask

SIG_BLOCK: 设置阻塞,set表示需要屏蔽的信号,相当于 mask = mask | set 。SIG_UNBLOCK: 解除阻塞,set表示需要解除屏蔽的信号,相当于 mask = mask & ~set 。SIG_SETMASK:替换信号集,set表示用于替代原始屏蔽集的新屏蔽集,相当于 mask = set,直接把传入的set设置为当前阻塞信号集。调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

set:传入参数,是一个位图(32位),set中哪位置1,就表示当前进程屏蔽哪个信号。

oldset:传出参数,保存旧的信号屏蔽集(阻塞信号集,也就是block),可用于恢复上次设置。

参数说明:

如果oldset是非空指针,则读取进程当前的信号屏蔽字(阻塞信号集,也就是block)通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字(阻塞信号集,也就是block),参数how指示如何更改。如果oldset和set都是非空指针,则先将原来的信号屏蔽字(阻塞信号集,也就是block)备份到oset里,然后根据set和how参数更改信号屏蔽字。

假设当前的信号屏蔽字(阻塞信号集,也就是block)为mask,下表说明了how参数的可选值及其含义:

返回值说明:

sigprocmask函数调用成功返回0,出错返回-1。

注意: 如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask函数返回前,至少将其中一个信号递达。

未决信号集——sigpending

sigpending函数可以用于读取进程的未决信号集,该函数的函数原型如下:

sigpending函数读取当前进程的未决信号集,并通过set参数传出,set是个传出参数。和上面的oldest参数是一样的

该函数调用成功返回0,出错返回-1。

1.5.使用例子

好了,接口讲到这里,我们接下来来使用一下

老规矩,先复习makefile

实验步骤如下:

先用上述的函数将2号信号进行屏蔽(阻塞)。使用kill命令或组合按键(ctrl+c)向进程发送2号信号。此时2号信号会一直被阻塞,并一直处于pending(未决)状态。使用sigpending函数获取当前进程的pending信号集进行验证。

<code>#include<iostream>

#include<signal.h>

#include<unistd.h>

using namespace std;

void printPending(sigset_t *pending)

{

int i = 1;

for (i = 1; i <= 31; i++){

if (sigismember(pending, i)){

printf("1 ");

}

else{

printf("0 ");

}

}

printf("\n");

}

int main()

{

//1.对2号信号进行屏蔽

//1.1.准备好我们要传进去的block信号集

sigset_t blockset;//创建一个要传入的block信号集

sigemptyset(&blockset);//清空要传入的block信号集

sigaddset(&blockset,2);//将要传入的block信号集的2号信号位置于1

//1.2.将准备好的block信号集通过系统调用接口传进进程

sigset_t oldblockset;//创建1个block信号集来存储当前进程旧的block信号集

sigemptyset(&oldblockset);//清空block信号集,准备存储当前进程旧的block信号集

sigprocmask(SIG_SETMASK,&blockset,&oldblockset);//更换block信号集合

//至此我们已经把2号信号屏蔽了

//2.重复打印当前进程的未决信号集

sigset_t pending;

while(1)

{

//2.1.获取

int n=sigpending(&pending);

if(n<0) continue;

//2.2.打印pending

printPending(&pending);

sleep(1);

}

}

        可以看到,程序刚刚运行时,因为没有收到任何信号,所以此时该进程的pending表一直是全0,而当我们使用kill命令向该进程发送2号信号后,由于2号信号是阻塞的,因此2号信号一直处于未决状态,所以我们看到pending表中的第二个数字一直是1。

        为了看到2号信号递达后pending表的变化,我们可以设置一段时间后,自动解除2号信号的阻塞状态,解除2号信号的阻塞状态后2号信号就会立即被递达。因为2号信号的默认处理动作是终止进程,所以为了看到2号信号递达后的pending表,我们可以将2号信号进行捕捉,让2号信号递达时执行我们所给的自定义动作。

<code>#include<iostream>

#include<signal.h>

#include<unistd.h>

using namespace std;

void printPending(sigset_t *pending)

{

int i = 1;

for (i = 1; i <= 31; i++){

if (sigismember(pending, i)){

printf("1 ");

}

else{

printf("0 ");

}

}

printf("\n");

}

void handler(int signo)

{

printf("handler signo:%d\n", signo);

}

int main()

{

signal(2, handler);//更改2号信号的动作

//1.对2号信号进行屏蔽

//1.1.准备好我们要传进去的block信号集

sigset_t blockset;//创建一个要传入的block信号集

sigemptyset(&blockset);//清空要传入的block信号集

sigaddset(&blockset,2);//将要传入的block信号集的2号信号位置于1

//1.2.将准备好的block信号集通过系统调用接口传进进程

sigset_t oldblockset;//创建1个block信号集来存储当前进程旧的block信号集

sigemptyset(&oldblockset);//清空block信号集,准备存储当前进程旧的block信号集

sigprocmask(SIG_SETMASK,&blockset,&oldblockset);//更换block信号集合

//至此我们已经把2号信号屏蔽了

//2.重复打印当前进程的未决信号集

sigset_t pending;

int cnt=0;

while(1)

{

//2.1.获取

int n=sigpending(&pending);

if(n<0) continue;

//2.2.打印pending

printPending(&pending);

sleep(1);

cnt++;

if (cnt == 20){

sigprocmask(2, &oldblockset, NULL); //恢复曾经的信号屏蔽字

printf("恢复信号屏蔽字\n");

}

}

}

         此时就可以看到,进程收到2号信号后,该信号在一段时间内处于未决状态,当解除2号信号的屏蔽后,2号信号就会立即递达,执行我们所给的自定义动作,而此时的pending表也变回了全0。

        细节: 在解除2号信号后,2号信号的自定义动作是在打印“恢复信号屏蔽字”之前执行的。因为如果调用sigprocmask解除对当前若干个未决信号的阻塞,则在sigprocmask函数返回前,至少将其中一个信号递达。

我们可以通过上面这个操作屏蔽所有信号吗?

绝对不可能,不用想都知道9号肯定不能,我没空写这部分啊,感兴趣的自己去试试看,我只能告诉你,我的试验结果是9号和19号不可被屏蔽,这个不就是不能被signal函数捕捉是信号吗!也就是说9号信号和19号信号不可被捕捉,也不可被屏蔽



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。