目录
前言:
按照信号学习的时间戳,从信号的预备知识,到信号的产生,到了信号的保存,终于,我们进入了信号学习的尾声,信号处理部分。
那么,信号处理部分,我们介绍的顺序是从捕捉信号入手,到多个子问题,子问题包括了内核态VS用户态,其中要了解的是再谈地址空间,谈谈键盘输入数据的过程,谈谈如何理解OS正常的运行,在OS正常运行这里,理解系统调用,理解OS如何运行的,最后是内核态和用户态的VS。
那么话不多说,我们直接进入主题吧!
捕捉信号
这里提问,什么是信号处理呢?处理信号难道不就是信号递达的过程吗?那么我们上次递达信号的时候,谈论到信号递达是有多种方式的,默认是进程终止,还有忽略,还有自定义行为。涉及到的两个宏是SIG_IGN,SIG_DFL:
cpp
int main()
{
signal(2,SIG_IGN);
while(true)
{
std::cout << "My pid is " << getpid() << std::endl;
sleep(1);
}
return 0;
}
使用的忽略方式,那么现象是:
正常打印。
使用的是默认的那么肯定就是终止了,这里就不掩饰了。
那么信号处理的时候,是在什么时候处理呢?是在信号合适的时候处理吧?
那么提问了就,合适的时候,是什么时候呢?
在此之前,我们需要了解一下这个图,当我们执行用户层面的代码的时候,因为某种原因,比如异常,比如是系统调用,程序进入到了内核,那么进入内核肯定是为了完成某种任务才进入的,此时,要完成之前可以递达的信号,然后进行信号处理,处理信号的时候,因为task_struct有3个表,对于handler表来说,我们自定义了函数,所以此时,流程从内核转移到了用户,那么执行完了之后,信号处理函数结束之后会调用sigretun返回内核,内核方面处理完了,重新返回正文部分。
那么这个过程是一个倒8的图吧? 此时,一根线,将整个流程分为了用户态和内核态,那么合适的信号处理是什么时候呢?这里直接给结论:
信号捕捉的过程,状态切换的时候进行信号的检测和处理。
应该有人会有疑问了,为什么OS不自己去执行handler的函数呢?实际上是可以的,但是因为handler是用户自定义的,并且,OS不相信任务用户,所以这部分的代码只能以用户的身份执行。
上面提到了内核态和用户态,这里我们就进入到下一个话题,什么是用户态,什么是内核态。
再谈地址空间
对于内核态和用户态的第一个子问题是,再谈谈地址空间。
我们能够理解的
是上面的图片,虚拟内存,也就是地址空间,是在task_struct里面的,叫做mm_struct,通过页表的方式,结合MMU,将二者的地址成功映射到一张表上面,那么,对于函数来说,我们执行函数的时候,得要先找到函数的地址吧?那么因为有了页表,所以我们能够找到地址。
可是,我们使用系统调用的时候,我们怎么找到呢?
对于一般的代码,我们可以通过正文代码部分找到,即便是动态库,在栈和堆之间有一个区域叫做共享区,动态库加载到了这里面,咱们也能找到。可以对于系统调用的代码我们没有,怎么找到呢?
OS是软件吧?在开机的时候,第一个加载的软件是OS吧?加载OS之后,OS内部的方法也会加载进去吧?地址空间里面有1G的空间是内核空间吧?内核空间应该指向物理内存的OS吧?应该要页表来映射地址吧?
可是,有了页表应该,难道每个进程都要映射同一份物理地址吗?这也太麻烦了吧,所以,存在一份页表,叫做内核级页表,通过内核级页表,每个地址空间都可以找到对应的OS,并且只有一份内核级页表。
以上是对地址空间的重新理解,实际上就多出来了一份内核级的页表而已。
而对于内核空间的访问,因为OS不相信任何的用户,所以我们访问内核空间的时候,受到了一定的约束,约束就是必须要使用系统调用才能够访问内核空间。
谈谈键盘输入数据
在信号产生,我们是遗留的一个问题的,键盘是如何输入数据的呢?或者是,OS怎么知道我们点击了键盘的哪个键呢?
当我们在键盘点击了一个按键之后,键盘会往cpu发送一个硬件中断的信号,可是我们知道cpu是不能直接和外设打交道的,所以,需要借助某些芯片,比如芯片8259,将信号发给cpu,cpu将信号放松到寄存器里面,而键盘有自己的中断号,所以在内存里,OS加载了对应方法,比如控制键盘的,控制显卡的,控制磁盘的,找到对应的方法,通过下标的方式找到,将数据转到中断向量表里面,此时,OS就知道我们按的是哪个键了。
那么对于中断向量表,它实际上就是一个函数指针数组了。
我们可以通过键盘往cpu发送信号,也可以通过时钟,但是这些都是外部发送的数据,cpu是否可以自己给自己发送信号呢?
抛开这个问题不谈,难道你不觉得这个过程和我们学习的信号产生十分的相似吗?
实际上,信号就是根据这个过程实现,不过信号是软件和软件之间的联系,键盘这种,是外设和OS这个软件之间的联系。
所以,信号不过是一种模拟实现而已。
对于cpu来说,它可以自己给自己发送中断信号,然后将中断信号放到寄存器里面,这种方式叫做陷阱,缺陷,这里就不深究了。我们了解键盘是如何输入数据的就可以了。
理解OS正常的运行
理解系统调用
对于系统调用这里,我们知道OS最先加载到内存里面,那么对应的函数也是加载进去了,加载的都是系统调用函数吧?
那么是怎么找到系统调用函数的呢?这么多系统调用函数。
就像刚才说的,假设键盘的中断号是3,cpu给到寄存器,OS开始找系统调用方法,那么我们得知道号码吧?
所以第一个必要条件,知道对应的系统调用号。
难道你不好奇,系统函数放的地方是哪里吗?或者说系统调用的地址是在哪里吗?
实际上就是这个数组咯,对吧,所以我们还需要这个系统调用表。
所以系统调用,实际上就是通过这两个必要条件找到的。
OS如何运行的
最后一个子问题,OS如何运行的?
难道你不好奇,一个软件的生命周期,为什么是从电脑开机到电脑关机呢?
因为OS的运行本身就是一个死循环啊!!!
它既然是一个死循环,是如何调用任务的呢?在键盘输入数据的时候我们提及到了,时钟可以给cpu发送中断信号,所以cpu接收到了中断信号,给OS一说,OS去找对应的系统号,找对应的表就可以找到了对应的方法。
可是好像现在离我们谈论的话题---用户态和内核态越来越远了?
提问,地址空间里面的内核空间是如何访问的呢?我们确实可以通过地址空间访问OS,可是!
我们是用户啊!!前一秒我们还在说OS不相信任何用户,后一秒我们难道就可以通过地址空间访问OS了吗?
当然不是:
在cpu里面有一个寄存器叫做code semgment,通过该寄存器,我们就可以实现从用户态到内核态的切换了。
你不是说只有内核态才能够访问内核空间吗?
那么我们就设置一个标志位,代表是从内核态和用户态之间的切换,该寄存的后两位是二进制,00代表内核态,11代表用户态,当我们从地址空间去访问数据的时候,会经历一次判断,判断是否符合内核态!!
这就是内核态和用户态之间的操作!
说了那么多,我们介绍一个函数吧,与信号处理相关的,叫做sigaction:
这个函数看起来是有点复杂的:
对于第一个参数是信号,第二个参数是输入型参数,结构体是上面的这个,我们其他的都不管,就管sa_mask和handler即可。第三个参数输出型参数,为了保存之前信号处理方式,这不就和前面介绍的函数sigpromask一样吗?
所以使用起来也是没什么区别的:
cpp
void Print(sigset_t &pending)
{
for(int sig = 31; sig > 0; sig--)
{
if(sigismember(&pending, sig))
{
std::cout << 1;
}
else
{
std::cout << 0;
}
}
std::cout << std::endl;
}
void handler(int signum)
{
std::cout << "get a sig: " << signum << std::endl;
while(true)
{
sigset_t pending;
sigpending(&pending);
Print(pending);
sleep(1);
}
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask); // 如果你还想处理2号(OS对2号自动屏蔽),同时对其他信号也进行屏蔽
sigaddset(&act.sa_mask, 3);
act.sa_flags = 0;
for(int i = 0; i <= 31; i++)
sigaction(i, &act, &oact);
while(true)
{
std::cout << "I am a process, pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
看起来更像是对sigset_t的一种封装?
当前如果正在对n号信号进行处理,默认n号信号会被自动屏蔽。
对n号信号处理完成的时候,会自动解除对n号信号的屏蔽
感谢阅读!