java开发工程师,专注于内核源码,算法,数据结构。 qq:630501400
分类: C/C++
2013-03-04 13:38:22
linux的信号处理机制就是在内核态中把信号的相关信息在写入到其他进程的task_struct(进程信号)或者signal_struct(线程组信号)中,然后内核在一定的时机去处理进程中未处理的信号,对于处理信号的时机有这么几个:系统调用后,系统中断或者异常,返回用户态之前,都会处理当前进程未处理的信号。
arch/x86/kernel/entry_32.S中,系统调用完成后会处理未处理的信号。
544 syscall_exit:
545 LOCKDEP_SYS_EXIT
546 DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
547 # setting need_resched or sigpending
548 # between sampling and the iret
549 TRACE_IRQS_OFF
550 movl TI_flags(%ebp), %ecx
551 testl $_TIF_ALLWORK_MASK, %ecx # current->work
552 jne syscall_exit_work //退出系统调用之前,检查是否需要处理信号
在work_notifysig中会处理挂起的信号。
656 work_notifysig: # deal with pending signals and
657 # notify-resume requests
658 #ifdef CONFIG_VM86
659 testl $X86_EFLAGS_VM, PT_EFLAGS(%esp)
660 movl %esp, %eax
661 jne work_notifysig_v86 # returning to kernel-space or
662 # vm86-space
663 xorl %edx, %edx
664 call do_notify_resume //调用信号处理函数
665 jmp resume_userspace_sig //返回用户空间
在中断或者异常结束之后,返回用户态之前会检查是否有信号挂起,未处理。
362 ENTRY(resume_userspace)
363 LOCKDEP_SYS_EXIT
364 DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
365 # setting need_resched or sigpending
366 # between sampling and the iret
367 TRACE_IRQS_OFF
368 movl TI_flags(%ebp), %ecx
369 andl $_TIF_WORK_MASK, %ecx # is there any work to be done on
370 # int/exception return?
371 jne work_pending //检查是否有信号未处理
372 jmp restore_all
373 END(ret_from_exception)
从上面的代码中可以看出,在系统调用或者中断,异常返回到用户空间之前内核都会检查是否有信号在当前进程中挂起。如果有信号就去处理执行函数do_notify_resume
对于一个信号,如果这个信号没有在用户态声明处理函数,那么有以下几种默认行为:
do_notify_resume函数中会判断thread_info_flags中是否设置了有信号未处理的标志,如果有的话就去处理
838 void
839 do_notify_resume(struct pt_regs *regs, void *unused, __u32 thread_info_flags)
840 {
841 #ifdef CONFIG_X86_MCE
842 /* notify userspace of pending MCEs */
843 if (thread_info_flags & _TIF_MCE_NOTIFY)
844 mce_notify_process();
845 #endif /* CONFIG_X86_64 && CONFIG_X86_MCE */
846
847 /* deal with pending signal delivery */
848 if (thread_info_flags & _TIF_SIGPENDING) //如果设置了_TIF_SIGPENDING标志,就说明有信号未处理
849 do_signal(regs); //就直接调用do_signal函数
在do_signal函数中,会去判断每个被挂起的信号,如果这个信号在用户态没有设置处理函数,那么内核会按照信号的默认处理行为处理信号,如果这个信号在用户态设置了处理函数,那么需要返回用户态去调用这个函数,在内核中会在用户态的栈中插入一个调用那个处理函数的栈针,调用完用户态处理函数后返回内核态,继续do_signal函数的处理。
791 signr = get_signal_to_deliver(&info, &ka, regs, NULL); //获得一个在用户态有处理函数的信号,没有设置用户态处理函数的信号,按照默认处理行为处理
792 if (signr > 0) {
793 /* Whee! Actually deliver the signal. */
794 if (handle_signal(signr, &info, &ka, oldset, regs) == 0) { //构造栈针返回用户态调用处理函数
这里我们关注 get_signal_to_deliver函数所作的事情。
从上面的图中可以看出,处理信号首先要关注一下当前进程是否是收到了SIGCONT信号而导致了进程状态的变化,如果在group
stop的过程中收到了SIGCONT信号,那么这里会默认group
stop过程已经让进程组完全停止,然后通知trace进程或者是父进程,子进程状态发生了改变。如果是在group_stop后收到了SIGCONT,说明进程被唤醒了,同时也要通知trace进程或者父进程,子进程状态发生了改变。
do_signal_stop的函数是判断是否是在group
stop过程中,如果在group
stop,那么当前进程会暂停。group
stop的分析参考stop信号对线程组的影响。
后面的过程是从进程的私有信号队列或者公用信号队列中取出一个信号,然后调度调试器来跟踪当前进程,具体参考信号对调试状态的影响。
后面的细节是如果进程设置了信号处理函数,那么函数就会退出,具体的处理过程交给了用户态。后面会处理stop信号,在调用do_signal_stop之前,会判断一下当前的进程组是否是孤儿进程组,如果是的话就忽略那些非SIGSTOP的暂停信号。
这里孤儿进程组的概念是:该组中每个成员的父进程要么是该组中的一个成员,要么不是该组所属会话(session)的成员。判断是否是孤儿进程的代码:
static int
will_become_orphaned_pgrp(struct pid *pgrp, struct task_struct
*ignored_task)
{
struct task_struct *p;
do_each_pid_task(pgrp,
PIDTYPE_PGID, p) {
if ((p == ignored_task)
||(p->exit_state
&& thread_group_empty(p)) ||is_global_init(p->real_parent))
continue;
//判断进程组的每一个进程,如果存在一个进程的父进程不是当前进程组的进程,同时又是当前会话中的进程,那么这个进程组肯定不是孤儿进程组
if
(task_pgrp(p->real_parent) != pgrp && task_session(p->real_parent) == task_session(p))
return 0;
} while_each_pid_task(pgrp,
PIDTYPE_PGID, p);
return 1;
}
对于孤儿进程组还不太理解,还有待研究。最后的处理过程就相对简单易懂了,stop信号处理完毕后,就剩下进程结束的那写信号了。如果当前信号应该去coredump,就去调用do_coredump,进程结束最后调用了do_group_exit函数。
总结:
信号会在进程返回用户空间前,去处理在信号队列中的信号,如果该信号在用户态设置了信号处理函数,就返回到用户空间去调用信号处理函数,如果未设置,就采取默认的处理行为。