Linux 2.6 内核阅读笔记 信号

2014年8月3日 信号处理程序调用过程

当一个进程接收到一个信号时,需要暂停进程执行转去执行专门的信号处理函数(如果定义了这个信号的专门处理函数的话),然后再继续执行进程代码。

所有的信号处理都是通过内核函数do_signal进行的,do_signal如果发现需要处理的信号,并且这个信号有专门的处理函数,就需要调用这个用户态的函数,这是通过handle_signal来处理的。执行信号处理函数是非常复杂的任务,因为在用户态和内核态来回切换需要特别谨慎地处理栈里的内容,在当前进程恢复“正常”执行前,首先需要执行用户态的信号处理函数。此外当内核打算恢复进程的正常执行时,内核态堆栈不再包含被中断程序的硬件上下文,因为每当从内核态到用户态转换时,都会清空内核栈的内容。另外一个复杂性是因为信号处理程序可以调用系统调用,在这种情况下,在执行了系统调用后,必须回到信号处理程序而不是到被中断程序的正常代码。

内核采用的解决方法是把保存在内核态中的硬件上下文拷贝到当前进程的用户态堆栈中。当信号处理程序结束时,自动调用sigreturn()系统调用把这个硬件上下文恢复到内核态堆栈中。

先来看个图:


一个非阻塞的信号发送给一个进程,当中断或者异常发生时,切换到内核态,正要返回到用户态之前,内核执行do_signal函数来处理信号(如果有的话),通过调用handle_signal来处理用户态的信号处理函数的执行环境的建立(调用setup_frame),然后回到用户态,直接到了信号处理函数(由于对用户态堆栈进行了修改),信号处理函数处理完之后,又调用了sys_sigreturn系统调用(由于对用户态堆栈进行了修改),然后对用户堆栈进行恢复操作,结束后回到了用户态的正常执行路径。

接下来让我们看下内核态是怎么对用户态内核的堆栈进行修改的,主要是setup_frame这个函数:

