linux异步信号handle浅析

在初学linux编程的时候,一直以为异步信号handle是个很神奇的东西,用户程序可使用singal之类的系统调用为某某信号注册一个信号处理函数(handle函数)。
程序的二进制代码在内存中都有着肯定的执行流程,为何收到异步信号之后,程序会被“中断”,而后跳转到这个handle函数里面去运行呢?内核怎么有能力让程序作这样的跳转呢,总不可能临时修改程序的可执行代码吧?
后来学习了一些内核知识,才知道原来进程收到信号之后,并非当即就被“中断”的,而是先在进程的控制结构(task_struct)中记录下收到了某某信号,而后等到进程即将从内核态返回用户态的时候,流程才被“中断”,handle函数才被调用。
用户进程何时会从内核态返回用户态呢?通常主要是三种状况:系统调用(用户进程主动进入内核)、中断(用户进程被动进入内核)、被调度执行(用户进程从等待执行变为正在执行)。
进程从收到信号到它从内核态返回用户态的过程,是须要必定时间的。可是这个时间通常会很短,至少时钟中断会以较大的频率(好比1毫秒一次)将用户进程带入内核(固然,只针对正在执行的进程)。
在进程即将从内核态返回用户态时,若是有信号须要处理,对应的handle函数将被调用(固然,可能没有注册handle,这时内核对信号进行默认的处理)。注意,如今进程还在内核态,内核是怎么调用用户态的handle函数的呢?
直接调用能够吗?固然不行。内核代码运行在高CPU特权级别下,若是直接调用handle函数,则handle函数也将在相同的CPU特权下被执行。那么用户将能够在handle函数里面随心所欲。
因此,调用handle必须先返回用户态。可是返回用户态后,程序流程又不受内核控制了,难不成内核还真的把用户进程的可执行代码临时改掉?
内核实际的作法仍是比较巧妙。用户进程进入内核之后,都会在其对应的内核栈上留下返回地址,以便流程返回。内核调用handle函数的办法就是临时改掉栈上的返回地址,而后按原有的返回用户态的流程去返回。结果这一返回,就到了handle函数去了。(固然,须要修改的并不止是返回地址,而是一整个调用栈。)
虽然如今临时把返回地址改了,可是用户进程最终仍是要返回到原先那个返回地址去的。那么,原先的返回地址及其调用栈应该保存在哪里呢?进程的内核栈空间有限,而且还须要应付handle函数中可能发生的系统调用,因此内核把这些信息放在内核栈上是不现实的,只能压到了用户栈上去。
当handle函数执行完毕,执行流程要返回到内核去。一样,因为CPU特权级别不一样,从handle函数返回内核时不能单纯地利用RET指令去返回的。须要执行一次系统调用。
在handle执行完后,为何要回到内核,再从内核返回到原始返回地址呢?若是直接返回到原始的返回地址那天然是很便捷。而且要这么作也不难,原始返回地址及其调用栈已经被压到了用户栈上,内核只须要在handle函数的调用栈上稍作手脚就好了。
一、返回到原始返回地址并非回到那个地址就好了,须要把整个现场都恢复(主要是寄存器什么的)。固然,内核也能够在用户栈上面压一些代码,来完成这些事情;
二、如今可能不止一个信号要处理,最好让用户进程返回内核,继续处理其余信号;
为了返回内核,首先,内核在返回到handle函数以前,先将某个返回地址压到用户栈上,以便从handle返回时可以返回到指定的地址上。这个指定的地址其实也在进程的用户栈上,内核又在这个地址上放了几条指令(在栈上放置可执行代码),让进程去调用一个名叫sigreturn的系统调用。
返回到handle函数前的用户栈大体以下:
原有数据 -> 调用sigreturn的指令(设其地址为a) -> 原始返回地址及其调用栈 -> 返回地址(值为a) -> handle的栈变量
内核在handle函数的调用栈上放置sigreturn指令,这是在linux 2.4时的作法。每次调用用户的handle函数都须要向用户栈拷贝这么几条指令,这并不太好。
linux 2.6有一个叫vsyscall page的页面,上面包含了内核为用户程序准备的一些指令,其中就包括调用sigreturn指令。这个vsyscall页被映射到每一个进程的虚拟地址空间靠近末尾的部分,被全部用户进程共享,对于用户进程是只读的。这样,handle函数的调用栈上就不须要再塞入sigreturn指令了,直接将handle函数的返回地址设为vsyscall页中对应的代码便可。
为了让handle执行完之后自动调用sigreturn返回内核,内核作了不少事情。那么可不能够约定好,让用户本身去调用sigreturn呢?
固然,这是能够的。只是为了让信号处理机制成为一套完整的机制,内核并无这么作。不然用户在handle函数里面忘记调用sigreturn的话,可能莫名其妙地进程就崩溃了。而编译器也很难找出这样的错误。
进程调用sigreturn系统调用从新进入内核后,压在用户栈上的原始返回地址及其调用栈被获取。最终内核又会修改栈,让进程返回用户空间时返回到这个原始返回地址上。