今天我们来看 kernel 目录下的 signal.c,在开始贴代码之前,先聊聊 Linux 中的信号机制
信号机制 基本概念 信号是 Linux 系统中最为古老的进程间通信机制。就如同日常生活中人与人之间通过互联网手段来联络沟通一样,有时需要让进程知道一些事件发生了,这样进程就能通过发生的事件类型做出相应的反应,信号就是进程间联络沟通的桥梁
信号发生的来源 上面的概念可能还是很抽象,来看个例子吧。
假设你在 Linux 终端执行了一个需要运行很久的程序,当你等了很久还是没反应想退出程序时,需要按下 Ctrl+C 来结束,这一按就会产生一个 SIGINT 信号发往当前正在运行的进程。进程收到该信号,就会去信号处理集中找到 SIGINT 对应的处理程序,也就是终止进程的处理程序,进而终止当前进程,返回命令行待输入的状态
言归正传,信号发生的具体来源:
硬件来源:刚举的例子就是一种硬件来源,信号由硬件驱动程序产生
软件来源:系统提供了一些发送信号的 API 函数,如 kill, raise, alarm, setitimer 等,这些信号由内核产生
进程对信号的响应及处理方式 当进程接收到一个信号时,有三种处理方式:
忽略:忽略信号,不作处理,但 SIGKILL 与 SIGSTOP 信号无法被忽略(高版本中)
执行默认操作:每个信号都有默认操作,大部分默认操作是终止进程
执行自定操作:使用系统提供的函数修改信号处理函数,达到用户自定义响应方式的目的,但 SIGKILL 与 SIGSTOP 信号的处理函数无法被修改(高版本中)
代码实现 修改信号处理函数的系统 API 是 signal,其原型为:
1 2 3 #include <signal.h> void (*signal(int _sig, void (*_func)(int )))(int );
参数:
_sig:信号值
_func:三种取值:
SIG_IGN:忽略信号
SIG_DFL:系统默认方式
自定信号处理函数指针
返回值:
成功返回之前的信号处理函数指针
失败返回 SIG_ERR
那现在就来写一个程序捕获 SIGINT 信号并实现自己的信号处理函数吧(高版本 Linux)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> #include <signal.h> #include <unistd.h> void mySigIntFunc (int signo) { printf ("You pressed Ctrl+C...but you can't stop me!HHHHHHH\n" ); } int main () { signal(SIGINT, mySigIntFunc); printf ("Pid: %d\n" , getpid()); while (1 ) {} return 0 ; }
运行起来:
现在按 Ctrl+C 就会调用我们自己写的函数 mySigIntFunc 了,因为这个函数没有退出程序的功能,所以 Ctrl+C 不会终止进程。如果想要将其终止掉,方法很多,我列出三种:
Ctrl + Z :发送 SIGTSTP 信号终止进程
Ctrl + \ :发送 SIGQUIT 信号终止进程
在另一个终端中键入 kill 21302 ,这里的 21302 是要终止进程的 pid,作用是发送 SIGKILL 信号将其终止(这就是无法修改 SIGKILL 处理方式的原因,需保留一个杀死进程的最终手段)
signal.c signal 系统调用 直观地把握了信号的效果后,再来深入它的实现细节,Linux 0.11 中只实现了 SIGKILL 信号无法被捕获
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int sys_signal (int signum, long handler, long restorer) { struct sigaction tmp ; if (signum<1 || signum>32 || signum==SIGKILL) return -1 ; tmp.sa_handler = (void (*)(int )) handler; tmp.sa_mask = 0 ; tmp.sa_flags = SA_ONESHOT | SA_NOMASK; tmp.sa_restorer = (void (*)(void )) restorer; handler = (long ) current->sigaction[signum-1 ].sa_handler; current->sigaction[signum-1 ] = tmp; return handler; }
该函数的三个参数分别为:
signum:信号值
handler:信号处理函数指针
restorer:恢复函数指针,该函数由 libc 库提供,用于在信号处理程序结束后恢复系统调用返回时几个寄存器的原有值以及系统调用的原返回值
各信号值及 sigaction 结构体定义在 signal.h 中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #define SIGHUP 1 #define SIGINT 2 #define SIGQUIT 3 #define SIGILL 4 #define SIGTRAP 5 #define SIGABRT 6 #define SIGIOT 6 #define SIGUNUSED 7 #define SIGFPE 8 #define SIGKILL 9 #define SIGUSR1 10 #define SIGSEGV 11 #define SIGUSR2 12 #define SIGPIPE 13 #define SIGALRM 14 #define SIGTERM 15 #define SIGSTKFLT 16 #define SIGCHLD 17 #define SIGCONT 18 #define SIGSTOP 19 #define SIGTSTP 20 #define SIGTTIN 21 #define SIGTTOU 22 #define SA_NOCLDSTOP 1 #define SA_NOMASK 0x40000000 #define SA_ONESHOT 0x80000000 #define SIG_DFL ((void (*)(int))0) #define SIG_IGN ((void (*)(int))1) typedef unsigned int sigset_t ;struct sigaction { void (*sa_handler)(int ); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void ); };
每个进程都有自己的 task_struct 任务结构体(定义在 include/linux/sched.h),结构体中就包含了 32 个这样的 sigaction 结构体(禁止套娃),每个 sigaction 结构体对应一个信号。当有信号到来时,CPU 就是通过当前任务 task_struct 中元素个数为 32 的 sigaction 结构体数组找到对应信号的处理方式的。当然 0.11 版本中,Linux 只实现了 22 个信号
0.11 版本中的信号处理流程 还记得在 kernel(一)介绍 system_call.s 部分时 ret_from_sys_call 的代码吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ret_from_sys_call: movl current,%eax cmpl task,%eax je 3f cmpw $0x0f ,CS(%esp) jne 3f cmpw $0x17 ,OLDSS(%esp) jne 3f movl signal (%eax) ,%ebx movl blocked (%eax) ,%ecx notl %ecx andl %ebx,%ecx bsfl %ecx,%ecx je 3f btrl %ecx,%ebx movl %ebx,signal (%eax) 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
进程每次调用系统调用或发生时钟等中断时,在结束部分都会来到这里,进行信号的处理。如果进程收到了信号,则 do_signal 函数就会把信号处理函数指针插入到用户堆栈 中。如此一来,当前 系统调用/中断 结束返回(iret)后就会立即执行信号处理函数(用户态下)。信号处理函数执行结束后,执行 ret 指令,执行流来到 sa_restorer 指向的恢复程序(sigaction 结构体最后一个字段,由 libc 提供),该程序将 CPU 及寄存器的状态恢复到系统调用后信号处理前 的状态,仿佛没有执行过信号处理函数一样。最后 sa_restorer 通过 ret 指令回到原用户程序继续执行,流程如图:
do_signal 的实现 do_signal 的参数是系统调用等压入的所有信息,其中 signr 是接收到的信号值,eax 是系统调用的返回值,其余都是保存的用户态的信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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 ; if (!sa_handler) { if (signr==SIGCHLD) return ; else do_exit(1 <<(signr-1 )); } if (sa->sa_flags & SA_ONESHOT) sa->sa_handler = NULL ; *(&eip) = sa_handler; longs = (sa->sa_flags & SA_NOMASK)?7 :8 ; *(&esp) -= longs; verify_area(esp,longs*4 ); 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; }
用两张图来总结一下 do_signal 带来的一些改变。执行前,内核堆栈与用户堆栈情况如下:
执行后,变成了这样:
sa_restorer 恢复函数 因此调用完 do_signal 并且 iret 中断返回后,就会到用户态去执行信号处理函数,且 esp 指向 sa_restorer。信号处理函数通过 ret 指令,就会去到 sa_restorer 执行恢复函数,该函数在 Libc-2.2.2 函数库中有定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /* 如果没有屏蔽码,使用该函数作为恢复函数 */ sig_restore: addl $4,%esp /* 丢弃 signr */ popl %eax /* 系统调用返回值还原到 eax */ popl %ecx /* 还原 ecx,edx */ popl %edx popfl /* 恢复 eflags */ ret /* 如果有屏蔽码,使用该函数 */ masksig_restore: addl $4,%esp call ssetmask /* 设置信号屏蔽码 */ addl $4,%esp /* 丢弃屏蔽码 */ popl %eax popl %ecx popl %edx popfl ret
ret 后回到原用户程序继续执行,一次信号处理就算完成了
signal.c 剩余部分 实际上,设置信号处理句柄的函数除了 signal 外,还有 sigaction。与 signal 不同的是,给 sigaction 传参时,除了信号值外,还需要两个 sigaction 结构体,一个用于设置新信号处理结构体,一个用于接收原信号处理结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 int sys_sigaction (int signum, const struct sigaction * action, struct sigaction * oldaction) { struct sigaction tmp ; if (signum<1 || signum>32 || signum==SIGKILL) return -1 ; tmp = current->sigaction[signum-1 ]; get_new((char *) action, (char *) (signum-1 +current->sigaction)); if (oldaction) save_old((char *) &tmp,(char *) oldaction); if (current->sigaction[signum-1 ].sa_flags & SA_NOMASK) current->sigaction[signum-1 ].sa_mask = 0 ; else current->sigaction[signum-1 ].sa_mask |= (1 <<(signum-1 )); return 0 ; } static inline void save_old (char * from,char * to) { int i; verify_area(to, sizeof (struct sigaction)); for (i=0 ; i< sizeof (struct sigaction) ; i++) { put_fs_byte(*from,to); from++; to++; } } static inline void get_new (char * from,char * to) { int i; for (i=0 ; i< sizeof (struct sigaction) ; i++) *(to++) = get_fs_byte(from++); }
余下的部分定义了一些系统调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <linux/sched.h> #include <linux/kernel.h> #include <asm/segment.h> #include <errno.h> #include <signal.h> volatile void do_exit (int error_code) ;int sys_sgetmask () { return current->blocked; } int sys_ssetmask (int newmask) { int old=current->blocked; current->blocked = newmask & ~(1 <<(SIGKILL-1 )); return old; } int sys_sigpending () { return -ENOSYS; } int sys_sigsuspend () { return -ENOSYS; }