系列文章目录
Linux 0.11启动过程分析(一)
Linux 0.11 fork 函数(二)
Linux0.11 缺页处理(三)
Linux0.11 根文件系统挂载(四)
Linux0.11 文件打开open函数(五)
Linux0.11 execve函数(六)
Linux0.11 80X86知识(七)
Linux0.11 内核体系结构(八)
Linux0.11 系统调用进程创建与执行(九)
Linux0.11 进程切换(十)
Linux0.11 管道(十一)
Linux0.11 信号(十二)
文章目录
- 系列文章目录
- 前言
- 一、信号说明
- 1、信号支持的机制
- 2、signal.c 文件说明
- 2.1 signal 函数
- 2.2 sigaction 函数
- 2.3 do_signal() 函数
- 二、实验代码 1
- 库函数 sa_restorer
- 三、实验代码 2
- 1、终端执行
- 四、内核背后实现
- 1、processsig 进程开始执行
- sys_signal 函数
- 2、processsig 进程进入可中断等待状态
- 3、sendsig 进程开始执行并向 processsig 进程发信号
- sys_kill 函数
- schedule 函数
- 4、系统检测当前进程接收到信号并准备处理
- 5、do_signal 函数构建用户堆栈,以调用信号处理函数
- do_signal 函数
- 五、signal.c 中其它函数
- sys_sigaction 函数
- 六、exit.c 中其它函数
- 描述
- 代码注释
- do_exit 函数
- sys_exit 函数
- sys_waitpid函数
- 七、进程信号说明
前言
信号机制是 Linux 0.11 为进程提供的一套"局部的类中断机制",即在进程执行的过程
中,如果系统发现某个进程接收到了信号,就暂时打断进程的执行,转而去执行该进程的信
号处理程序,处理完毕后,再从进程"被打断"之处继续执行。
一、信号说明
1、信号支持的机制
系统需要具备以下三个功能,以支持信号机制。
- 系统要支持进程对信号的发送和接收
系统在每个进程 task_struct 中都设置了用以接收信号的数据成员 signal(信号位图),每个进程接收到的信号就"按位"存储在这个数据结构中。系统支持两种方式给进程发送信号:- 一种方式是一个进程通过调用特定的库函数给另一个进程发送信号;
- 另一种方式是用户通过键盘输入信息产生键盘中断后,中断服务程程序给进程发送信号。
这两种方式的信号发送原理是相同的,都是通过设置信号位图(signal)上的信号位来实现的。本实例将结合第一种方式,即一个进程给另一个进程发送信号来展现系统对信号的发送和接收。
- 系统要能够及时检测到进程接收到的信号
系统通过两种方式来检测进程是否接收到信号 :- 一种方式是在系统调用返回之前检测当前进程是否接收到信号;
- 另一种方式是时钟中断产生后,其中断服务程序执行结束之前检测当前进程是否接收到信号。
这两种信号检测方式大体类似。本实例将结合第一种方式来展现系统对进程接收到的信号的检测。
- 系统要支持进程对信号进行处理
系统要能够保证,当用户进程不需要处理信号时,信号处理函数完全不参与用户进程的执行;当用户进程需要处理信号时,进程的程序将暂时停止执行,转而去执行信号处理函数,待信号处理函数执行完毕后,进程程序将从"暂停的现场处"继续执行。
2、signal.c 文件说明
signal.c 程序涉及内核中所有有关信号处理的函数。在 UNIX 系统中,信号是一种"软件中断"处理机制。有许多较为复杂的程序会使用到信号。信号机制提供了一种处理异步事件的方法。例如,用户在终端键盘上键入 ctrl-C 组合键来终止一个程序序的执行。该操作就会产生一个 SIGINT(Signal Interrupt)信号,并被发送到当前前台执行的进程中;当进程设置的一个报警时钟到期时,系统就会向进程发送一个 SIGALRM 信号;当发生硬件异常时,系统也会向正在执行的进程发送相应的信号。另外,一个进程也可以向另一个进程发送信号。例如使用 kill() 函数向同组的子进程发送终止执行信号。
信号处理机制在很早的 UNIX 系统中就已经有了,但那些早期 UNIX 内核中信号处理的方法并不是那么可靠。信号可能会被丢失,而且在处理紧要区域代码时进程有时很难关闭一个指定的信号,后来 POSIX 提供了一种可靠处理信号的方法。为了保持兼容性,本程序中还是提供了两种处理信号的方法。
在内核代码中通常使用一个无符号长整数(32 位)中的比特位来表示各种不同信号。因此最多可表示 32 个不同的信号。在本版 Linux 内核中,定义了 22 种不同的信号。其中 20 种信号是 POSIX.1 标准中规定的所有信号,另外 2 种是 Linux 的专用信号:SIGUNUSED(未定义)和 SIGSTKFLT(堆栈错),前者可表示系统目前还不支持的所有其他信号种类。这 22 种信号的具体名称和定义可参考程序后的信号列表,也可参阅 include/signal.h 头文件。
对于进程来说,当收到一个信号时,可以由三种不同的处理或操作方式。
- 忽略该信号。大多数信号都可以被进程忽略。但有两个信号忽略不掉:SIGKILL 和 SIGSTOP。其原因是为了向超级用户提供一个确定的方法来终止或停止指定的任何进程。另外,若忽略掉某些硬件异常而产生的信号(例如被 0 除),则进程的行为或状态就可能变得不可知了。
- 捕获该信号。为了进行该操作,我们必须首先告诉内核在指定的信号发生时调用我们自定义的信号处理函数。在该处理函数中,我们可以做任何操作,当然也可以什么不做,起到忽略该信号的同样作用。自定义信号处理函数来捕获信号的一个例子是:如果我们在程序执行过程中创建了一些临时文件,那么我们就可以定义一个函数来捕获 SIGTERM(终止执行)信号,并在该函数中做一些清理临时文件的工作。SIGTERM 信号是 kill 命令发送的默认信号。
- 执行默认操作。内核为每种信号都提供一种默认操作。通常这些默认操作就是终止进程的执行。参
见程序后信号列表中的说明。
本程序给出了:(1)设置和获取进程信号阻塞码(屏蔽码)系统调用函数 sys_ssetmask() 和 sys_sgetmask() ,(2)信号处理系统调用 sys_signal() (即传统信号处理函数 signal()),(3)修改进程在收到特定信号时所采取的行动的系统调用 sys_sigaction()(既可靠信号处理函数 sigaction()),(4)以及在系统调用中断处理程序中处理信号的函数 do_signal()。有关信号操作的发送信号函数 send_sig() 和通知父进程函数 tell_father() 则被包含在另一个程序(exit.c)中。程序中的名称前缀 sig 均是信号 signal 的简称。
2.1 signal 函数
signal()和 sigaction()的功能比较类似,都是更改信号原处理句柄(handler ,或称为处理程序)。但 signal() 就是内核操作上述传统信号处理的方式,在某些特殊时刻可能会造成信号丢失。当用户想对特定信号使用自己的信号处理程序(信号句柄)时,需要使用 signal() 或 sigaction() 系统调用首先在进程自己的任务数据结构中设置 sigaction[] 结构数组项,把自身信号处理程序的指针和一些属性"记录"在该结构项中。当内核在退出一个系统调用和某些中断过程时会检测当前进程是否收到信号。若收到了用户指定的特定信号,内核就会根据进程任务数据结构中 sigaction[] 中对应信号的结构项执行用户自己定义的信号处理服务程序。
// 在 include/signal.h 头文件第 55 行上,signal()函数原型声明如下
void (*signal(int signr, void (*handler)(int)))(int);
这个 signal() 函数有两个参数。一个指定需要捕获的信号 signr;另外一个是新的信号处理函数指针(新的信号处理句柄)void (*handler)(int)。新的信号处理句柄是一个无返回值且具有一个整型参数的函数指针,该整型参数用于当指定信号发生时内核将其传递给处理句柄。
signal() 函数的原型声明看上去比较复杂,但是若我们定义一个如下类型:
typedef void sigfunc(int);
那么我们可以把 signal()函数的原型改写成下面的简单样子:
sigfunc *signal(int signr, sigfunc *handler);
signal() 函数会给信号值是 signr 的信号安装一个新的信号处理函数句柄 handler,该信号句柄可以是用户指定的一个信号处理函数,也可以是内核提供的特定的函数指针 SIG_IGN 或 SIG_DFL。
当指定的信号到来时,如果相关的信号处理句柄被设置成 SIG_IGN,那么该信号就会被忽略掉。如果信号句柄是 SIG_DFL,那么就会执行该信号的默认操作。否则,如果信号句柄被设置成用户的一个信号处理函数,那么内核首先会把该信号句柄被复位成其默认句柄,或者会执行与实现相关的信号阻塞操作,然后会调用执行指定的信号处理函数。
signal() 函数会返回原信号处理句柄,这个返回的句柄也是一个无返回值且具有一个整型参数的函数指针。并且在新句柄被调用执行过一次后,信号处理句柄又会被恢复成默认处理句柄值 SIG_DFL。
在 include/signal.h 文件中(第 45 行起),默认句柄 SIG_DFL 和忽略处理句柄 SIG_IGN 的定义是:
#define SIG_DFL ((void (*)(int))0) /* default signal handling */
#define SIG_IGN ((void (*)(int))1) /* ignore signal */
都分别表示无返回值的函数指针,与 signal() 函数中第二个参数的要求相同。指针值分别是 0 和 1。这两个指针值逻辑上讲是实际程序中不可能出现的函数地址值。因此在 signal() 函数中就可以根据这两个特殊的指针值来判断是否使用默认信号处理句柄或忽略对信号的处理(当然 SIGKILL 和 SIGSTOP 是不能被忽略的)。参见下面程序列表中第 94-98 行的处理过程。
当一个程序被执行时,系统会设置其处理所有信号的方式为 SIG_DFL 或 SIG_IGN。另外,当程序 fork() 一个子进程时,子进程会继承父进程的信号处理方式(信号屏蔽码)。因此父进程对信号的设置和处理方式在子进程中同样有效。
为了能连续地捕获一个指定的信号,signal() 函数的通常使用方式例子如下。
void sig_handler(int signr) { // 信号句柄。
signal(SIGINT, sig_handler); // 为处理下一次信号发生而重新设置自己的处理句柄。
// ...
}
void main() {
signal(SIGINT, sig_handler); // 主程序中设置自己的信号处理句柄。
}
signal() 函数不可靠的原因在于当信号已经发生而进入自己设置的信号处理函数中,但在重新再一次设置自己的处理句柄之前,在这段时间内有可能又有一个信号发生。但是此时系统已经把处理句柄设置成默认值。因此就有可能造成信号丢失。
2.2 sigaction 函数
sigaction() 函数采用了 sigaction 数据结构来保存指定信号的信息,它是一种可靠的内核处理信号的机制,它可以让我们方便地查看或修改指定信号的处理句柄。该函数是 signal() 函数的一个超集。该函数在 include/signal.h 头文件(第 66 行)中的声明为:
int sigaction(int sig, struct sigaction *act, struct sigaction *oldact);
其中参数 sig 是我们需要查看或修改其信号处理句柄的信号,后两个参数是 sigaction 结构的指针。当参数 act 指针不是 NULL 时,就可以根据 act 结构中的信息修改指定信号的行为。当 oldact 不为空时,内核就会在该结构中返回信号原来的设置信息。sigaction 结构见如下所示:
// include/signal.h
struct sigaction {
void (*sa_handler)(int); // 信号处理句柄。
sigset_t sa_mask; // 信号的屏蔽码,可以阻塞指定的信号集。
int sa_flags; // 信号选项标志。
void (*sa_restorer)(void); // 信号恢复函数指针(系统内部使用)。
};
当修改一个信号的处理方法时,如果处理句柄 sa_sandler 不是默认处理句柄 SIG_DFL 或忽略处理句柄 SIG_IGN,那么在 sa_handler 处理句柄可被调用前,sa_mask 字段就指定了需要加入到进程信号屏蔽位图中的一个信号集。如果信号处理句柄返回,系统就会恢复进程原来的信号屏蔽位图。这样在一个信号句柄被调用时,我们就可以阻塞指定的一些信号。当信号句柄被调用时,新的信号屏蔽位图会自动地把当前发送的信号包括进去,阻塞该信号的继续发送。从而在我们处理一指定信号期间能确保阻塞同一个信号而不让其丢失,直到此此次处理完毕。另外,在一个信号被阻塞期间而又多次发生时通常只保存其一个样例,也即在阻塞解除时对于阻塞的多个同一信号只会再调用一次信号处理句柄。在我们修改了一个信号的处理句柄之后,除非再次更改,否则就一直使用该处理句柄。这与传统的 signal() 函数不一样。signal() 函数会在一处理句柄结束后将其恢复成信号的默认处理句柄。
sigaction 结构中的 sa_flags 用于指定其他一些处理信号的选项,这些选项的定义请参见 include/signal.h 文件中(第 36-39 行)的说明。
sigaction 结构中的最后一个字段和 sys_signal() 函数的参数 restorer 是一函数指针。它在编译连接程序时由 Libc 函数库提供,用于在信号处理程序结束后清理用户态堆栈,并恢复系统调用存放在 eax 中的返回值,见下面详细说明。
2.3 do_signal() 函数
do_signal() 函数是内核系统调用(int 0x80)中断处理程序中对信号的预处理程序。在进程每次调用系统调用或者发生时钟等中断时,若进程已收到信号,则该函数就会把信号的处理句柄(即对应的信号处理函数)插入到用户程序堆栈中。这样,在当前系统调用结束返回后就会立刻执行信号句柄程序,然后再继续执行用户的程序,见图 8-10 所示。
在把信号处理程序的参数插入到用户堆栈中之前,do_signal() 函数首先会把用户程序堆栈指针向下扩展 longs 个长字(参见下面程序中 106 行),然后将相关的参数添入其中,参见图 8-11 所示。由于 do_signal() 函数从 104 行开始的代码比较难以理解,下面我们将对其进行详细描述。
在用户程序调用系统调用刚进入内核时,该进程的内核态堆栈上会由 CPU 自动压入如图 8-11 中所示的内容,也即:用户程序的 SS 和 ESP 以及用户程序中下一条指令的执行点位置 CS 和 EIP。在处理完此次指定的系统调用功能并准备调用 do_signal() 时(也即 system_call.s 程序 118 行之后,系统调用响应函数之后,返回用户态之前调用),内核态堆栈中的内容见图 8-12 中左边所示。因此 do_signal() 的参数即是这些在内核态堆栈上的内容。
在 do_signal() 处理完两个默认信号句柄(SIG_IGN 和 SIG_DFL)之后,若用户自定义了信号处理程序(信号句柄 sa_handler),则从 104 行起 do_signal() 开始准备把用户自定义的句柄插入用户态堆栈中。它首先把内核态堆栈中原用户程序的返回执行点指针 eip 保存为 old_eip 变量中,然后将该 eip 替换成指向自定义句柄 sa_handler,也即让图中内核态堆栈中的 eip 指向 sa_handler。接下来通过把内核态中保存的 “原 esp” 减去 longs (longs 是一个变量)值,把用户态堆栈向下扩展了 7 或 8 个长字空间。最后把内核堆栈上的一些寄存器内容复制到了这个空间中,见图中右边所示。
总共往用户态堆栈上放置了 7 到 8 个值,我们现在来说明这些值的含义以及放置这些值的原因。
old_eip 即是原用户程序的返回地址,它是在内核堆栈上 eip 被替换成信号句柄地址之前保留下来的。 eflags、edx 和 ecx 是原用户程序在调用系统调用之前的值,基本上也是调用系统调用的参数,在系统调用返回后仍然需要恢复这些用户程序的寄存器值。eax 中保存有系统调用的返回值。如果所处理的信号还允许收到本身,则堆栈上还存放有该进程的阻塞码 blocked。下一个是信号 signr 值。
最后一个是信号活动恢复函数的指针 sa_restorer。这个恢复函数不是由用户设定的,因为在用户定义 signal() 函数时只提供了一个信号值 signr 和一个信号处理句柄 handler。
系统所在的这些操作都是为了在用户堆栈上模拟一个调用信号处理函数的堆栈,用来执行信号处理函数,然后返回到原调用处。
二、实验代码 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 所示。