在初学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函数的调用栈上稍做手脚就行了。
1、回来到原始回来地址并不是回到那个地址就行了,需求把整个现场都康复(主要是寄存器什么的)。当然,内核也能够在用户栈上面压一些代码,来完结这些工作;
2、现在或许不止一个信号要处理,最好让用户进程回来内核,持续处理其他信号;
为了回来内核,首要,内核在回来到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体系调用从头进入内核后,压在用户栈上的原始回来地址及其调用栈被获取。终究内核又会修正栈,让进程回来用户空间时回来到这个原始回来地址上。