LINUX0.11信号机制
一、信号的本质
信号(signal)是Linux操作系统在软件层面上对中断的模拟,是一种异步通信机制。进程之间可以相互发送信号来通知对方发生了什么事情,一个进程不必等待一个信号的到来,他也不知道什么时候会有信号到来。
进程对信号有三种处理方法:
第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。进程通过系统调用signal来指定进程对某个信号的处理行为。
第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。
第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。
二、信号处理流程
信号产生
信号在进程中注册
信号执行和注销
2.1 信号产生
信号产生有两个来源:硬件来源(按下键盘ctrl+c或者硬件故障)和软件来源(向其他进程发信号比如kill、alarm),linux0.11中定义了22种信号。
include/sys/signal.h
2.2注册信号
注册信号就是在与信号绑定的任务上指定对某信号的处理句柄。任务控制结构中有三个字段signal、sigaction[32]、blocked用于描述本任务对信号的处理方式。
include/linux/sched.h
signal是信号处理位图,任务收到一个信号signal中相应的位就会置1。sigaction指定了对某一信号的处理句柄和处理方式,这一点类似于中断向量。如果blocked中某一位被置1则表示对应的信号被阻塞处于阻塞状态,只有blocked中的该位置0才能处理该信号。所以信号的处理与中断的处理是很类似的。
再来看一下sigaction结构。在任务控制结构中有一个sigaction[32]的数组结构,该数组就是本任务的信号处理向量表,下标对应于信号的数值减一(信号值都是从1开始)。
include/sys/signal.h
sa_handler指定了信号处理句柄,指向用户进程中某一代码块。
sa_flags指定了对信号句柄的处理方式,一共有三种:SA_NOCLDSTOP表示进程处于停止状态,不对信号做处理。SA_NOMASK表示当前信号在处理的过程中可以收相同的信号,这个时候可以运行信号嵌套。
SA_ONESHOT指明信号处理函数一旦被调用过就恢复到默认的信号处理函数去,即使得sa_handler=SIG_DFL。
sa_mask是当前信号在处理的过程中需要屏蔽掉哪些信号,通常用于防止信号处理的嵌套。
sa_restorer是用于信号处理完毕后清理用户堆栈并返回到正常程序流的句柄,由gcc编译时的函数库提供。
include/sys/signal.h
综上所述,对信号的注册其实就是设置task_struct中的sigaction[32]数组用于指定对信号的处理方式。
系统调用sys_signal和sys_sigaction都用于完成信号的注册,区别是sys_signal按照系统默认的方式完成信号注册,而sys_sigaction可以由用户自己定义sigaction结构并赋值给task_struct。
kernel/signal.c
sys_signal接收三个参数,信号值、处理句柄、堆栈恢复句柄,按照默认的处理方式处理信号,即允许信号嵌套、只能使用1次,用完即清空句柄,最后返回旧的处理句柄。
kernel/signal.c
sys_sigaction也是接收三个参数,信号值、用户进程提供的sigaction指针和旧的sigaction保存位置。如果指定了oldaction则将旧的sigaction保存其中。这种定义方式较sys_signal灵活。
2.3信号执行和注销
信号的处理发生在1:由于系统调用从内核态返回用户态之前 2:时钟中断发生时 。在kernel/system_call.s中可以看到,当系统调用处理完毕和时钟中断处理程序执行完毕都会跳转到ret_from_sys_call处执行。理解起来也很简单,时钟中断到来会引发进程调度,被选中的进程都是处于运行态(TASK_RUNNING),只有当一个进程处于运行态才有机会执行信号处理句柄。
kernel/system_call.s
ret_from_sys_call的作用就是在系统调用返回之前或者在时钟中断处理完毕返回用户进程之前判断当前用户是否有待处理的未决信号,如果有则提取出信号值,并作为参数调用do_signal,最后通过iret进入到信号处理函数中。如果没有待处理信号或者当前进程是任务0,则直接恢复用户进程现场并返回正常程序流中。
do_signal函数很关键也很有技巧,其作用是将内核栈中保存系统调用返回地址的eip修改为信号处理句柄,这样的话当调用iret时就会进入到信号处理代码中。重新布置用户堆栈,将用户现场从内核栈复制到用户栈。
kernel/signal.c
do_signal的寄存器参数对应于执行系统调用时保存的用户进程现场。
kernel/signal.c
修改内核栈保存的返回地址为信号处理句柄的同时也要修改内核栈保存的用户栈esp,将esp向下调整把内核栈中保存的用户进程现场eax、ecx、edx复制到用户栈中,当信号句柄执行完后ret到sa_restorer中清空用户堆栈恢复用户现场。为什么这里要将用户现场保存到用户栈中呢?
我的理解是,在iret函数之前,ret_from_sys_call就已经通过pop指令恢复了用户现场,这时再通过iret进入到信号处理函数有可能破坏已恢复的现场,而进入到信号处理函数就意味着进入了用户空间,堆栈也已经切换回了用户栈,所以需要再次把用户现场复制到用户栈中,当信号处理函数执行完ret到栈上保存的sa_restorer就负责恢复用户现场,最后sa_restorer执行ret将old_eip弹出,返回到正常程序流。
整个过程借用其他高手的画的一幅图:
信号的注销在信号执行的过程中已经完成,ret_from_sys_call在do_signal之前已经复位了任务控制结构中的signal位图。
kernel/system_call.s
kernel/signal.c
do_signal中也恢复了信号处理向量中的信号句柄。
三、两点问题
在研究完linux0.11的信号处理机制后有两个问题一直没有找到答案。
将用户现场从内核栈保存到用户栈为什么不将所有的寄存器值全部复制过来,而仅仅复制了eax、ecx、edx、eflags。
将信号阻塞码blocked复制到用户栈是什么目的?
如果不允许信号嵌套,do_signal最后设置了信号屏蔽码,那么当信号处理结束时由谁将该信号屏蔽位复位,一直没有找到谁来恢复信号屏蔽码,难道第一次处理完该信号就一直阻塞他不让他再次运行?
还有我发现linux0.11中很多功能处于半成品状态,比如虽然提供了sys_signal和sys_sigaction这两个系统调用,但却没有提供用户调用接口,不知能否有高手提供解答。
- End -
看雪ID:极目楚天舒
https://bbs.pediy.com/user-741085.htm
本文由看雪论坛 极目楚天舒 原创
转载请注明来自看雪社区
热门图书推荐:
(点击图片即可进入)
热门技术文章:
公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com