目录
[1.1 信号其他相关常见概念](#1.1 信号其他相关常见概念)
[1.实际执行信号的处理动作称为信号递达(Delivery)](#1.实际执行信号的处理动作称为信号递达(Delivery))
2.信号从产生到递达之间的状态,称为信号未决(Pending)。
[3.进程可以选择阻塞 (Block )某个信号。](#3.进程可以选择阻塞 (Block )某个信号。)
1.阻塞信号
在解释信号的保存时,我们要先了解一下信号中的专有名词及其概念。
1.1****信号其他相关常见概念
1.实际执行信号的处理动作称为信号递达(Delivery)
信号递达,通俗来说就是处理信号。
处理信号的三种方式:
1.信号的忽略
2.信号的默认
3.信号的自定义捕捉
信号的默认
这里我们先编写一个程序:
cpp
#include<iostream>
#include<unistd.h>
#include<signal.h>
int main()
{
while(true)
{
std::cout<<"getpid():"<<getpid()<<" running..."<<std::endl;
sleep(1);
}
return 0;
}
运行起来,然后再使用kill -信号编号 进程pid。给进程发送信号。
这里就是信号9的默认。
信号的自定义
这里我们要使用函数:
cpp
sighandler_t signal(int sugnum,sighandler_t handler)
这里signum是你想自定义信号的编号,handler一个函数指针,指向的是返回值为void,参数为一个int的函数。(这里也可以用一个宏SIG_DFL恢复信号的默认处理)
作用:收到信号,不按照默认方式,自定义处理信号。
cpp
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handler(int signal)//自定义处理信号2的内容
{
std::cout<<"handler:"<<signal<<std::endl;
exit(2);
}
int main()
{
signal(2,handler);//自定义信号2.
while(true)
{
std::cout<<"getpid():"<<getpid()<<" running..."<<std::endl;
sleep(1);
}
return 0;
}
使用宏SIG_DFL恢复默认处理:
cpp
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handler(int signal)//自定义处理信号2的内容
{
std::cout<<"handler:"<<signal<<std::endl;
exit(2);
}
int main()
{
signal(2,handler);//自定义信号2.
signal(2,SIG_DFL);//恢复信号2,默认处理
while(true)
{
std::cout<<"getpid():"<<getpid()<<" running..."<<std::endl;
sleep(1);
}
return 0;
}
信号的忽略
这里还是使用函数signal,但是要使用宏SIG_IGN才能实现函数忽略。
代码:
cpp
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handler(int signal)//自定义处理信号2的内容
{
std::cout<<"handler:"<<signal<<std::endl;
exit(2);
}
int main()
{
signal(2,SIG_IGN);//忽略信号2.
while(true)
{
std::cout<<"getpid():"<<getpid()<<" running..."<<std::endl;
sleep(1);
}
return 0;
}
这里我们可以看到当我们向进程发送信号2时,它并没有退出进程,所以它忽略了信号2.
这里可能大家会有这样的疑问?
signal函数的第二个参数是一个函数指针,为什么可以使用宏SIG_DFL与SIG_ING呢?
这里我们可以直接看这两个宏的定义:
这里我们可以看出,将0强转成了函数指针类型,1也强转成了函数指针类型。
如何看待信号的忽略?
这里忽略,就是处理信号。
2.信号从产生到递达之间的状态,称为信号未决(Pending)。
这里我们知道,如果当进程收到信号时,正在执行更为重要的事务,进程并不会立马处理信号,而是先将信号保存下来,等待合适的时机再处理。所以在信号产生,到信号处理(信号递达)这段时间里我们叫做信号未决。
这里我们知道,保存信号是使用位图来保存的,信号未决具体点可以理解为:在信号位图中,就叫做信号未决。
3.进程可以选择阻塞 (Block )某个信号。
信号未决之后,暂时不递达,直到接触信号的阻塞。
举例理解:
在上课前老师留下了放假作业(接受信号,保存),但是因为上课要听老师讲课,我们不能在课堂上写作业(被阻塞),我们只能在放学后才能写作业(接触阻塞)。
注:
1.信号未决不一定阻塞,但收到信号并阻塞,一定未决。
2.没有收到信号,可以设置信号的递达动作。
3.没有收到信号,可以设置信号阻塞,也可以解除阻塞。
4.阻塞与忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
1.2信号在内核中的表示
上面,我们介绍了信号的递达,未决,阻塞。在OS里是这样实现的。
信号在内核中的表示示意图
首先有block,pending,handler三张表,其中:
pending表:是信号未决表,表的下标代表着是几号信号,表的内容代表着是否收到信号。(使用位图的方式记录)
handler表:对应信号的处理方法,里面可以存放函数指针,或者是SIG_DFL/SIG_ING这样的宏。数组的下标代表着信号编号-1。
这里在进程启动时,就形成了这两张表。因此在进程收到信号前,就认识信号(如何处理信号)
block表:信号阻塞表,pending表一样,使用了位图。比特位的位置:表示信号编号。比特位的内容表示,是否对特定的信号进行屏蔽(阻塞)。
对于一个信号的识别,使用这三张表,要横着看
比如以1号信号为列:
当我们收到1号信号时,我们先将pending表中对应的比特位置为1,然后再看block表中对应的比特位的内容,为0,表示不阻塞,在合适的时候,会在handler表中找到对应的方法处理信号。若为1,则表示信号阻塞,不会对信号进行任何处理,等到比特位的内容变为0,会在合适的时候,在handler表中找到对应的方法处理信号。
因此在收到信号时,能不能被处理,首先取决于对因block表中内容。
上面我们知道对于信号的处理取决于PCB中的这三张表,因此如果我们要控制进程对信号的处理,那么就需要改变这三张表的内容,但是对于我们用户来说,我们并不清楚这三张是如何存储。因此操作系统为我们提供了一系列的系统调用函数,来帮助我们。
sigset_t
从上图来看 , 每个信号只有一个 bit 的未决标志 , 非 0 即 1, 不记录该信号产生了多少次 , 阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型 sigset_t 来存储 ,sigset_t 称为信号集 , 这个类型可以表示每个信号 的" 有效 " 或 " 无效 " 状态 , 在阻塞信号集中 " 有效 " 和 " 无效 " 的含义是该信号是否被阻塞 , 而在未决信号集中 " 有效" 和 " 无效 " 的含义是该信号是否处于未决状态。这里将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask), 这里的 " 屏蔽 " 应该理解为阻塞而不是忽略。
在Linux中是这样定义sigset_t类型的:
信号集操作函数
cpp
#include <signal.h>
//将位图里的比特位全部清零
int sigemptyset(sigset_t *set);
//对指定的位图全部置1
int sigfillset(sigset_t *set);
//将指定信号集中的指定比特位置1
int sigaddset (sigset_t *set, int signo);
//将指定信号集中的指定比特位置0
int sigdelset(sigset_t *set, int signo);
//判定一个信号是否在信号集中
int sigismember(const sigset_t *set, int signo);
使用sigprocmask函数修改block表
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
cpp
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
set为想要修改的信号,
oset记录调用前的block,用于恢复原来的block
返回值:若成功则为0,若出错则为-1
如果 oset 是非空指针 , 则读取进程的当前信号屏蔽字通过 oset 参数传出。如果 set 是非空指针 , 则 更改进程的信号屏蔽字, 参数 how 指示如何更改。如果 oset 和 set 都是非空指针 , 则先将原来的信号 屏蔽字备份到 oset 里 , 然后根据set 和 how 参数更改信号屏蔽字。假设当前的信号屏蔽字为 mask, 下表说明了 how 参数的可选值
代码演示:屏蔽2号信号。
cpp
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handler(int signal)//自定义处理信号2的内容
{
std::cout<<"handler:"<<signal<<std::endl;
exit(2);
}
int main()
{
signal(2,handler);
sigset_t block,oblock;
sigemptyset(&block);//位图置0操作
sigemptyset(&oblock);
sigaddset(&block,2);//这里并没有对2号信号做屏蔽
sigprocmask(SIG_BLOCK,&block,&oblock);//调用系统接口,对2号信号进行屏蔽
while(true)
{
sleep(1);
std::cout<<"getpid: "<<getpid()<<std::endl;
}
return 0;
}
这里可能有人会这样问,既然我们可以控制屏蔽信号,那么如果我们把所有信号都屏蔽了,是不是一旦这样的死循环进程运行起来,是不是就不会被杀掉了?
这里我们直接上代码演示:
cpp
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handler(int signal)
{
std::cout<<"handler:"<<signal<<std::endl;
exit(2);
}
int main()
{
signal(2,handler);
sigset_t block,oblock;
sigemptyset(&block);//位图置0操作
sigemptyset(&oblock);
for(int i=1;i<=31;i++)
sigaddset(&block,i);//将block位图上的所有比特位全部置为1.
sigprocmask(SIG_BLOCK,&block,&oblock);//对所有信号进行屏蔽
while(true)
{
sleep(1);
std::cout<<"getpid: "<<getpid()<<std::cout<<" 我已经屏蔽掉了所有信号,你来打我啊!"<<std::endl;
}
return 0;
}
这里我们可以发现当我们对进程发送除9之外的信号,进程并不会停止(信号被屏蔽),而当收到9信号时,进程就直接执行信号9对应的默认动作。(19号信号也没有被屏蔽)
9号信号我们成为管理员信号,它并不会被屏蔽。
使用sigpending函数查看pending表
cpp
#include <signal.h>
int sigpending(sigset_t *set)
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 下面用刚学的几个函数做个实验。程
序如下
这里我们直接上代码:
解释程序目的,不断打印pendling表中比特位的内容。在最开始的前10秒,阻塞信号2。然后将信号2从block表中删除,我们要在10秒前向进程发送2号信号。
cpp
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handler(int signal)
{
std::cout<<"handler:"<<signal<<std::endl;
//exit(2);
}
void PrintPending(sigset_t& pending)//打印pending中每一个比特位的内容
{
for(int signo=21;signo>=1;signo--)
{
if(sigismember(&pending,signo))
{
std::cout<<'1';
}
else
{
std::cout<<'0';
}
}
std::cout<<std::endl;
}
int main()
{
signal(2,handler);//自定义2号信号
//屏蔽2号信号
sigset_t block,oblock;
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block,2);
sigprocmask(SIG_BLOCK,&block,&oblock);
std::cout<<"getpid: "<<getpid()<<std::endl;
int cnt=0;
//让进程不断获取当前进程的pending表
sigset_t pending;
while(true)
{
cnt++;
sigpending(&pending);//获取pending表中的内容
PrintPending(pending);
if(cnt==10)
{
std::cout<<"解除对2号信号的屏蔽,2号信号已递达。"<<std::endl;
sigprocmask(SIG_SETMASK,&oblock,nullptr);
}
sleep(1);
}
return 0;
}
这里我们可以看到一旦信号2,不阻塞,就立即递达了。并且pending表中对应的比特位置0.
这里可能有人会问pending表置0这个操作是在处理信号前,还是后执行的?
这里我们可以直接用代码验证:
cpp
#include<iostream>
#include<unistd.h>
#include<signal.h>
void PrintPending(sigset_t& pending);
void handler(int signal)
{
std::cout<<"#####################"<<std::endl;
sigset_t sig;
sigpending(&sig);
PrintPending(sig);
std::cout<<"######################"<<std::endl;
std::cout<<"handler:"<<signal<<std::endl;
//exit(2);
}
void PrintPending(sigset_t& pending)//打印pending中每一个比特位的内容
{
for(int signo=21;signo>=1;signo--)
{
if(sigismember(&pending,signo))
{
std::cout<<'1';
}
else
{
std::cout<<'0';
}
}
std::cout<<std::endl;
}
int main()
{
signal(2,handler);
std::cout<<"getpid: "<<getpid()<<std::endl;
while(true)
{
std::cout<<"running..."<<std::endl;
sleep(1);
}
return 0;
}
这里我们采用的是,自定义信号2,在处理时,将pending表的内容打印出来,如果全部位0,则在修改pending实在递达前实行的,否则实在递达之后修改的。