//sig为捕获的信号
//ka包含用户态的信号处理函数
//set为组设信号的位掩码数组地址
//regs为当前内核栈的寄存器值,修改这个对象可以使得内核跳到用户态的信号处理函数里
static int setup_frame(int sig, struct k_sigaction *ka,
		       sigset_t *set, struct pt_regs * regs)
{
	void __user *restorer;
	struct sigframe __user *frame;
	int err = 0;
	int usig;
	
	//得到一个用户态的信号处理栈结构,里面包含了信号值,信号处理函数执行完毕后的返回地址,信号上下文等等数据
	frame = get_sigframe(ka, regs, sizeof(*frame));

	if (!access_ok(VERIFY_WRITE, frame, sizeof(*frame)))
		goto give_sigsegv;

	usig = current_thread_info()->exec_domain
		&& current_thread_info()->exec_domain->signal_invmap
		&& sig < 32
		? current_thread_info()->exec_domain->signal_invmap[sig]
		: sig;
	//将信号值存放到信号处理栈里
	err = __put_user(usig, &frame->sig);//
	if (err)
		goto give_sigsegv;
	//建立信号处理上下文,存放到信号处理栈里
	err = setup_sigcontext(&frame->sc, &frame->fpstate, regs, set->sig[0]);
	if (err)
		goto give_sigsegv;

	if (_NSIG_WORDS > 1) {
		err = __copy_to_user(&frame->extramask, &set->sig[1],
				      sizeof(frame->extramask));
		if (err)
			goto give_sigsegv;
	}
	//这里就是信号处理函数执行完毕后的返回地址
	restorer = &__kernel_sigreturn;
	if (ka->sa.sa_flags & SA_RESTORER)
		restorer = ka->sa.sa_restorer;

	/* Set up to return from userspace.  */
	err |= __put_user(restorer, &frame->pretcode);
	 
	/*
	 * This is popl %eax ; movl $,%eax ; int $0x80
	 *
	 * WE DO NOT USE IT ANY MORE! It's only left here for historical
	 * reasons and because gdb uses it as a signature to notice
	 * signal handler stack frames.
	 */
	err |= __put_user(0xb858, (short __user *)(frame->retcode+0));
	err |= __put_user(__NR_sigreturn, (int __user *)(frame->retcode+2));
	err |= __put_user(0x80cd, (short __user *)(frame->retcode+6));

	if (err)
		goto give_sigsegv;

	/* Set up registers for signal handler */
	//将栈顶指针置为信号处理栈
	regs->esp = (unsigned long) frame;
	//将epi指向信号处理函数的第一条指令
	regs->eip = (unsigned long) ka->sa.sa_handler;
	//eax为信号的值,也就是信号处理函数中的参数
	regs->eax = (unsigned long) sig;
	regs->edx = (unsigned long) 0;
	regs->ecx = (unsigned long) 0;

	set_fs(USER_DS);
	regs->xds = __USER_DS;
	regs->xes = __USER_DS;
	regs->xss = __USER_DS;
	regs->xcs = __USER_CS;

	/*
	 * Clear TF when entering the signal handler, but
	 * notify any tracer that was single-stepping it.
	 * The tracer may want to single-step inside the
	 * handler too.
	 */
	regs->eflags &= ~TF_MASK;
	if (test_thread_flag(TIF_SINGLESTEP))
		ptrace_notify(SIGTRAP);

#if DEBUG_SIG
	printk("SIG deliver (%s:%d): sp=%p pc=%p ra=%p\n",
		current->comm, current->pid, frame, regs->eip, frame->pretcode);
#endif

	return 0;

give_sigsegv:
	force_sigsegv(sig, current);
	return -EFAULT;
}
由于对内核栈寄存器进行了修改(esp,eip),即当内核回到用户态的时候,就到了信号处理函数进行执行,当信号处理函数执行完毕后,就回到了__kernel_sigreturn的汇编函数里:

_ _kernel_sigreturn:
popl %eax  //将信号编号从栈中移除
movl $_ _NR_sigreturn, %eax //将sys_sigreturn系统调用编号放入eax
int $0x80 //触发系统调用中断,进入sys_sigreturn系统调用
然后sys_sigreturn系统调用对内核栈和用户栈进行了恢复操作:

asmlinkage int sys_sigreturn(unsigned long __unused)
{
	struct pt_regs *regs = (struct pt_regs *) &__unused;
	//注意这个frame在setup_fram函数里已经存到esp里了
	struct sigframe __user *frame = (struct sigframe __user *)(regs->esp - 8);
	sigset_t set;
	int eax;
	//检查用户态的地址是否合法
	if (!access_ok(VERIFY_READ, frame, sizeof(*frame)))
		goto badframe;
	if (__get_user(set.sig[0], &frame->sc.oldmask)//将执行信号处理函数之前所阻塞的信号的位数组从frame->sc里拷贝出来
	    || (_NSIG_WORDS > 1			
		&& __copy_from_user(&set.sig[1], &frame->extramask,
				    sizeof(frame->extramask))))
		goto badframe;

	sigdelsetmask(&set, ~_BLOCKABLE);
	spin_lock_irq(¤t->sighand->siglock);
	current->blocked = set;//将当前进程的信号阻塞掩码恢复成执行信号处理函数之前所阻塞的信号的位数组
	recalc_sigpending();
	spin_unlock_irq(¤t->sighand->siglock);
	
	if (restore_sigcontext(regs, &frame->sc, &eax))//将存放在frame里面的硬件上下文恢复到当前内核栈里,并从用户态删除frame
		goto badframe;
	return eax;

badframe:
	force_sig(SIGSEGV, current);
	return 0;
}	




Linux 2.6 内核阅读笔记 信号,古老的榕树,5-wow.com

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。