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信号产生和处理的大致原理。