linux0.11信号机制
本文简单描述linux0.11信号机制的实现
www.zhishiwu.com
一:有关信号
当进程收到一个信号后,进程根据相关设定调用信号处理函数。
有三类信号处理方式:默认处理方式、忽略信号方式、执行用户设定的信号处理函数。
发送信号的方式:按下相应的键(如CTRL+C)、使用kill命令或函数向指定进程发送信号。
typedef void sig_func(int);
sig_func *signal(int signr, sig_func *handler);
当在进程中调用signal(signo, handler)之后,
如果进程收到信号signo,则进程会执行handler指向的函数。
假设有进程A和进程B,当进程A给进程B发送了一个信号后,那进程B的信号处理函数什么时候执行?
若要执行进程B的信号处理函数,则进程B必须处于执行状态。也就是说只有当调度程序调度到了进程B后,
才可能执行进程B的信号处理函数。否则进程B不处于可执行状态,收到了信号也没用。
若进程B自己调用kill函数,给自己发送了一个信号,我们会假定这个信号处理函数会立即执行。
因此在执行系统函数kill之后会立即处理进程B的信号。
从这两点可以意识到,内核需要在时钟中断和系统调用后对当前进程的信号进行处理。
需要在时钟中断时是因为时钟中断会调用schedule函数,因为这是分时系统,
如果进程A给B发了信号,而且现在调度到了B,那理所当然要执行B的信号处理函数。
二:linux0.11的信号机制
以 kill 函数为例来简单说明大致流程, 下面再来详细描述内核中的do_signal函数。
当以kill函数给当前进程发送一个信号之后。
因为这是个系统函数,因此会执行int 0x80进入system_call的入口点
_system_call:
cmpl $nr_system_calls-1, %eax # %eax保存kill函数的调用号
ja bad_sys_call # 无效的系统调用
push %ds
push %es
push %fs
push %edx
push %ecx
push %ebx # 相关数据入栈
.....................
call _sys_call_table(,%eax,4) # 执行系统调用, 这里就是 sys_kill 函数了
pushl %eax # 系统调用的返回值入栈,也即是 sys_kill 的返回值
......................
ret_from_sys_call:
.....
pushl %ecx # %ecx中保存了信号的信号值。
call _do_signal # 对信号进行处理
popl %eax # 将信号值出栈
popl %eax # 将系统调用返回值出栈, 也就是sys_kill的返回值存入%eax寄存器
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret
可见每次系统调用之后,可能会执行ret_from_sys_call,进而对信号进行处理。
除了在_system_call里会这样, 在一些中断下也会调用ret_from_sys_call,时钟中断就是其中之一。
现在已经知道内核是“何时”来处理进程的信号了。
三:do_signal函数。
do_signal的功能主要是设置了内核的堆栈和应用的用户堆栈,设置好堆栈后,
当执行ret_from_sys_call最下面的iret指令的时候,去自动执行进程的信号处理函数。当信号处理函数执行完成后,又会接着进程的下一条指令去执行。
下图是《Linux0.11内核完全注释》一书里的,很好的显示调用do_signal前后的堆栈变化。
左边的为内核态堆栈,就是在执行call _do_signal之前的堆栈内容。
do_signal执行如下操作
1:将堆栈中的eip值,保存到old_eip中,old_eip就指向了用户程序中即将执行代码
2:将eip执行信号处理函数。这样当执行ret_from_sys_call中的iret时,会执行cs:eip指向的代码,也就是信号处理函数。
3:将用户态堆栈的esp的值,向下移7或8个长字(32位)
4:然后将sa_resotrer, signr等值放入堆栈, 见图右边的用户堆栈。
完成上述操作后,do_signal执行完毕,返回到ret_from_sys_call中,
ret_from_sys_call执行一些pop操作后执行iret指令, 这时会跳转到信号处理函数去执行。
当信号处理函数执行完后,会执行ret操作(函数的返回使用ret,中断的返回使用iret),这时会将sa_restorer存入eip,
因此接下来就会执行sa_restorer
sa_restorer会恢复用户堆栈
__sig_restore:
addl $4, %esp
popl %eax # 将系统调用的返回值存入eax
popl %ecx
popl %edx
popfl
ret
当执行完popfl之后,明显用户堆栈里面只剩下old_eip了, 因此执行ret,程序就会跳转到cs:old_eip去执行,也就是系统调用的下一条用户指令了。
至此信号处理函数已经执行,系统调用也已返回,用户程序无忧无虑的继续执行。