本文简单介绍下Linux信号处理机制,为介绍二进制翻译下信号处理机制作一个铺垫。
本文主要参考书目 《Linux内核源代码情景分析》 《独辟蹊径品内核:Linux内核源代码导读》
首先,先说一下什么是信号。信号本质上是在软件层次上对
中断机制的一种模拟,其主要有如下几种来源:
- 程序错误:除零,非法内存访问…
- 外部信号:终端Ctrl-C产生SGINT信号,定时器到期产生SIGALRM…
- 显式请求:kill函数容许进程发送任何信号给其余进程或进程组。
在Linux下,能够经过如下命令查看系统全部的信号:
能够经过相似下面的命令显式的给一个进程发送一个信号:
上面的命令将2号信号发送给进程id为pid的进程。不存在编号为0的信号。
目前Linux支持64种信号。信号分为非实时信号(不可靠信号)和实时信号(可靠信号)两种类型,对应于 Linux 的信号值为 1-31 和 34-64。信号是异步的,一个进程没必要经过任何操做来等待信号的到达,事实上,进程也不知道信号到底何时到达。本文着重于Linux的信号处理机制,对信号更多的介绍能够参考
这里。
通常状况下一个进程接受到信号后,会有以下的行为:
进程对信号的响应
- 忽略信号:大部分信号可被忽略,除SIGSTOP和SIGKILL信号外(这是超级用户杀掉或停掉任意进程的手段)。
- 捕获信号:注册信号处理函数,它对产生的特定信号作处理。
- 让信号默认动做起做用:unix内核定义的默认动做,有5种状况:
- a) 流产abort:终止进程并产生core文件。
- b) 终止stop:终止进程但不生成core文件。
- c) 忽略:忽略信号。
- d) 挂起suspend:挂起进程。
- e) 继续continue:若进程是挂起的,则resume进程,不然忽略此信号。
注册信号处理函数
若是想要进程捕获某个信号,而后做出相应的处理,就须要注册信号处理函数。同中断相似,内核也为每一个进程准备了一个
信号向量表,信号向量表中记录着每一个信号所对应的处理机制,默认状况下是调用默认处理机制。当进程为某个信号注册了信号处理程序后,发生该信号时,内核就会调用注册的函数。
注册信号处理函数是经过系统调用signal()、sigaction()。其中signal()在可靠信号系统调用的基础上实现, 是库函数。它只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;而sigaction()是较新的函数(由两个系统调用实 现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用,固然,sigaction()一样支持非实时信号的安装。sigaction()优于signal()主要体如今支持信号带有参数。关于这方面的内容,若是想获取更多,也可参考
这里。
Linux下信号处理机制
进程如何发现和接受信号?
咱们知道,信号是异步的,一个进程不可能等待信号的到来,也不知道信号会到来,那么,进程是如何发现和接受信号呢?实际上,信号的接收不是由用户进程来完成的,而是由内核代理。当一个进程P2向另外一个进程P1发送信号后,内核接受到信号,并将其放在P1的信号队列当中。当P1再次陷入内核态时,会检查信号队列,并根据相应的信号调取相应的信号处理函数。以下图所示:
其中,动做c:发现和捕捉信号
信号检测和响应时机
刚才咱们说,当P1再次陷入内核时,会检查信号队列。那么,P1何时会再次陷入内核呢?陷入内核后在什么时机会检测信号队列呢?
- 当前进程因为系统调用、中断或异常而进入系统空间之后,从系统空间返回到用户空间的前夕。
- 当前进程在内核中进入睡眠之后刚被唤醒的时候(一定是在系统调用中),或者因为不可忽略信号的存在而提早返回到用户空间。
进入信号处理函数
发现信号后,根据信号向量,知道了处理函数,那么该如何进入信号处理程序,又该如何返回呢?
咱们知道,用户进程提供的信号处理函数是在
用户态里的,而咱们发现信号,找到信号处理函数的时刻处于内核态中,因此咱们须要从内核态跑到用户态去执行信号处理程序,执行完毕后还要返回内核态。这个过程以下图所示:
如图中所见,
处理信号的整个过程是这样的:进程因为 系统调用或者中断 进入内核,完成相应任务返回用户空间的前夕,检查信号队列,若是有信号,则根据信号向量表找到信号处理函数,设置好
“frame”后,跳到用户态执行信号处理函数。信号处理函数执行完毕后,返回内核态,设置
“frame”,再返回到用户态继续执行程序。
在上面这段话中,我提到“
frame”,frame是什么?那么为何要设置frame?为何在执行完信号处理函数后还要返回内核态呢?
什么叫Frame?
在调用一个子程序时,堆栈要往下(逻辑意义上是往上)伸展,这是由于须要在堆栈中保存子程序的返回地址,还由于子程序每每有局部变量,也要占用堆栈中的空间。此外,调用子程序时的参数也是在堆栈中。子程序调用嵌套越深,则堆栈伸展的层次也越多。在堆栈中的每个这样的层次,就称为一个”框架”,即frame。
通常来讲,当子程序和调用它的程序在同一空间中时,堆栈的伸展,也就是堆栈中框架的创建,过程主要以下:
- call指令将返回地址压入堆栈(自动)
- 用push指令压入调用参数
- 调整堆栈指针来分配局部变量
为何以及怎么设置frame?
咱们知道,当进程陷入内核态的时候,会在堆栈中保存中断现场。由于用户态和内核态是两个运行级别,因此要使用两个不一样的栈。当用户进程经过系统调用刚进入内核的时候,CPU会自动在该进程的内核栈上压入下图所示的内容:(图来自《Linux内核彻底注释》)
在处理完系统调用之后,就要调用do_signal()函数进行设置frame等工做。这时内核堆栈的状态应该跟下图左半部分相似(系统调用将一些信息压入栈了):
在找到了信号处理函数以后,do_signal函数首先把内核堆栈中存放返回执行点的eip保存为old_eip,而后将eip替换为信号处理函数的地址,而后将内核中保存的“原ESP”(即用户态栈地址)减去必定的值,目的是扩大用户态的栈,而后将内核栈上的内容保存到用户栈上,
这个过程就是设置frame.值得注意的是下面两点:
- 之因此把EIP的值设置成信号处理函数的地址,是由于一旦进程返回用户态,就要去执行信号处理程序,因此EIP要指向信号处理程序而不是原来应该执行的地址。
- 之因此要把frame从内核栈拷贝到用户栈,是由于进程从内核态返回用户态会清理此次调用所用到的内核栈(相似函数调用),内核栈又过小,不能单纯的在栈上保存另外一个frame(想象一下嵌套信号处理),而咱们须要EAX(系统调用返回值)、EIP这些信息以便执行完信号处理函数后能继续执行程序,因此把它们拷贝到用户态栈以保存起来。
以上这些搞清楚以后,下面的事情就顺利多了。这时进程返回用户空间,就会根据内核栈中的EIP值执行信号处理函数。那么,信号处理程序执行完后,怎么返回程序继续执行呢?
信号处理函数执行完后怎么办?
信号处理程序执行完毕以后,进程会主动调用
sigreturn()系统调用再次回到内核,查看有没有其余信号须要处理,若是没有,这时内核就会作一些善后工做,将以前保存的frame恢复到内核栈,恢复eip的值为old_eip,而后返回用户空间,程序就可以继续执行。至此,内核遍完成了一次(或几回)信号处理工做。