前面介绍了信号的保存与产生的基本原理,下面介绍一下信号处理的相关知识。
1、信号何时被处理?
前面我们提到,信号在被进程接受后,不一定会被马上处理,而是要等到合适的时机才会被进程处理。而这个合适的时机其实就是OS在从内核态转为用户态时,对信号进行处理的。
<1>内核态与用户态:
什么是内核态,什么是用户态呢?简单来说,内核态是我们调用系统调用或处理中断和异常时,OS就会陷入内核,此时就处于一种内核态。而用户态就是执行普通的代码(例如循环、判断语句)时,系统所处的状态。下面用一张图来先简单了解一下。
当OS执行中断、异常、系统调用时会进入内核,当内核处理完这些异常时,就会检查是否有信号需要处理。如果信号自定义了处理函数,那么OS会转回用户态执行信号自定义处理函数。处理完成后,会返回内核态,然后再从主控制流程中上次被中断的地方继续向下执行(一定是先回内核,再返回主控制流程)。为了方便理解,我们可以将该过程中的状态转换抽象成上图B。图中的交点就是信号处理的过程,而这一个流程下来会经历4次状态的变换。(为了方便记忆,我们可以将其看成倒8,或是无穷大符号)
为了更深入的理解用户态与内核态,我们需要对地址空间进行重新的认识。
前面我们介绍的地址空间,都只是再用户的3GB的空间内,而在地址空间中,内核空间的1GB,我们一直没有介绍,这内核的1GB其实就是用来映射操作系统的一些信息的。
在用户级的页表中,进程可以向内存中存储自己的数据。而内核级的页表是用于映射OS(OS只有一个)的关键数据和相关结构,也就是说,所有进程的内核页表是可以一样的,它们都用于映射物理内存中同一块空间,而这也就是不同的进程能访问同一操作系统中的数据和一些调用。尤其是系统调用,系统调用其实就是函数指针数组,调用系统调用其实就是先获取该数组的地址,然后进入内核空间访问数组中对应的方法。而用户态和内核态的转换,其实就是在用户空间和内核空间中进行不断地切换。
<2>操作系统是如何运行
前面我们提到,当键盘产生硬件中断时,cpu会暂停处理正在运行的进程,并且通知OS从对应的寄存器中读取硬件中断的相关数据,然后查找对应的处理函数并处理。而中断向量表这个设计,其实和我们的信号非常相似。实际上,信号技术的产生就是为了模拟硬件中断,只不过这个是以软件的方式对其进行模拟。
进程可以被OS调度运行,但是谁来让OS运行起来呢?答案就是硬件中断,当硬件中断产生时,OS会去对应中断向量表中查询下一步该做什么。当查询到后,就会执行对应的操作,就比如进程调度等。为了让OS正常运行,我们就需要一个能以非常高频,并且在极短时间内产生中断的装置。这样,我们就能使cpu不断地处理中断,而OS也就会不断查询对应操作而正常运行。而这个过程就称为OS的周期时钟中断。而我们也可以看出,操作系统是一个死循环,需要不断处理外部的硬件中断。
<3>内核态和用户态在硬件中的标识
在地址空间当中,OS在执行用户代码时,是不能随意的访问内核中的数据。因为OS不会信任任何人,所以产生了内核态和用户态防止用户空间中的代码随意访问内核中的数据。为了标识这两个状态,cpu中会存在一个寄存器,里面存储了两个比特位,其中我们以0标识内核态,3标识用户态。当然,这个过程非常复杂,这里简单介绍。
2、信号处理函数sigaction
该接口的功能比signal要更加强大一些。第一个参数表示需要处理的信号,第二个参数和第三个参数都是struct sigaction结构,第二个参数表示新的处理方法,第三个参数表示老的信号处理方法。下面我们看一下这个结构体的具体参数。
该结构中,第一个参数是一个信号处理的自定义函数,第二个暂时不需要关心,这是处理实时信号的,第三个是一个信号集,第四个标志设置为零即可,第五个成员暂时也可以不用设置。
这个信号集是屏蔽信号集,当某个信号的处理被调用是,内核自动将当前信号加入进程的信号屏蔽字。如果我们处理完对应的信号,该信号默认也会从信号屏蔽字中进行移除,这样做主要是为了不想让信号嵌套式地捕捉处理。如果我们希望屏蔽其他信号,就需要向该信号集中添加信号即可。
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signum)
{
while (true)
{
sigset_t pending;
sigemptyset(&pending);
sleep(1);
sigpending(&pending);
std::cout << "pending bitmap : ";
for (int i = 31; i > 0; i--)
{
if (sigismember(&pending, i))
{
std::cout << "1";
}
else
{
std::cout << "0";
}
}
std::cout << std::endl;
}
}
int main()
{
struct sigaction act, oact;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = handler;
sigaction(2, &act, &oact);
while (1)
sleep(1);
return 0;
}
就比上述的代码,如果我们没有做信号的屏蔽,上述的handler方法会一直执行,不会终止。在这期间可能又会收到2号信号的话,就会造成嵌套。