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 内核阅读笔记 信号,布布扣,bubuko.com

时间: 2025-01-03 21:53:03

Linux 2.6 内核阅读笔记 信号的相关文章

Linux 2.6 内核阅读笔记 中断和异常

2014年7月24日 中断门.陷阱门及中断门 中断是可以禁止的,可以通过告诉PIC停止对某个中断的发布.被禁止的中断是不会丢失的,在解除禁止后又会发送到CPU上. 禁止中断和屏蔽(mask)中断的不同之处是屏蔽是忽略掉某个中断,而禁止相当于延迟发送. Intel提供了三种类型的中断描述符:任务门.中断门及陷阱门描述.linux使用与inten稍有不同的细分分类和术语,把他们进行如下分类: 中断门:用户态进程不能访问的一个intel中断门(DPL为0),所有的linux中断处理程序都通过中断门在内

Linux 2.6 内核阅读笔记 内存管理

2014年7月29日 buddy分配算法 内核需要为分配一组连续的页框提供一种健壮.高效的分配策略.分配连续的页框必须解决内存管理中的外碎片(external fragmentation).频繁的请求和释放不同大小的一组连续页框,必然导致分配页框的块分算来许多小块的空闲页框无法被一次性大量分配使用. linux内核采用著名的伙伴系统算法来解决外碎片问题.该算法的核心思想是把所有的空闲页框分成11个链块表.每个链块表的大小分别为1,2,4,8,16,32,64,128,256,512和1024个连

Linux 2.6 内核阅读笔记 内核同步

2014年7月26日 内核抢占和内核控制路径的设计 内核抢占的一种定义:如果进程正在内核态执行内核函数时,允许发生内核切换(就是被替换的进程是内核函数所在进程),这个内核就是抢占的. linux内核提供了内核抢占的开启和关闭功能,在current_thread_info的preempt_count字段大于0时,内核就是不能抢占的.可以通过preempt_disable和preempt_enable来增加这个字段来控制这个开关. 中断和软中断和抢占的控制主要使用到current_thread_in

Linux概念与体系阅读笔记

[Linux概念与体系教程http://www.cnblogs.com/vamei/archive/2012/10/10/2718229.html] 1.Linux开机启动(bootstrap) 启动顺序:BIOS -> MBR -> boot loader -> kernel -> init process -> login BIOS:Basic Input/Output System MBR :Master Boot Record 2.Linux文件管理 (1)文件附件信

《Linux权威指南》阅读笔记(4)

第十五章  TCP/IP和PPP <DNS与BIND>,见转帖的读书笔记. TCP和UDP数据单位被称为分组包(packet),包头中指定了目的和源端口地址. 互联网协议(IP)在协议层次中位于TCP和UDP下面,将TCP和UDP分组包封装在另一个分组包中(被称为IP报文),

《Linux权威指南》阅读笔记(3)

第十三章 程序设计语言 Linux共享函数库使用一种叫跳跃表格(jump table)的数据结构 gcc -o 执行链接,指定文件名,-c不执行链接,-O优化,-g调试信息放入目标文件, 建立和使用静态函数库的方法: 先gcc -c将含目标函数源码(多个文件也可以)编译为.o文件,然后用ar程序 ar -r 建立.a静态库, 然后用ranlib命令产生索引,然后为该函数库建立头文件 gcc  -I../include -L../lib  -o  wibble  wibble.c  -lstuff

Linux kernel源码阅读笔记2-2.6版本调度器sched.c功能

来自:http://www.ibm.com/developerworks/cn/linux/l-scheduler/ 2.6 版本调度器的源代码都很好地封装到了 /usr/src/linux/kernel/sched.c 文件中.我们在表 1 中对在这个文件中可以找到的一些有用的函数进行了总结. 表 1. Linux 2.6 调度器的功能 函数名 函数说明 schedule 调度器主函数.调度优先级最高的任务执行. load_balance 检查 CPU,查看是否存在不均衡的情况,如果不均衡,就

Linux内核中的信号机制--一个简单的例子【转】

本文转载自:http://blog.csdn.net/ce123_zhouwei/article/details/8562958 Linux内核中的信号机制--一个简单的例子 Author:ce123(http://blog.csdn.NET/ce123) 信号机制是类UNIX系统中的一种重要的进程间通信手段之一.我们经常使用信号来向一个进程发送一个简短的消息.例如:假设我们启动一个进程通过socket读取远程主机发送过来的网络数据包,此时由于网络因素当前主机还没有收到相应的数据,当前进程被设置

[转]linux内核分析笔记----内存管理

转自:http://blog.csdn.net/Baiduluckyboy/article/details/9667933 内存管理,不用多说,言简意赅.在内核里分配内存还真不是件容易的事情,根本上是因为内核不能想用户空间那样奢侈的使用内存. 先来说说内存管理.内核把物理页作为内存管理的基本单位.尽管处理器的最小可寻址单位通常是字,但是,内存管理单元MMU通常以页为单位进行处理.因此,从虚拟内存的交代来看,页就是最小单位.内核用struct  page(linux/mm.h)结构表示系统中的每个