ToB企服应用市场:ToB评测及商务社交产业平台
标题:
【Linux】信号2——信号的保存
[打印本页]
作者:
吴旭华
时间:
2024-7-30 05:35
标题:
【Linux】信号2——信号的保存
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
#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;
}
复制代码
好像跟它原来的差不多
我们再看看
#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种
#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的方案可能不同)
#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信号集进行验证。
#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号信号递达时执行我们所给的自定义动作。
#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号信号不可被捕捉,也不可被屏蔽
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4