二、实验代码 1
下面是为 SIGINT 信号设置自定义信号处理句柄的一个简单例子,默认情况下,按下 Ctrl-C 组合键会产生 SIGINT 信号。
#include <signal.h> #include <stdio.h> #include <unistd.h> void handler(int sig) { // 信号处理句柄 printf("This signal is %d\n", sig); (void) signal(SIGINT, SIG_DFL); // 恢复 SIGINT 信号的默认处理句柄。(实际上内核会 // 自动恢复默认值,但对于其他系统未必如此) } int main() { (void) signal(SIGINT, SIG_DFL); // 设置 SIGINT 的用户自定义信号处理句柄 while (1) { printf("Signal test.\n)"; sleep(1); // 等待 1 秒钟 } }
库函数 sa_restorer
其中,信号处理函数 handler() 会在信号 SIGINT 出现时被调用执行。该函数首先输出一条信息,然后会把 SIGINT 信号的处理过程设置成默认信号处理句柄。因此在第二次按下 Ctrl-C 组合键时,SIG_DFL 会让该程序结束运行。
那么 sa_restorer 这个函数是从哪里来的呢?其实它是由函数库提供的。在 Linux 的 Libc 2.2.2 函数库文件(misc/子目录)中有它的函数,定义如下:
.globl ____sig_restore .globl ____masksig_restore # 若没有 blocked 则使用这个 restorer 函数 ____sig_restore: addl $4, %esp # 丢弃信号值 signr popl %eax # 恢复系统调用返回值 popl %ecx # 恢复原用户程序寄存器值 popl %edx popfl # 恢复用户程序时的标志寄存器 ret # 若有 blocked 则使用下面这个 restorer 函数, blocked 供 ssetmask 使用 ____masksig_restore: addl $4, %esp # 丢弃信号值 signr call ____ssetmask # 设置信号屏蔽码 old blocking addl $4, %esp # 丢弃 blocked 值 popl %eax popl %ecx popl %edx popfl ret
该函数的主要作用是为了信号处理程序结束后,恢复用户程序执行系统调用后的返回值和一些寄存器内容,并清除作为信号处理程序参数的信号值 signr 。
在编译连接用户自定义的信号处理函数,编译程序会调用 Libc 库中信号系统调用函数把 sa_restorer() 函数插入到用户程序中。库文件中信号系统调用的函数实现见如下所示。
#define __LIBRARY__ #include <unistd.h> extern void ____sig_restore(); extern void ____masksig_restore(); // 库函数中用户调用的 signal() 包裹函数 void (*signal(int sig, __sighandler_t func))(int) { void (*res)(); register int __fooebx __asm__("bx") = sig; __asm__("int $0x80" : "=a"(res) : "0"(__NR_signal), "r"(__fooebx), "c"(func), "d"((long)____sig_restore)); return res; } // 用户调用的 sigaction() 函数 int sigaction(int sig, struct sigaction *sa, struct sigaction *old) { register int __fooebx __asm__("bx") = sig; if (sa->sa_flags & SA_NOMASK) sa->sa_restorer = ____sig_restore; else sa->sa_restorer = ____masksig_restore; __asm__("int $0x80" : "=a"(sig) : "0"(__NR_sigaction), "r"(__fooebx), "c"(sa), "d"(old)); if (sig >= 0) return 0; errno = -sig; return -1; };
sa_restorer() 函数负责清理在信号处理程序执行完后恢复用户程序的寄存器值和系统调用返回值,就好像没有运行过信号处理程序,而是直接从系统调用中返回的。
最后说明一下执行的流程。在 do_signal() 执行完后,system_call.s 将会把进程内核态堆栈上 eip 以下的所有值弹出堆栈。在执行了 iret 指令之后,CPU 将把内核态堆栈上的 cs:eip、eflags 以及 ss:esp 弹出,恢复到用户态去执行程序。(系统调用进入可参考 系统调用进入,系统调用返回可参考 系统调用返回。)由于 eip 已经被替换为指向信号句柄,因此,此刻即会立即执行用户自定义的信号处理程序。在该信号处理程序执行完后,通过 ret 指令, CPU 会把控制权移交给 sa_restorer 所指向的恢复程序去执行。而 sa_restorer 程序会做一些用户态堆栈的清理工作,也即会跳过堆栈上的信号值 signr,并把系统调用后的返回值 eax 和寄存器 ecx、edx 以及标志寄存器 eflags 弹出,完全恢复了系统调用后各寄存器和 CPU 的状态。最后通过 sa_restorer 的 ret 指令弹出原用户程序的 eip(也即堆栈上的 old_eip),返回去执行用户程序。
三、实验代码 2
这里采用一个关于"信号的发送、接收以及处理"的实例来介绍对系统以及进程处理信号的过程。其包含两个用户进程。一个进程用来接收及处理信号,名字叫做 processsig。它所对应的程序源代码如下 :
// processsig .cpp #include <signal.h> #include <stdio.h> void sig_usr(int signo) { // 处理信号的函数 if (signo == SIGUSR1) printf("received SIGUSR1\n"); else printf("received %d\n", signo); signal(SIGUSR1, sig_usr); // 重新设置 processsig 进程的信号处理函数指针, // 以便下次使用 } int main(int argc, char **argv) { signal(SIGUSR1, sig_usr); // 挂接 processsig 进程的信号处理函数指针 for (;;) pause(); return 0; }
// sendsig.cpp #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> int main(int argc, char **argv) { int pid, ret, signo; int i; if (argc != 3) { printf("Usage: sendsig <signo> <pid>\n"); return -1; } signo = atoi(argv[1]); pid = atoi(argv[2]); ret = kill(pid, signo); for (i = 0; i < 1000000; i++) { if (ret != 0) printf("send signal error\n"); } return 0; }
1、终端执行
终端中执行如下命令:
./processsig & ./sendsig 10 160 # 10 代表 SIGUSR1 这个信号, # 160 是 processsig 进程的进程号
四、内核背后实现
1、processsig 进程开始执行
processsig 进程开始执行,要为接收信号做准备,具体表现为,指定对哪种信号进行什么样的处理。为此,进入 main() 函数后,先要将用户自定义的信号处理函数与 **processsig ** 进程绑定。用户程序是通过调用 signal() 函数来实现这个绑定的。这个函数是库函数,它执行后会产生软中断 INT 0x80,兵映射到 sys_signal() 这个系统调用函数去执行。 sys_signal() 函数的功能是将用户自定义的信号处理函数 sig_usr() 与 **processsig ** 进程绑定。这意味着,只要 **processsig ** 进程接收到 SIGUSR1 信号,就调用 sig_usr 函数来处理该信号,绑定工作就是通过该函数来完成的。
进入 sys_signal() 函数后,系统先要在绑定之前检测用户指定的信号是否符合规定。由于 Linux 0.11 中只能默认处理 32 种信号,而且默认忽略 SIGKILL 这个信号,所以只要用户给出的信号不符合这些要求,系统将不能处理。执行代码如下:
sys_signal 函数
// kernel/signal.c // signal()系统调用。类似于 sigaction()。为指定的信号安装新的信号句柄(信号处理程序)。 // 信号句柄可以是用户指定的函数,也可以是 SIG_DFL(默认句柄)或 SIG_IGN(忽略)。 // 参数 signum --指定的信号; handler -- 指定的向柄; restorer - 恢复函数指针,该函数由 // Libc 库提供。用于在信号处理程序结束后恢复系统调用返回时几个寄存器的原有值以及系统 // 调用的返回值,就好象系统调用没有执行过信号处理程序而直接返回到用户程序一样。 // 函数返回原信号句柄。 int sys_signal(int signum, long handler, long restorer) { struct sigaction tmp; // 首先验证信号值在有效范围(1--32)内,并且不得是信号 SIGKILL(和 SIGSTOP)。因为这 // 两个信号不能被进程捕获。 if (signum<1 || signum>32 || signum==SIGKILL) return -1; // 然后根据提供的参数组构建 sigaction 结构内容。sa_handler 是指定的信号处理句柄(函数)。 // sa_mask 是执行信号处理句柄时的信号屏蔽码。sa_flags 是执行时的一些标志组合。这里设定。 // 该信号处理句柄只使用 1 次后就恢复到默认值,并允许信号在自己的处理句柄中收到。 tmp.sa_handler = (void (*)(int)) handler; tmp.sa_mask = 0; tmp.sa_flags = SA_ONESHOT | SA_NOMASK; tmp.sa_restorer = (void (*)(void)) restorer; // 保存恢复处理函数指针。 // 接着取该信号原来的处理句柄,并设置该信号的 sigaction 结构。最后返回原信号句柄。 handler = (long) current->sigaction[signum-1].sa_handler; current->sigaction[signum-1] = tmp; return handler; }
执行完 sys_signal 函数后,其示意图为:
2、processsig 进程进入可中断等待状态
在 **processsig ** 进程的程序中,为了体现信号对进程执行状态的影响,我们特意调用了 pause() 函数。这个函数最终将导致该进程被设置为“可中断等待状态”。等到该进程接收到信号后,它的状态将由 “可中断等待状态” 转换为 “就绪态” 。
3、sendsig 进程开始执行并向 processsig 进程发信号
**processsig ** 进程暂时挂起,sendsig 进程执行。 sendsig 进程就会给 **processsig ** 进程发送信号,然后切换到 **processsig ** 进程去执行。
sendsig 进程会先执行
ret = kill(pid, signo);
这一行代码,其中 kill 是个库函数,最终会映射到 sys_kill 函数中去执行,并将参照 “10” 和 “160” 这两个参数给 **processsig ** 进程发送 SIGUSR1 信号,执行代码如下:
sys_kill 函数
// 文件路径 kernel/exit.c /* * XXX need to check permissions needed to send signals to process * groups, etc. etc. kill() permissions semantics are tricky! */ /* * 为了向进程组等发送信号,XXX 需要检查许可。kill()的许可机制非常巧妙! */ // 系统调用 kill() 可用于向任何进程或进程组发送任何信号,而并非只是杀死进程。 // 参数 pid 是进程号; sig 是需要发送的信号。 // 如果 pid 值>0, 则信号被发送给进程号是 pid 的进程。 // 如果 pid=0, 那么信号就会被发送给当前进程的进程组中的所有进程。 // 如果 pid=-1,则信号 sig 就会发送给除第一个进程(初始进程 init)外的所有进程。 // 如果 pid < -1,则信号 sig 将发送给进程组-pid 的所有进程。 // 如果信号 sig 为 0,则不发送信号,但仍会进行错误检查。如果成功则返回 0。 // 该函数扫描任务数组表,并根据 pid 的值对满足条件的进程发送指定的信号 sig。若 pid 等于 0, // 表明 当前进程是进程组组长,因此需要向所有组内的进程强制发送信号 sig。 int sys_kill(int pid,int sig) { struct task_struct **p = NR_TASKS + task; int err, retval = 0; 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; } // 文件路径 kernel/exit.c 向指定任务 p 发送信号 sig,权限为 priv。 // 参数:sig - 信号值; p - 指定任务的指针:priv - 强制发送信号的标志。即不需要考虑进程 // 用户属性或级别而能发送信号的权利。该函数首先判断参数的正确性,然后判断条件是否满足。 // 如果满足就向指定进程发送信号 sig 并退出,否则返回未许可错误号。 static inline int send_sig(long sig,struct task_struct * p,int priv) { // 若信号不正确或任务指针为空则出错退出。 if (!p || sig<1 || sig>32) return -EINVAL; // 如果强制发送标志置位,或者当前进程的有效用户标识符(euid)就是指定进程的 euid(也即是自己), // 或者当前进程是超级用户,则向进程 p 发送信号 sig,即在进程 p 位图中添加该信号, 否则出错退出。 // 其中 suser()定义为(current->euid==0),用于判断是否是超级用户。 if (priv || (current->euid==p->euid) || suser()) p->signal |= (1<<(sig-1)); else return -EPERM; return 0; } // 文件路径 include/linux/kernel.h #define suser() (current->euid == 0) // 判断是否是超级用户
将 SIGUSR1 信号发送给 **processsig ** 进程之后,就返回 sendsig 用户进程空间内继续执行,随着时钟中断不断产生,sendsig 进程的时间片将被消减为 0, 导致进程切换,schedule() 函数开始执行(可参考:2、schedule 函数)。
// 文件路径 kernel/sched.c void schedule(void) { // ... for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) if (*p) { if ((*p)->alarm && (*p)->alarm < jiffies) { (*p)->signal |= (1<<(SIGALRM-1)); (*p)->alarm = 0; } /* * 遍历到 processsig 进程后,检测到其接收的信号,此时 * processsig 进程还是可中断等待状态, * 将 processsig 进程设置为就绪态 */ if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) && (*p)->state==TASK_INTERRUPTIBLE) (*p)->state=TASK_RUNNING; } // ... }
接下来进行第二次遍历时,就会切换到 **processsig ** 进程去执行,执行代码如下:
schedule 函数
// 文件路径 kernel/sched.c void schedule(void) { // ... while (1) { c = -1; next = 0; i = NR_TASKS; p = &task[NR_TASKS]; while (--i) { if (!*--p) continue; if ((*p)->state == TASK_RUNNING && (*p)->counter > c) c = (*p)->counter, next = i; // 这时候 processsig 进程已经就绪了 } if (c) break; for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) if (*p) (*p)->counter = ((*p)->counter >> 1) + (*p)->priority; } switch_to(next); // 切换到 processsig 进程去执行 }
4、系统检测当前进程接收到信号并准备处理
processsig 进程开始执行后,会继续在 for 循环中执行 pause() 函数。由于这个函数最终会映射到 sys_pause() 这个系统调用函数中去执行,所以当系统调用返回时,就一定会执行到 ret_from_sys_call: 标号处,并最终调用 do_signal() 函数,开始着手处理 processsig 进程的信号。执行代码如下:
# 文件路径 kernel/system_call.s # ... ret_from_sys_call: movl current,%eax # task[0] cannot have signals cmpl task,%eax 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 notl %ecx andl %ebx,%ecx bsfl %ecx,%ecx je 3f btrl %ecx,%ebx movl %ebx,signal(%eax) incl %ecx pushl %ecx call do_signal # 准备处理信号 # ...
5、do_signal 函数构建用户堆栈,以调用信号处理函数
现在开始介绍信号处理之前的准备工作。
进入 do_signal() 函数后,先要对 processsig 进程的信号处理函数进行判定。我们知道, processsig 进程的信号处理函数指针被加载到了进程 task_struct 中的 sigaction[32] 结构中。
现在这个指针开始发挥作用,它指向了 processsig 进程的信号处理函数 sig_usr() 。
do_signal 函数
system_call 可参考:2、 system_call 函数
系统调用进入可参考 系统调用进入
系统调用返回可参考 系统调用返回
do_signal可参考本文 2.3 do_signal() 函数 段
// 文件路径:kernel/signal.c // 系统调用的中断处理程序中真正的信号预处理程序(在 kernel/system_call.s,119 行)。 // 该段代码的主要作用是将信号处理句柄插入到用户程序堆栈中,并在本系统调用结束返回。 // 后立刻执行信号句柄程序,然后继续执行用户的程序。这个函数处理比较粗略,尚不能处 // 理进程暂停 SIGSTOP 等信号。 // 函数的参数是进入系统调用处理程序 system_ca11.s 开始,直到调用本函数(system_ca11.s // 第 119 行)前逐步压入堆找的值。这些值包括(在 system_call.s 中的代码行): // 1) CPU 执行中断指令压入的用户找地址 ss 和 esp、标志寄存器 eflags 和返回地址 cs 和 eip; // 2) 第 83--88 行在刚进入 system_ca11 时压入栈的寄存器 ds、es、fs 和 edx、ecx、ebx; // 3) 第 95 行调用 sys_call_table 后压入栈中的相应系统调用处理函数的返回值(eax)。 // 4) 第 118 行压入栈中的当前处理的信号值(signr)。 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; // 即 current->sigaction[signr-1]。 unsigned long * tmp_esp; // 如果信号句柄为 SIG_IGN (1,默认忽略句柄) 则不对信号进行处理而直接返回;如果句柄为 // SIG_DFL(0,默认处理),则如果信号是 SIGCHLD 也直接返回,否则终止进程的执行。 // 句柄 SIG_IGN 被定义为 1,SIG_DFL 被定义为 0。参见 include/signal.h,第 45、46 行。 // 第 100 行 do_exit()的参数是返回码和程序提供的退出状态信息。可作为 wait()或 waitpid()函数 // 的状态信息。 参见 sys/wait.h 文件第 13--18 行。 waitQ 或 waitpid() 利用这些宏就可以取得子 // 进程的退出状态码或子进程终止的原因(信号)。 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)); // 不再返回到这里。 } // OK, 以下准备对信号句柄的调用设置。 如果该信号句柄只需使用一次,则将该句柄置空。 // 注意,该信号句柄已经保存在 sa_handler 指针中。 // 在系统调用进入内核时,用户程序返回地址(eip、cs)被保存在内核态栈中。下面这段代 // 码修改内核态堆找上用户调用系统调用时的代码指针 eip 为指向信号处理句柄,同时也将 // sa_restorer、signr、进程屏蔽码(如果 SA_NOMASK 没置位)、eax、ecx、edx 作为参数以及 // 原调用系统调用的程序返回指针及标志寄存器值压入用户堆栈。 因此在本次系统调用中断 // 返回用户程序时会首先执行用户的信号句柄程序,然后再继续执行用户程序。 if (sa->sa_flags & SA_ONESHOT) sa->sa_handler = NULL; // 将内核态栈上用户调用系统调用下一条代码指令指针 eip 指向该信号处理句柄。由于 C 函数 // 是传值函数,因此给 eip 赋值时需要使用"*(&eip)"的形式。 另外,如果允许信号自己的 // 处理句柄收到信号自己,则也需要将进程的阻塞码压入堆栈。 // 这里请注意,使用如下方式(第 104 行)对普通 C 函数参数进行修改是不起作用的。因为当 // 函数返回时堆栈上的参数将会被调用者丢弃。这里之所以可以使用这种方式,是因为该函数 // 是从汇编程序中被调用的,并且在函数返回后汇编程序并没有把调用 do_signal ()时的所有 // 参数都丢弃。eip 等仍然在堆栈中。 // sigaction 结构的 sa_mask 字段给出了在当前信号句柄(信号描述符)程序执行期间应该被 // 屏蔽的信号集。同时,引起本信号句柄执行的信号也会被屏蔽。不过若 sa_flags 中使用了 // SA_NOMASK 标志,那么引起本信号句柄执行的信号将不会被屏蔽掉。如果允许信号自己的处 // 理句柄程序收到信号自己,则也需要将进程的信号阻塞码压入堆栈。 // 此处为代码的第 104 行 *(&eip) = sa_handler; longs = (sa->sa_flags & SA_NOMASK)?7:8; // 将原调用程序的用户堆栈指针向下扩展7(或 8)个长字(用来存放调用信号句柄的参数等), // 并检查内存使用情况(例如如果内存超界则分配新页等)。 *(&esp) -= longs; verify_area(esp,longs*4); // 在用户堆栈中从下到上存放 sa_restorer、信号 signr、屏蔽码 blocked(如果 SA_NOMASK // 置位)、eax、ecx、edx、eflags 和用户程序原代码指针。 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; // 进程阻塞码(屏蔽码)添上 sa_mask 中的码位。 }
do_signal 函数主要工作是对用户栈中的数据进行调整,使得此次系统调用返回后会 “首先” 执行 processsig 进程的 “信号处理函数”,然后从用户进程 “中断位置” 继续执行。即在 pause() 函数执行后产生 int 0x80 软中断的下一条指令处就是这个用户进程的 “中断位置” (当然,如果不需要处理信号,直接返回 “中断位置” 处就可以了,但现在要先处理信号问题,再回 “中断位置” )。
系统调用返回后,就会到 processsig 进程的 sig_usr 函数处执行,处理信号,函数执行结束后,会执行 “ret” 指令。 ret 的本质就是用当时保存在栈中的 EIP 的值来恢复 EIP 寄存器,跳转到 EIP 指向的地址位置去执行。。于是此时处于栈顶的 sa->sa_restorer 所代表的函数地址值就发挥作用了,此时就应该跳转到 sa->sa_restorer 所代表的函数地址值位置去执行了。restorer 函数见本章 库函数 sa_restorer
五、signal.c 中其它函数
// 程序路径:linux/kernel/signal.c #include <linux/sched.h> // 调度程序头文件,定义了任务结构 task_struct、初始任务 0 的数据, // 还有一些有关描述符参数设置和获取的嵌入式汇编函数宏语句。 #include <linux/kernel.h> // 内核头文件。含有一些内核常用函数的原形定义。 #include <asm/segment.h> // 段操作头文件。定义了有关段寄存器操作的联入式汇编函数。 #include <signal.h> // 信号头文件。定义信号符号常量,信号结构以及信号操作函数原型。 // 下面函数名前的关键字 volatile 用于告诉编译器 gcc 该函数不会返回。这样可让 gcc 产生更好一 // 些的代码,更重要的是使用这个关键字可以避免产生某些(未初始化变量的)假警告信息。 // 等同于现在 gcc 的函数属性说明:void do_exit(int error_code) __attribute__((noreturn)); volatile void do exit(int error_code); // 获取当前任务信号屏蔽位图(屏蔽码或阻塞码)。sgetmask 可分解为 signal-get-mask。以下类似。 int sys_sgetmask() { return current->blocked; } // 设置新的信号屏蔽位图。SIGKILL 不能被屏蔽。返回值是原信号屏蔽位图。 int sys_ssetmask(int newmask) { int old=current->blocked; current->blocked = newmask & ~(1<<(SIGKILL-1)); return old; } // 复制 sigaction 数据到 fs 数据段 to 处。即从内核空间复制到用户(任务)数据段中。 static inline void save_old(char * from,char * to) { int i; // 首先验证 to 处的内存空间是否足够大。然后把一个 sigaction 结构信息复制到 fs 段(用户) // 空间中。宏函数 put_fs_byte() 在 include/asm/segment.h 中实现。 verify_area(to, sizeof(struct sigaction)); for (i=0 ; i< sizeof(struct sigaction) ; i++) { put_fs_byte(*from,to); from++; to++; } } // 把 sigaction 数据从 fs 数据段 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++); }
sys_sigaction 函数
// 程序路径:linux/kernel/signal.c // sigaction()系统调用。改变进程在收到一个信号时的操作。signum 是除了 SIGKILL 以外的。 // 任何信号。[如果新操作(action)不为空 ]则新操作被安装。如果 oldaction 指针不为空, // 则原操作被保留到 oldaction。成功则返回 0,否则为-1。 int sys_sigaction(int signum, const struct sigaction * action, struct sigaction * oldaction) { struct sigaction tmp; // 信号值要在(1-32)范围内,并且信号 SIGKILL 的处理句柄不能被改变。 if (signum<1 || signum>32 || signum==SIGKILL) return -1; // 在信号的 sigaction 结构中设置新的操作(动作)。如果 oldaction 指针不为空的话,则将。 // 原操作指针保存到 oldaction 所指的位置。 tmp = current->sigaction[signum-1]; get_new((char *) action, (char *) (signum-1+current->sigaction)); if (oldaction) save_old((char *) &tmp,(char *) oldaction); // 如果允许信号在自己的信号句柄中收到,则令屏蔽码为 0,否则设置屏蔽本信号。 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; }
六、exit.c 中其它函数
描述
该程序主要描述了进程(任务)终止和退出的有关处理事宜。主要包含进程释放、会话(进程组)终止和程序退出处理函数以及杀死进程、终止进程、挂起进程等系统调用函数。还包括进程信号发送函数 send_sig() 和通知父进程子进程终止的函数 tell_father()。
释放进程的函数 release() 主要根据指定的任务数据结构(任务描述符)指针,在任务数组中删除指定的进程指针、释放相关内存页,并立刻让内核重新调度任务的运行。
进程组终止函数 kill_session() 通过向会话号与当前进程相同的进程发送挂断进程的信号。
系统调用 sys_kill() 用于向进程发送任何指定的信号。根据参数 pid(进程标识号)不同的数值,该系统调用会向不同的进程或进程组发送信号。程序注释中已经列出了各种不同情况的处理方式。
程序退出处理函数 do_exit() 是在 exit 系统调用的中断处理程序中被调用。它首先会释放当前进程的代码段和数据段所占的内存页面。如果当前进程有子进程,就将子进程的 father 置为 1,即把子进程的父进程改为进程 1(init 进程)。如果该子进程已经处于僵死状态,则向进程 1 发送子进程终止信号 SIGCHLD。接着关闭当前进程打开的所有文件、释放使用的终端设备、协处理器设备,若当前进程是进程组的领头进程,则还需要终止所有相关进程。随后把当前进程置为僵死状态,设置退出码,并向其父进程发送子进程终止信号 SIGCHLD。最后让内核重新调度任务的运行。
系统调用 waitpid() 用于挂起当前进程,直到 pid 指定的子进程退出(终止)或者收到要求终止该进程的信号,或者是需要调用一个信号句柄(信号处理程序)。如果 pid 所指的子进程早已退出(已成所谓的僵死进程),则本调用将立刻返回。子进程使用的所有资源将释放。该函数的具体操作也要根据其参数进行不同的处理。详见代码中的相关注释。
代码注释
// 文件路径 kernel/exit.c 释放指定进程占用的任务槽及其任务数据结构占用的内存页面。 // 参数 p 是任务数据结构指针。该函数在后面的 sys_kill() 和 sys_waitpid() 函数中被调用。 // 扫描任务指针数组表 task[] 以寻找指定的任务。如果找到,则首先清空该任务槽,然后释放 // 该任务数据结构所占用的内存页面,最后执行调度函数并在返回时立即退出。如果在任务数组 // 表中没有找到指定任务对应的项,则内核 panic 。 void release(struct task_struct * p) { int i; if (!p) // 如果进程数据结构指针是 NULL,则什么也不做,退出。 return; for (i=1 ; i<NR_TASKS ; i++) // 扫描任务数组,寻找指定任务。 if (task[i]==p) { task[i]=NULL; // 置空该任务项并释放相关内存页。 free_page((long)p); schedule(); // 重新调度(似乎没有必要)。 return; } panic("trying to release non-existent task"); //指定任务若不存在则死机。 } 终止会话(session)。 // 进程会话的概念请参见第 7 章中有关进程组和会话的说明。 static void kill_session(void) { struct task_struct **p = NR_TASKS + task; // 指针*p 首先指向任务数组最末端。 // 扫描任务指针数组,对于所有的任务(除任务 0 以外),如果其会话号 session 等于当前进程的 // 会话号就向它发送挂断进程信号 SIGHUP。 while (--p > &FIRST_TASK) { if (*p && (*p)->session == current->session) (*p)->signal |= 1<<(SIGHUP-1); // 发送挂断进程信号。 } } 通知父进程 - 向进程 pid 发送信号 SIGCHLD:默认情况下子进程将停止或终止。 // 如果没有找到父进程,则自己释放。但根据 POSIX.1 要求,若父进程已先行终止,则子进程应该 // 被初始进程 1 收容。 static void tell_father(int pid) { int i; if (pid) // 扫描进程数组表,寻找指定进程 pid,并向其发送子进程将停止或终止信号 SIGCHLD。 for (i=0;i<NR_TASKS;i++) { if (!task[i]) continue; if (task[i]->pid != pid) continue; task[i]->signal |= (1<<(SIGCHLD-1)); return; } /* if we don't find any fathers, we just release ourselves */ /* This is not really OK. Must change it to make father 1 */ /* 如果没有找到父进程,则进程就自己释放。这样做并不好,必须改成由进程 1 充当其父进程。*/ printk("BAD BAD - no father found\n\r"); release(current); // 如果没有找到父进程,则自己释放。 }
do_exit 函数
// 文件路径 kernel/exit.c 程序退出处理函数。在下面 137 行处的系统调用处理函数 sys_exit()中被调用。 // 该函数将把当前进程置为 TASK_ZOMBIE 状态,然后去执行调度函数 schedule(),不再返回。 // 参数 code 是退出状态码,或称为错误码。 int do_exit(long code) { int i; // 首先释放当前进程代码段和数据段所占的内存页。 函数 free_page_tables() 的第 1 个参数 // (get_base()返回值)指明在 CPU 线性地址空间中起始基地址,第 2 个(get_limit()返回值) // 说明欲释放的字节长度值。get_base()宏中的 current->ldt[1]给出进程代码段描述符的位置 // (current->ldt[2] 给出进程代码段描述符的位置);get_limit()中的 0x0f 是进程代码段的 // 选择符(0x17 是进程数据段的选择符)。即在取段基地址时使用该段的描述符所处地址作为 // 参数,取段长度时使用该段的选择符作为参数。 free_page_tables() 函数位于 mm/memory.c // 文件的 105 行,get_base() 和 get_limit() 宏位于 include/linux/sched.h 头文件的 213 行处。 free_page_tables(get_base(current->ldt[1]),get_limit(0x0f)); free_page_tables(get_base(current->ldt[2]),get_limit(0x17)); /// 如果当前进程有子进程,就将子进程的 father 置为 1(其父进程改为进程 1,即 init 进程)。 // 如果该子进程已经处于僵死(ZOMBIE)状态,则向进程 1 发送子进程终止信号 SIGCHLD。 for (i=0 ; i<NR_TASKS ; i++) if (task[i] && task[i]->father == current->pid) { task[i]->father = 1; if (task[i]->state == TASK_ZOMBIE) /* assumption task[1] is always init */ /* 这里假设 task[1]肯定是进程 init */ (void) send_sig(SIGCHLD, task[1], 1); } // 关闭当前进程打开着的所有文件。 for (i=0 ; i<NR_OPEN ; i++) if (current->filp[i]) sys_close(i); /// 对当前进程的工作目录 pwd、根目录 root 以及执行程序文件的 i 节点进行同步操作,放回各个 // i 节点并分别置空(释放)。 iput(current->pwd); current->pwd=NULL; iput(current->root); current->root=NULL; iput(current->executable); current->executable=NULL; // 如果当前进程是会话头领(leader)进程并且其有控制终端,则释放该终端。 if (current->leader && current->tty >= 0) tty_table[current->tty].pgrp = 0; // 如果当前进程上次使用过协处理器,则将 last_task_used_math 置空。 if (last_task_used_math == current) last_task_used_math = NULL; // 如果当前进程是 leader 进程,则终止该会话的所有相关进程。 if (current->leader) kill_session(); // 把当前进程置为僵死状态,表明当前进程已经释放了资源。并保存将由父进程读取的退出码。 current->state = TASK_ZOMBIE; current->exit_code = code; // 通知父进程,也即向父进程发送信号 SIGCHLD -- 子进程将停止或终止。 tell_father(current->father); schedule(); // 重新调度进程运行,以让父进程处理僵死进程其他的善后事宜。 // 下面 return 语句仅用于去掉警告信息。因为这个函数不返回,所以若在函数名前加关键字 // volatile,就可以告诉 gcc 编译器本函数不会返回的特殊情况。这样可让 gcc 产生更好一 // 些的代码,并且可以不用再写这条 return 语句也不会产生假警告信息。 return (-1); /* just to suppress warnings */ }
sys_exit 函数
系统调用 exit()。终止进程。 // 参数 error_code 是用户程序提供的退出状态信息,只有低字节有效。把 error_code 左移 8 // 比特是 wait() 或 waitpid()函数的要求。低字节中将用来保存 wait()的状态信息。例如, // 如果进程处于暂停状态(TASK_STOPPED),那么其低字节就等于 0x7f。参见 sys/wait.h // 文件第 13--18 行。 wait( 或 waitpid() 利用这些宏就可以取得子进程的退出状态码或子 // 进程终止的原因(信号)。 int sys_exit(int error_code) { return do_exit((error_code&0xff)<<8); }
sys_waitpid函数
系统调 系统调用 waitpid()。挂起当前进程,直到 pid 指定的子进程退出(终止)或者收到要求终止 /// 该进程的信号,或者是需要调用一个信号句柄(信号处理程序)。如果 pid 所指的子进程早已 // 退出(已成所谓的僵死进程),则本调用将立刻返回。子进程使用的所有资源将释放。 // 如果 pid > 0, 表示等待进程号等于 pid 的子进程。 // 如果 pid = 0, 表示等待进程组号等于当前进程组号的任何子进程。 // 如果 pid < -1,表示等待进程组号等于 pid 绝对值的任何子进程。 // 如果 pid = -1,表示等待任何子进程。 // 若 options = WUNTRACED,表示如果子进程是停止的,也马上返回(无须跟踪)。 // 若 options = WNOHANG,表示如果没有子进程退出或终止就马上返回。 // 如果返回状态指针 stat_addr 不为空,则就将状态信息保存到那里。 // 参数 pid 是进程号;*stat_addr 是保存状态信息位置的指针;options 是 waitpid 选项。 int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options) { int flag, code; // flag 标志用于后面表示所选出的子进程处于就绪或睡眠态。 struct task_struct ** p; verify_area(stat_addr,4); repeat: flag=0; // 从任务数组末端开始扫描所有任务,跳过空项、本进程项以及非当前进程的子进程项。 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) { if (!*p || *p == current) continue; if ((*p)->father != current->pid) continue; // 此时扫描选择到的进程 p 肯定是当前进程的子进程。 // 如果等待的子进程号 pid>0,但与被扫描子进程 p 的 pid 不相等,说明它是当前进程另外的子 // 进程,于是跳过该进程,接着扫描下一个进程。 if (pid>0) { if ((*p)->pid != pid) continue; // 否则,如果指定等待进程的 pid=0,表示正在等待进程组号等于当前进程组号的任何子进程。 // 如果此时被扫描进程 p 的进程组号与当前进程的组号不等,则跳过。 } else if (!pid) { if ((*p)->pgrp != current->pgrp) continue; // 否则,如果指定的 pid<-1,表示正在等待进程组号等于 pid 绝对值的任何子进程。如果此时 // 被扫描进程 p 的组号与 pid 的绝对值不等,则跳过。 } else if (pid != -1) { if ((*p)->pgrp != -pid) continue; } // 如果前 3 个对 pid 的判断都不符合,则表示当前进程正在等待其任何子进程,也即 pid =-1 // 的情况。此时所选择到的进程 p 或者是其进程号等于指定 pid,或者是当前进程组中的任何 // 子进程,或者是进程号等于指定 pid 绝对值的子进程,或者是任何子进程(此时指定的 pid // 等于-1)。接下来根据这个子进程 p 所处的状态来处理。 switch ((*p)->state) { // 子进程 p 处于停止状态时,如果此时 WUNTRACED 标志没有置位,表示程序无须立刻返回, // 于是继续扫描处理其他进程。如果 WUNTRACED 置位,则把状态信息 0x7f 放入*stat_addr, // 并立刻返回子进程号 pid。这里 0x7f 表示的返回状态使 WIFSTOPPED()宏为真。 // 参见 include/sys/wait.h, 14 行。 case TASK_STOPPED: if (!(options & WUNTRACED)) continue; put_fs_long(0x7f,stat_addr); return (*p)->pid; // 如果子进程 p 处于僵死状态,则首先把它在用户态和内核态运行的时间分别累计到当前进程 // (父进程)中,然后取出子进程的 pid 和退出码,并释放该子进程。最后返回子进程的退出 // 码和 pid。 case TASK_ZOMBIE: current->cutime += (*p)->utime; current->cstime += (*p)->stime; flag = (*p)->pid; // 临时保存子进程 pid。 code = (*p)->exit_code; // 取子进程的退出码。 release(*p); // 释放该子进程。 put_fs_long(code,stat_addr); // 置状态信息为退出码值。 return flag; // 返回子进程的 pid. // 如果这个子进程 p 的状态既不是停止也不是僵死,那么就置 flag=1。表示找到过一个符合 // 要求的子进程,但是它处于运行态或睡眠态。 default: flag=1; continue; } } // 在上面对任务数组扫描结束后,如果 flag 被置位,,说明有符合等待要求的子进程并没有处 // 于退出或僵死状态。如果此时已设置 WNOHANG 选项(表示若没有子进程处于退出或终止态就 // 立刻返回),就立刻返回 0,退出。 否则把当前进程置为可中断等待状态并重新执行调度。 // 当又开始执行本进程时, 如果本进程没有收到除 SIGCHLD 以外的信号,则还是重复处理。 // 否则,返回出错码'中断的系统调用'并退出。针对这个出错号用户程序应该再继续调用本 // 函数等待子进程。 if (flag) { if (options & WNOHANG) // 若 options = WNOHANG,则立刻返回。 return 0; current->state=TASK_INTERRUPTIBLE; // 置当前进程为可中断等待状态。 schedule(); // 重新调度。 if (!(current->signal &= ~(1<<(SIGCHLD-1)))) goto repeat; else return -EINTR; // 返回出错码(中断的系统调用)。 } // 若没有找到符合要求的子进程,则返回出错码(子进程不存在)。 return -ECHILD; }
七、进程信号说明
进程中的信号是用于进程之间通信的一种简单消息,通常是下表中的一个标号数值,并且不携带任何其他的信息。例如当一个子进程终止或结束时,就会产生一个标号为 18 的 SIGCHILD 信号发送给父进程,以通知父进程有关子进程的当前状态。
关于一个进程如何处理收到的信号,一般有两种做法:一是程序的进程不去处理,此时该信号会由系统相应的默认信号处理程序进行处理;第二种做法是进程使用自己的信号处理程序来处理信号。Linux 0.11 内核所支持的信号见表 8-4 所示。