linux信号处理源码分析(基于linux0.11)

linux的信号处理时机在系统调用结束、时钟中断处理后,硬件中断处理后,等等。这里以fork系统调用函数为例子讲解这个过程。下面是fork函数的定义。

_syscall0(int,fork)

宏展开

#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
	// 输入输出都是eax,输入是系统调用函数在系统调用表的序号
	: "=a" (__res) \
	: "0" (__NR_##name)); \
if (__res >= 0) \
	return (type) __res; \
errno = -__res; \
return -1; \
}

假设我们有这样一个程序。

init main() {
	int pid = fork();
	return 0;
}

main初始化的用户栈结构
在这里插入图片描述
执行fork函数时的用户栈结构
在这里插入图片描述
fork调用,进入内核态时的内核栈结构。
在这里插入图片描述
我们从sched.c的sched_init函数中知道,中断号80对应的中断处理程序是system_call。该函数在system_call.s中定义。具体的分析可以看linux0.11系统调用过程和fork源码解析这篇文章。下面贴一下代码。

_system_call:
	// 比较参数,不合法的参数直接返回中断,错误码是-1
	cmpl $nr_system_calls-1,%eax
	ja bad_sys_call
	// 寄存器压栈,保存现场和用户传递的参数
	push %ds
	push %es
	push %fs
	// 执行系统调用的函数时用户传入的三个参数,右到左,ebx是第一个参数
	pushl %edx
	pushl %ecx		# push %ebx,%ecx,%edx as parameters
	pushl %ebx		# to the system call
	// 0x10是内核数据段的选择子
	movl $0x10,%edx		# set up ds,es to kernel space
	mov %dx,%ds
	mov %dx,%es
	movl $0x17,%edx		# fs points to local data space
	mov %dx,%fs
	// 根据参数,从系统表格里找到对应的函数,每个函数地址4个字节
	call _sys_call_table(,%eax,4)
	// 系统调用的返回值,压栈保存,因为下面需要用eax
	pushl %eax
	// 把当前进程的pcb地址赋值给eax
	movl _current,%eax
	// 判断当前进程状态,0是可执行,即判断当前进程是否可以继续执行
	cmpl $0,state(%eax)		# state
	// CMP结果为0则zf等于1,jne是zf为0则跳转,所以下面是当前进程state不为0,则跳转,即重新调度
	jne reschedule
	// 时间片用完则重新调度
	cmpl $0,counter(%eax)		# counter
	je reschedule
ret_from_sys_call:
	movl _current,%eax		# task[0] cannot have signals
	// 判断当前执行的进程是不是0号进程
	cmpl _task,%eax
	// 是的话跳到标签3
	je 3f
	cmpw $0x0f,CS(%esp)		# was old code segment supervisor ?
	jne 3f
	cmpw $0x17,OLDSS(%esp)		# was stack segment = 0x17 ?
	jne 3f
	// 把这两个字段赋值给寄存器
	movl signal(%eax),%ebx
	movl blocked(%eax),%ecx
	// 对block变量的值取反,即没有屏蔽的为变成1,表示需要处理的信号
	notl %ecx
	// 把收到的信号signal和没有屏蔽的信号,得到需要处理的信号,放到ecx中
	andl %ebx,%ecx
	/*
		Bit Scan Forward,如果ecx等于0,则zf等于1,否则zf是0
		从低位到高位扫描ecx,把等于第一个是1的位置写到ecx中,即第一位是1则位置是0
	*/
	bsfl %ecx,%ecx
	// zf=1即ecx是0则跳转,代表没有需要处理的信号则跳转
	je 3f
	// 把ebx的第ecx位清0,并把1移到CF,处理了该信号,清0
	btrl %ecx,%ebx
	
	movl %ebx,signal(%eax)
	// 当前需要处理的信号加1,因为ecx保存的是位置,位置是0开始的,信号是1-32
	incl %ecx
	// 入参压栈
	pushl %ecx
	// 执行信号处理函数
	call _do_signal
	popl %eax
3:	popl %eax
	popl %ebx
	popl %ecx
	popl %edx
	pop %fs
	pop %es
	pop %ds
	iret

我们直接从call _do_signal这里开始分析。这时候的内核栈结构是。
在这里插入图片描述
然后调用do_signal函数。

void do_signal(long signr,long eax, long ebx, long ecx, long edx,
	long fs, long es, long ds,
	long eip, long cs, long eflags,
	unsigned long * esp, long ss)
{
	unsigned long sa_handler;
	// 系统调用返回时下一句代码的地址
	long old_eip=eip;
	// 取得该信号对应的处理结构
	struct sigaction * sa = current->sigaction + signr - 1;
	int longs;
	unsigned long * tmp_esp;
	// 处理函数
	sa_handler = (unsigned long) sa->sa_handler;
	// 处理函数是忽略信号量
	if (sa_handler==1)
		return;
	// 没有处理函数又不是SIGCHLD信号则进程退出
	if (!sa_handler) {
		if (signr==SIGCHLD)
			return;
		else
			do_exit(1<<(signr-1));
	}
	// 该处理函数只处理一次信号,即只会执行一次,清空
	if (sa->sa_flags & SA_ONESHOT)
		sa->sa_handler = NULL;
	// 修改eip的值,即系统调用返回时从这执行
	*(&eip) = sa_handler;
	// SA_NOMASK即在执行当前信号的处理函数时屏蔽当前的信号,防止嵌套,不开启的时候,需要多压栈一个参数,见下面 
	longs = (sa->sa_flags & SA_NOMASK)?7:8;
	// 拓展用户栈栈顶
	*(&esp) -= longs;
	verify_area(esp,longs*4);
	// 不能修改esp,所以赋值一下
	tmp_esp=esp;
	// 压入用户栈,中断返回时执行
	put_fs_long((long) sa->sa_restorer,tmp_esp++);
	put_fs_long(signr,tmp_esp++);
	if (!(sa->sa_flags & SA_NOMASK))
		put_fs_long(current->blocked,tmp_esp++);
	put_fs_long(eax,tmp_esp++);
	put_fs_long(ecx,tmp_esp++);
	put_fs_long(edx,tmp_esp++);
	put_fs_long(eflags,tmp_esp++);
	// 用户程序的地址
	put_fs_long(old_eip,tmp_esp++);
	// 执行该信号处理函数时屏蔽某些信号
	current->blocked |= sa->sa_mask;
}

