信号众所周知就是一种通知的行为,当信号通知到本人的时候,我们会知道具体要做什么。
同样,进程信号就是操作系统OS用于控制进程的一种机制。
进程本身不知道做什么,只有当操作系统给相关进程发送对应的信号之后进程才会开始处理。
在Linux中,进程信号收到后如果还没处理完当前事务,那么则会进行等待。
为什么?因为信号的产生是异步的,有可能进程正在做更重要的事情!
异步:进程间相互独立,无需等待彼此。
既然信号已经产生了,有可能又不会立即被处理,那么肯定就需要被保存起来!等到需要的时候我们再拿出来递达!
所以我们本节的讨论将从信号产生—信号保存—信号处理—信号关闭来进行分享。
信号的产生可以因为很多的原因:
1.键盘等外设可以产生信号
2.CPU寄存器可以产生信号
3.软件可以产生信号
4.系统调用也可以产生信号
…
不同信号产生的时间和原因不同,导致了信号被递达之后的处理动作不同。
由于信号可能存在多个,那么一个进程在执行的时候就会有先后顺序,这也从侧面说明了进程收到信号之后并不一定会马上执行,往往是等待当前进程执行结束之后再处理。
信号被接收到之后由于不会被立即处理,那么就必定需要被暂时保存起来。那么保存在哪里呢?
一般会被记录在PCB中。
具体来讲,会被记录在也变中的三张表中:pending表、block表和handler表
pending表:位图结构,01代表是否收到该信号
block表:位图结构,代表信号是否被阻塞
handler表:函数指针数组,代表具体的信号递达动作
信号就是通过这三张表,pending表代表是否收到了信号、block表代表信号是否被阻塞、handler表代表了具体动作。通过这一连串的动作来存储信号。
其实信号暂存在页表中也间接说明了,信号要么不被处理,要处理那么就会肯定走到通过页表映射虚拟内存的地方间接保证了信号处理的一致性。
值得一提的是:三张表都是通常放在内核级的页表中!
在32位操作系统下,虚拟内存默认有4G,其中:
所有进程都有自己独立的用户级页表,也就是说存储堆栈数据的[0,3]GB不同,代表用户,是用户态。
但是所有进程都享有同一张内核级页表,也就是说[3,4]GB的数据是相同的,代表操作系统,是内核态。
当从内核态切换到用户态的时候,此时信号就会被看成合适的时机去作处理。
在信号处理部分中我们会讲三个系统调用接口模拟人工处理信号。分别是:signal()、sigprocmask()和sigpending()函数来进行信号处理。
1.可以将信号换成自定义动作的signal()函数
我们接下来用包含signal的简单代码来自定义替换一个信号来执行对信号的处理。
void handler(int signo)//自定义函数
{
std::cout<< "调用了handler方法:"<< signo << std::endl;
}
int main()
{
signal(2,handler);//切换2号信号的动作
while(true)
{
std::cout<<"一个程序正在被执行...,PID:"<
这个代码我们替换了2号信号ctrl+C的动作,切换成输出我们自己的函数。
在执行完后,我们按ctrl+C已经关闭不了进程了,取而代之的是打印我们自己的函数!这就是信号替换函数
再讲解这两个函数之前,我们需要了解信号集。
信号集也很简单,就是一种单独表示信号的数据类型:sigset_t。
了解之后我们再来介绍这两种信号处理办法
2.sigprocmask()函数可以读取添加更改信号的屏蔽字
3.sigpending()函数用来读取pending表
下面我们利用这两个函数来完成一个场景:将2号信号SIGINT(ctrl+C)屏蔽之后,打印pending位图表,看看屏蔽后pending表是否能收到信号。
void PrintPending(const sigset_t& pending) //自定义打印函数
{
cout<<"pending位图:";
for(int signo = 1;signo<=31;signo++)
{
if(sigismember(&pending,signo)) cout<<"1"; //sigismember函数检查信号是否在信号集中
else cout << "0";
}
cout<<"\n";
}
int main()
{
//这里完成一个任务:将2号信号SIGINT(ctrl+C)屏蔽之后,打印pending表
sigset_t set,oset;//定义的信号
sigemptyset(&set);//置空
sigemptyset(&oset);
sigaddset(&set,2);//添加信号
sigprocmask(SIG_BLOCK,&set,&oset);//屏蔽信号
//while信号获取进程的pending信号集,并01打印
while(true)
{
sigset_t pending;//定义pending表
sigemptyset(&pending);
int n = sigpending(&pending);//获取pending表
assert(n == 0);
(void)n; //防止出现warning
PrintPending(pending);//调用自定义打印函数
sleep(1);//休眠1s
}
return 0;
}
在完成我们这段代码后,我们应该观察到:最开始的pending位图为0,在我们按下ctrl+C后pending表上会获取到2号信号并答应输出
在我们按下ctrl+C后,观察到确实捕捉到了2号信号出现的位图。代表我们模拟人工处理信号成功。
信号的关闭我们可以通过kill杀死信号来直接终止信号。由于信号终止有着不同的原因,因此我们给kill信号会给上不同的标识符。
例如最有名的kill -9杀死进程命令。
其中,1-31号信号被称为普通信号 34之后的被称为实时信号
其实,在kill - l列表中,每一个1-31的信号都可以杀掉进程(因为不同原因),但为什么kill -9命令这么出名,以至于所有人在讲杀死进程命令的时候第一时间就会说的是kill - 9而不是其他命令后缀呢? 这是因为kill -9 比较特殊,它无法通过例如signal()函数来重设自定义动作,但是其他的kill后缀命令却可以被重写(例如上面2号命令的例子)。这是系统为了防止出现进程永远无法关闭的进程而设计出的优良规则!
此外,信号终止会有两种结束方式:Term终止和Core终止
Term:标志着终止后不做任何事情。
Core:标志着在终止前会核心转储相关代码,形成core二进制转储文件,之后再终止。这种通常是在发生重大异常之前会生成这种文件。但本身这种core dump设置是被默认关闭的,因为会涉及到隐私以及占用空间过大的问题!