Linux - signal处理的一些补充
signal是一类比较特殊的存在,需要kernel和userspace通力合作才能将signal处理流程走通。Android 平台 Native 代码的崩溃捕获机制及实现对signal的描述比较完整,建议读读。signal处理流程简单的流程描述如下:userspace:利用系统调用sigaction(…)或signal(…)设置感兴趣的signal的属性,包括处理函数、flags等。已经不鼓励
signal是一类比较特殊的存在,需要kernel和userspace通力合作才能将signal处理流程走通。
Android 平台 Native 代码的崩溃捕获机制及实现对signal的描述比较完整,建议读读。
signal处理流程
简单的流程描述如下:
userspace:
- 利用系统调用sigaction(…)或signal(…)设置感兴趣的signal的属性,包括处理函数、flags等。
- 已经不鼓励使用signal系统调用,在libc中被封装成sigaction的调用。
kernel:
在处理sigaction时,把userspace的signal处理函数地址以及flags保存在当前进程的控制块中:
task_struct->sighand->action[]
当有signal触发后,会将相应signal number在如下成员中置位,并置目标进程的状态为TIF_SIGPENDING,然后唤醒目标进程;如果目标进程处于running状态且运行于其他CPU,则发送reschedule IPI中断以便目标进程可以进入kernel并在退出kernel时处理signal;如果目标进程就是当前进程,则在退出kernel时处理signal;如果目标进程从睡眠状态被唤醒,检测到有signal pending,也会退出睡眠状态,从而返回user space。
task_struct->pending task_struct->signal->shared_pending
在进程被唤醒并返回userspace前夕, 发现TIF_SIGPENDING置位,从而执行信号处理函数do_signal(…)。
do_signal: 根据signal number,从 task_struct->sighand->action[]中得到sigaction后,在进程的user stack或user专门的处理信号stack上建立 struct rt_sigframe栈帧。当前kernel stack中的数据会复制到struct rt_sigframe的成员uc.uc_mcontext中。以signal的user处理函数为返回地址,然后返回用户空间。
SA_RESTORER:为了保证执行完user signal handler后能够立刻返回kernel,返回地址指向一个特殊的user函数__restore_rt,实现在libc中。这个地址是在sigaction时传递给kernel的。__restore_rt如下(以x86_64为例)。可见就是系统调用rt_sigreturn,直接进入kernel。
ENTRY_PRIVATE(__restore_rt) .L__restore_rt_START: mov $__NR_rt_sigreturn, %rax syscall .L__restore_rt_END: END(__restore_rt)
userpsace:
- 进入userspace时,直接进入signal handler函数。执行完毕后,函数返回。根据上面所述,返回的地址就是__restore_rt,从而进入kernel。
kernel:
- 进入stub_rt_sigreturn -> sys_rt_sigreturn,恢复信号处理之前的现场,包括恢复uc.uc_mcontext中的数据至kernel stack中、signal handler stack等,以便下次返回userspace时可进入正常流程。
以上就是signal处理的简化流程。
栈的问题
这里再说明一下处理signal时所用的栈的问题。可以用进程的用户栈或用专用于处理signal的栈,在Android上使用的是后者。
在Bionic的文件pthread_create.cpp中,有如下的函数实现:
pthread_create
-> __pthread_start
->__init_alternate_signal_stack
在线程的入口函数__pthread_start中,会分配一段user空间作为信号处理栈空间,并利用系统调用sigaltstack将此栈的地址和长度告诉kernel,kernel会将此地址和长度保存在如下地方:
task_struct -> sas_ss_sp
task_struct -> sas_ss_size
后期在处理此线程的signal时,将此栈作为signal处理函数的栈。
signal的作用域
signal的作用域是进程,跨进程是无效的。在实现signal时,充分利用此限制,从而简单化了处理流程。
从上述的流程分析可见,kernel会保存user signal handler的地址,待处理signal时直接跳转至此地址。由于各个进程的userspace是不一样的,此进程的handler地址在其他进程内是无效或不是期望的函数地址。
那么作用域是线程还是进程的线程组昵?如果signal是给线程的,则pending在 task_struct->pending上;如果是给线程组的,则pending在 task_struct->signal->shared_pending。 由fork时的copy_signal(…)可见,当创建线程时所有的线程共享同一个对象task_struct->signal。
可参见kill, tgkill。kill是发送signal给进程,而tgkill则是发送signal给线程。
更多推荐
所有评论(0)