该函数的入参和内核栈一一对应。执行完后的内核栈。

我们回到system_call函数。do_signal后执行了以下代码。

 	popl %eax
3:	popl %eax
	popl %ebx
	popl %ecx
	popl %edx
	pop %fs
	pop %es
	pop %ds
	iret

这时候的内核栈是空的(iret指令和call指令的影响可以百度或者参考intel手册),用户栈结构是。iret指令会弹出ip寄存器的值,在do_signal函数里,已经把这ip的值改为sa_handler的地址(有点缓冲区溢出攻击的感觉),所以结束系统调用后,会执行sa_handler函数。这时候的用户栈是。
在这里插入图片描述
在执行sa_handler函数的时候,会执行

push ebp;
mov ebp,esp

这时候的用户栈是
在这里插入图片描述
sa_handler函数通过ebp+8得到第一个实参,即信号的数值。通过esp+4得到第一个局部变量的值。如此类推。sa_handler执行完后。会执行。

mov esp,ebp
pop ebp

执行完后的用户栈是。
在这里插入图片描述
然后在执行

ret

把sa_restorer函数的值压进ip寄存器。这时候的用户栈是。
在这里插入图片描述
接着执行sa_restorer。sa_restorer函数是库函数提供的。用于清除栈上的参数和正确返回用户进程要执行的下一条执行。
代码如下:

addl $4, %esp
popl %eax
popl %ecx
popl %edx
popfl // 即eflags寄存器
ret

执行完后的用户栈
在这里插入图片描述
我们发现这个栈结构和开始系统调用之前的结构是一样的。这时候回到_syscall0这个宏的代码里。把eax的值给_res变量。然后从_syscall0返回。同样是执行。

mov ebp, esp 
pop ebp

这时候的栈结构是。
在这里插入图片描述
再通过ret指令。回到main函数执行return 0。整个过程结束。信号处理的时机是在进程进行系统调用的时候。

我们看一下信号的发送到处理的整个流程。
假设通过kill系统调用给进程发送一个信号。

int sys_kill(int pid,int sig)
{
	struct task_struct **p = NR_TASKS + task;
	int err, retval = 0;
	// pid等于0则给当前进程的整个组发信号,大于0则给某个进程发信号,-1则给全部进程发,小于-1则给某个组发信号
	if (!pid) while (--p > &FIRST_TASK) {
		if (*p && (*p)->pgrp == current->pid) 
			if (err=send_sig(sig,*p,1))
				retval = err;
	} else if (pid>0) while (--p > &FIRST_TASK) {
		if (*p && (*p)->pid == pid) 
			if (err=send_sig(sig,*p,0))
				retval = err;
	} else if (pid == -1) while (--p > &FIRST_TASK)
		if (err = send_sig(sig,*p,0))
			retval = err;
	else while (--p > &FIRST_TASK)
		if (*p && (*p)->pgrp == -pid)
			if (err = send_sig(sig,*p,0))
				retval = err;
	return retval;
}
/*
  发送信号给进程sig是发送的信号,p是接收信号的进程,priv是权限,
  1是代表可以直接设置,比如给自己发信息,priv为0说明需要一定的权限
*/
static inline int send_sig(long sig,struct task_struct * p,int priv)
{
	if (!p || sig<1 || sig>32)
		return -EINVAL;
	// 这里使用euid,即进程设置了suid位的话,可以扩大权限,即拥有文件属主的权限
	if (priv || (current->euid==p->euid) || suser())
		p->signal |= (1<<(sig-1));
	else
		return -EPERM;
	return 0;
}

我们发现发送一个信号只是在进程的block字段打标记,然后结束kill系统调用。接着系统就会进入信号处理的流程。但是系统处理的只是当前进程的信号。比如a进程给b进程发信号。那这时候是处理a进程的信号。那b进程在的信号在什么被处理呢?
答案是:时钟中断。我们看一下时钟中断的主要代码。

_timer_interrupt:
	...
	call _do_timer		
	addl $4,%esp
	jmp ret_from_sys_call

do_timer函数里可能会进行进程调度,假设b进程被调度执行。那么jmp ret_from_sys_call后就会处理b进程的信号(参考ret_from_sys_call的代码)。

do_timer =// 当前进程的可用时间减一,不为0则接着执行,否则可能需要重新调度
	if ((--current->counter)>0) return;
	current->counter=0;
	// 是系统进程则继续执行
	if (!cpl) return;
	// 进程调度
	schedule();

这就是linux信号产生和处理的大致原理。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值