[334页]
(-:这一小节很难理解。但我基本都理解了,哈哈。
1、为什么signal不可靠,而sigaction可靠;
2、 为什么系统调用会被打断?
3、 sys_signal,sys_sigaction,函数作用?
4、 do_signal,sa_restorer,函数作用?
5、 信号句柄是在用户态还是内核态处理?
6、 do_signal的orig_eax哪里赋值,eax哪里赋值?
7、signal和sigaction的函数在那个文件编写?
8、 信号句柄可以分为几类?答案是3类,那哪三类?
暂时想到那么问题哈。
8-7 signal.c程序
8-7-1 功能描述
(a)signal.c程序涉及内核中所有有关信号处理函数。在类UNIX系统中,信号是一种"软件中断"处理机制。有许多较为复杂的程序会使用信号。
信号机制提供了一种处理异步事件的方法。
例如:
(1)用户在终端键盘上键入ctrl-C组合键来终止一个程序的执行。该操作就会产生一个SIGINT(SIGnal INTerrupt)信号,并被发送到当前前台执行的进程中;
(2)当进程设置的一个警报时钟到期是,系统就会向进程发送一个SIGALRM信号;
(3)当发送硬件异常时,系统也会向正在执行的进程发送相应的信号。
(4)另外,一个进程也可以向另一个进程发送信号。例如使用kill()函数想同组的子进程发送终止执行信号。
(b)信号处理机制在很早的UNIX系统中就已经有了,但那些早期UNIX内核中信号处理的方法并不是那么可靠。信号可能会被丢失,而且在处理紧要区域代码时进程有时很难关闭一个指定的信号,后来POSIX提供了一种处理信号的可靠方法。为了保持兼容,本程序中还是提供了两种处理信号的方法。
©在内核代码中通常使用一个无符号长整数(32位)中的位来表示各种不同信号。因此最多可表示32个不同的信号。在Linux0.12内核中,定义了22种不同的信号。其中20中信号是POSIX.1标准中规定的所有信号,另外2中是Linux的专用信号:SIGUNUSED(未定义)和SIGSTKFLT(堆栈错),前者可表示系统目前还不支持的所有其他信号种类。这22中信号的具体名称和定义可参考程序后的信号列表,也可参阅include/signal.h头文件。
1、 信号处理
(1)忽略该信号。大多数信号都可以被进程忽略。但有两个信号忽略不掉:SIGKILL和SIGSTOP。其原因是为了给超级用户提供一个确定的方法来终止或停止指定的任何进程。另外,若忽略掉某些硬件异常而产生的信号(例如被0除),则进程的行为或状态就可能变得不可知了。
(2)捕获该信号。有2个参数,捕获的消息句柄+回调函数。
例子:
说明:SIGTERM信号是kill命令发送的默认信号。
SIGTERM(终止执行)+函数(动作:清理临时文件的工作。)
(3)执行默认操作。内核为每种信号都提供了一种默认操作。通常这些默认操作就是终止进程的执行。参见程序后信号列表中的说明。
(d)signal.c大部分函数的简单说明
(1)设置和获取进程信号阻塞码(屏蔽码)系统调用函数sys_ssetmask()和sys_sgetmask()
(2)信号处理系统调用sys_signal()(即传统信号处理函数signal())
(3)修改进程在收到特定信号时所采取的行动的系统调用sys_sigaction()(即可靠信号处理函数sigaction())
(4)系统调用中断处理程序中处理信号的函数do_signal()
(5)有关信号操作的发送函数send_sig()和通知父进程函数tell_father()则被包含在另一个程序(exit.c)
(e)signal()和sigaction()的功能比较类似,都是更改信号原处理句柄(handler,或称为处理程序)。但signal()就是内核操作上述传统信号处理的方式,在某些特殊时刻可能会造成信号丢失。当用户想对特定信号使用自己的信号处理程序(信号句柄)时,需要使用signal()或sigaction()系统调用首先在进程自己的任务数据结构中设置sigaciton[]结构数组项,把自己信号处理程序的指针和一些属性"记录"在该结构项
中。当内核在退出一个系统调用和某些中断过程时会检测当前进程是否收到信号。
若收到了用户指定的特定信号,内核就会根据进程任务数据结构中sigaction[]中对应信号的结构项执行用户自己定义的信号处理服务程序。
(f)signal()函数会给信号值是signr的信号安装一个新的信号处理函数句柄handler,该信号句柄可以是用户指定的一个信号处理函数,也可以是内核提供的特定的函数指针SIG_IGN或SIG_DFL。
当指定的信号到来时,
(1)信号句柄是SIG_IGN,那么该信号就会被忽略掉。
(2)信号句柄是SIG_DFL,那么就会执行该信号的默认操作。
(3)信号句柄被设置成用户的一个信号处理函数,那么内核首先会把该信号句柄复位成默认句柄,或者会执行与实现相关的信号阻塞操作,然后会调用指定的信号处理函数。
signal()函数会返回原信号处理句柄,这个返回的句柄也是一个无返回值且具有一个整型参数的函数指针。并且在新句柄被调用过一次后,信号处理句柄有会恢复成默认处理句柄值SIG_DFL。
连续地捕获一个指定的信号,signal()函数的通常使用方式例子如下:
void sig_handler(int signr)
{
signal(SIGINT,sig_handler);
}
main()
{
signal(SIGINT,sig_handler);
}
signal()函数不可靠的原因在于当信号已经发生而进入自己设置的信号处理函数中,但在重新设置自己的处理句柄之前,在这段时间内有可能又有一个信号发生。但是此时系统已经把处理句柄设置成默认值。因此就有可能造成信号丢失。
(g)sigaction()函数采用了sigaction数据结构来保存指定信号的信息,它是一种可靠的内核处理信号的机制,它可以让我们方便地查看或修改指定信号的处理句柄。
该函数是signal()函数的一个超集。该函数在include/signal.h头文件。(第66行)中的声明为:
int sigaction(int sig, struct sigaction *act, struct sigaction *oldact);
其中参数sig是我们需要查看或修改其信号处理句柄的信号,后两个参数是sigaction结构的指针。当参数act指针不是NULL时,就可以根据act结构中的信息修改指定信号的行为。当oldact不为空时,内核就会在该结构中返回信号原来的设置信息。
sigaction结构如下所示:
struct sigaction {
void (*sa_handler)(int); //信号处理句柄。
sigset_t sa_mask; //信号的屏蔽码,可以阻塞指定的信号集。
int sa_flags; //信号选项标志。
void (*sa_restorer)(void); //信号恢复函数指针(系统内部使用)。
};
当修改一个信号的处理方法时,如果处理句柄sa_handler不是默认处理句柄SIG_DFL或忽略处理句柄SIG_IGN,那么在sa_handler处理句柄可被调用前,sa_mask字段就指定了需要加入到进程信号屏蔽位图中的一个信号集。如果信号处理句柄返回,系统就会回复进程原来的信号屏蔽位图。这样在一个信号句柄被调用时,我们就可以阻塞指定的一些信号。当信号句柄被调用时,新的信号屏蔽位图会自动地把当前发送的信号包括进去,阻塞该信号的继续发送。
从而在我们处理一指定信号期间能确保阻塞同一个信号而不让其丢失,直到此次处理完毕。
另外,在一个信号被阻塞期间而又多次发生时通常只保存其一个样例,即在阻塞解除时对于阻塞的多个同一信号只会再调用一次信号处理句柄。
在我们修改了一个信号的处理句柄之后,除非再次更改,否则就一直使用处理句柄。这与传统的signal()函数不一样。signal()函数会在处理句柄结束后将其恢复成信号的默认处理句柄。
[caicai原因:signal() 的sa_flags置位 SA_ONESHOT]
sigaction结构中的最后一个字段和sys_signal()函数的参数restorer是一个函数指针。它在编译连接程序时由Libc函数库提供,用于在信号处理程序结束后清理用户态堆栈,并恢复系统调用存放在eax中的返回值。
do_signal()函数是内核系统调用(int 0x80)中断处理程序中对信号的预处理程序。在进程每次调用系统调用或者发生时钟等中断时,若进程已收到信号,则该函数就会把信号的处理句柄(即对于的信号处理函数)插入到用户程序堆栈中。这样,在当前系统调用结束返回后就会立刻执行信号句柄程序,然后继续执行用户的程序。
在把信号处理 程序的参数插入到用户堆栈中之前,do_signal()函数首先会把用户程序堆栈指针向下扩展longs个长字(参见下面程序中195行),然后将相关的参数添入其中,
在用户程序调用系统调用刚进入内核时,该进程的内核态堆栈上会由CPU自动压入如图8-11中所示的内容,即:用户程序的SS和ESP以及用户程序中下一条指令的执行点位置CS和EIP.在处理完毕此次指定的系统调用功能并准备调用do_signal()时,内核态堆栈中的内容如图8-12中左边所示。
在do_signal()处理完两个默认信号句柄之后,若用户自定义了信号处理程序,则从104行起do_signal()开始准备把用户自定义的句柄插入用户态堆栈中。
(1)它首先把内核态堆栈中原用户程序的返回点指针eip保存为old_eip,然后将该eip替换成指向自定义句柄
sa_handler,即让图中内核态堆栈中的eip指向sa_handler。
(2)接下来通过把内核态中保存的"原esp"减去longs值,把用户态堆栈向下扩展了7或8个长字空间。
(3)最后把内核堆栈上的一些寄存器内容复制到了这个空间中。
总共往用户态堆栈上放置了7到8个值,我们现在来说明这些值的含义以及放置这些指的原因:
(1)old_eip即是原用户程序的返回地址,它是在内核堆栈上eip被替换成信号句柄地址之前保留下来的。
(2)eflags、edx和ecx是原用户程序在调用系统调用之前的值,基本上也是调用系统调用的参数,在系统调用返回后仍然需要恢复这些用户程序的寄存器值。
(3)eax中保存了系统调用的返回值。
(4)如果所处理的信号还允许收到本身,则堆栈上还存放该进程的阻塞码blocked。
(5)下一是信号signr值。
(6)最后一个是信号获得恢复函数的指针sa_restorer。这个恢复函数不是由用户设定的,因为在用户定义signal()函数时值提供了一个信号值signr和一个信号处理句柄handler。
(h)sa_restorer这个函数是从哪里来的呢?其实它是由函数库提供的。在Linux的Libc-2.2.2函数库文件中
有它的函数,定义如下:
.globl __sig_restore
.globl __masksig_restore
#若没有blocked则使用这个restorer函数
__sig_restore:
add $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()函数插入到用户程序中。
库文件中信号系统调用的函数实现如下所示。
(i)在do_signal()执行完后,sys_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),返回去执行用户程序。
sys_suspend()系统调用用于临时把进程信号屏蔽码替换成参数中给定的set,然后挂起进程,直到 收到一个信号为止。该系统调用被声明为带有三个参数的如下形式:
int sys_sigsuspend(int restart, unsigned long old_mask,unsigned long set)
其中restart是一个标志。当第1次调用该系统调用时,它是0。并且在该函数中会把进程原来的阻塞码blocked保存起来(old_mask),并设置restart为非0值。因此当进程第2次调用该系统调用时,它就会恢复进程原来保存在old_mask中的阻塞码。
虽然该系统调用带有三个参数,但一般用户程序在调用该函数时过程仅使用带有一个set参数的如下形式:
int sigsuspend(unsigned long set )
看看赵老师给出该系统调用C库中的实现方式。
2、被信号中断的系统调用的重新启动
(a)如果进程在执行一个慢速系统调用而被阻塞期间收到了一个信号,那么这个系统调用就会被中断而不再继续执行。此时该系统调用会返回出错信息,相应的全局错误码变量errno被设置成EINTR,表示系统调用被信号中断。
例如:
(1)读写管道、终端设备以及网络设备时,如果所读数据不存在或者设备不能立刻接受数据,那么系统调用的调用程序将被一直阻塞着。因此对于一些慢速系统调用就可以在必要时使用信号来中断它们并返回到用户程序中。这也包括pause()和wait()等系统调用。
但在某些情况下并无必要让用户程序来亲自处理被中断的系统调用。因为有时用户并不知道设备是否为低速设备。如果编制的程序可以以交互方式运行,那么它可能会读写低速设备。如果在这中程序中捕捉信号,而系统并没有提供系统调用的自动重新启动功能,那么程序在每次读写系统调用时就需要对出错返回码进行检测。如果是被信号中断的就需要再次进行读写操作。
赵老师举了例子和代码。
为了让用户程序不必处理某些被中断的系统调用情况,在处理信号时引用了对某些被中断系统调用的重新启动(重新执行)功能。自动重新启动的系统调用包括:ioctl、read、write、wait和waitpid。其中前面3个系统调用只有对低速设备进行操作时才会被信号中断。而wait和waitpi在捕捉到信号时总是会被中断。
在处理信号时根据设置在sigaction结构中的标志,可以选择是否重新启动被中断的系统调用。在Linux0.12内核中,如果在sigaction结构中设置了SA_INTERRUPT标志(系统调用可中断),并且 相关信号不是SIGCONT、SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU,那么在系统调用执行时手段信号就会被中断。否则内核会自动重新执行被中断的系统调用。执行的方法是首先恢复调用系统调用时原来寄存器eax的值,然后把用户程序代码的执行指针eip回调两个字节,即让eip重新指向系统调用中断int 0x80指令。
对于目前的 Linux系统,标志SA_INTERRUPT已经弃置不用,取而代之的是具有相反含义的标志SA_RESTART,即在本信号处理句柄执行完毕需要重新启动被中断的系统调用。
8-7-2 代码注释
/*
* linux/kernel/signal.c
*
* (C) 1991 Linus Torvalds
*/
#include <linux/sched.h>
#include <linux/kernel.h>
#include <asm/segment.h>
#include <signal.h>
#include <errno.h>
//获取当前任务信号屏蔽位图(屏蔽码或阻塞码)。sgetmask可分解为signal-get-mask。以下类似。
int sys_sgetmask()
{
return current->blocked;
}
//设置新的信号屏蔽位图。信号SIGKILL和SIGSTOP不能被屏蔽。返回值是原信号屏蔽位图。
int sys_ssetmask(int newmask)
{
int old=current->blocked;
current->blocked = newmask & ~(1<<(SIGKILL-1)) & ~(1<<(SIGSTOP-1));
return old;
}
//检测并取得进程收到的但被屏蔽(阻塞)的信号。还未处理信号的位图将被放入set中。
int sys_sigpending(sigset_t *set)
{
/* 用还未处理并且被阻塞信号的位图填入set指针所指位置处 */
//首先验证进程提供的用户存储空间应有4个字节。然后把还未处理并且被阻塞的信号位图填入set
//指针所指位置处。
verify_area(set,4);
put_fs_long(current->blocked & current->signal, (unsigned long *)set);
return 0;
}
/* 自动地更换成新的信号屏蔽码,并等待信号的到来。
我们需要对系统调用(syscall)做一些处理。我们会从系统调用库接口取得某些信号。
注意,我们需要把调用规则与libc库中的子程序统一考虑。
"set"正是POSIX标准1003.1-1988的3.3.7节中所描述的信号屏蔽码sigmask。
其中认为类型sigset_t能够作为一个32位量传递。
"restart"中保持有重启指示标志。如果非0值,那么我们就设置原来的屏幕码,
并且正常返回。如果它为0,那么我们就把当前的屏蔽码保持在oldmask中
并且阻塞进程,知道收到任何一个信号为止。
*/
[343]
//该系统调用临时把进程信号屏蔽码替换成参数给定的set,然后挂起进程,
//直到收到一个信号为止。
//restart是一个被中断的系统调用重新启动标志。当第1次调用该系统调用时,它是0。并且
//在该函数中会把进程原来的阻塞码blocked保存起来(old_mask),并设置restart为非0
//值。因此当进程第2次调用该系统调用时,它就会恢复进程原来保存在old_mask中阻塞码。
int sys_sigsuspend(int restart, unsigned long old_mask, unsigned long set)
{
//pause()系统调用将导致调用它的进程进入睡眠状态,直到收到一个信号。该信号或者终止进程的
//执行,或者导致进程去执行相应的信号捕获函数。
extern int sys_pause(void);
//如果restart标志不为0,表示重新让程序运行起来。于是恢复前面保存在old_mask中的原进程
//阻塞码。并返回码-EINTR(系统调用被信号中断)。
if (restart) {
/* 我们正在重启启动系统调用 */
current->blocked = old_mask;
return -EINTR;
}
//否则表示restart标志的值是0.表示第1次调用。于是首先设置restart标志(置为1),保存
//进程当前阻塞码blocked到old_mask中,并把进程的阻塞码替换成set。然后调用pause()让
//进程休眠,等待信号的到来。当进程收到一个信号时,pause()就会返回,并且进程会去执行信号
//处理函数,然后本调用返回-ERESTARTNOINTR码退出。这个返回码说明在处理完信号后要求返回
//到本系统调用中继续运行,即本系统调用不会被中断。
/* 我们不是再次重新运行,那么就干活吧 */
*(&restart) = 1;
*(&old_mask) = current->blocked;
current->blocked = set;
(void) sys_pause(); /* return after a signal arrives */
return -ERESTARTNOINTR; /* handle the signal, and come back */
}
//复制sigaction数据到fs数据段to处。即从内核空间复制到用户(任务)数据段中。
static inline void save_old(char * from,char * to)
{
int i;
//首先验证to处的内存空间是否足够大。然后把一个sigaction结构信息复制到fs段(用户)
//空间中。宏函数put_fs_bute()在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++);
}
//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 || signum==SIGSTOP)
return -EINVAL;
//然后根据提供的参数数组建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;
}
//sigaction()系统调用。改变进程在收到一个信号时额操作。signum同上。
//[如果新操作(action)不为空]则新操作被安装。如果oldaction指针不为空,则原操作
//被保留到oldaction。成功则返回0,否则为-EINVAL。
int sys_sigaction(int signum, const struct sigaction * action,
struct sigaction * oldaction)
{
struct sigaction tmp;
if (signum<1 || signum>32 || signum==SIGKILL || signum==SIGSTOP)
return -EINVAL;
//在信号的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;
}
/*
在当前目录中产生的core dump映像文件的子程序。目前还没有实现。
*/
int core_dump(long signr)
{
return(0); /* We didn't do a dump */
}
//系统调用的中断处理程序中真正的信号预处理程序(在kernel/sys_call.s,119行)。这段代码
//的主要作用是将信号处理句柄插入到用户程序堆栈中,并在本系统调用结束返回后立刻执行信号句柄
//程序,然后继续执行用户的程序。
//函数的参数是进入系统调用处理程序sys_call.s开始,直到调用本函数(sys_call.s第125行)
//前逐步压入堆栈的值。这些值包括(在sys_call.s中的代码行):
//①CPU执行中断指令压入的用户栈地址ss和esp、标志寄存器eflags和返回地址cs和eip;
//②第85--91行在刚进入system_call时压入栈的段寄存器ds、es、fs以及寄存器eax
//(orig_eax)、edx、ecx和ebax的值;
//
int do_signal(long signr,long eax,long ebx, long ecx, long edx, long orig_eax,
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;
//即current->sigaction[signr-1]
struct sigaction * sa = current->sigaction + signr - 1;
int longs;
unsigned long * tmp_esp;
//以下是调试语句。当定义了notdef时会打印相关信息。
#ifdef notdef
printk("pid: %d, signr: %x, eax=%d, oeax = %d, int=%d\n",
current->pid, signr, eax, orig_eax,
sa->sa_flags & SA_INTERRUPT);
#endif
//如果不是系统调用而是其他中断执行过程中调用到本函数时,roig_eax值为-1.参见
//sys_call.s第144行等语句。因此当orig_eax不等于-1时,说明是在某个系统调用的
//最后调用了本函数。在kernel/exit.c的waitpie()函数中,如果收到了SIGCHLD信号,
//或者在读管道函数fs/pipe.c中,管道当前读数据但没有读到任何数据等情况下,进程收到了任何
//一个非阻塞的信号,则都会以-ERESTARTSYS返回值返回。它表示进程可以被中断,但是在继续执行
//后会重新启动系统调用。返回码-ERESTARTNOINTR说明在处理完信号后要求返回到原系统调用中
//继续运行,即系统调用不会被中断。参见前面第62行。
//因此下面语句说明如果是在系统调用中调用的本行,并且相应系统调用的返回码eax等于
//-ERESTARTSYS或-ERESTARTNOINTR时进行下面的处理(实际上还没有真正回到用户程序中)。
if ((orig_eax != -1) &&
((eax == -ERESTARTSYS) || (eax == -ERESTARTNOINTR))) {
//如果系统调用返回码是-ERESTARTSYS(重新启动系统调用),并且sigaction中含有标志
//SA_INTERRUPT(系统调用被信号中断后不重新启动系统调用)或者信号值小于SIGCONT或者
//信号值大于SIGTTOU(即信号不是SIGCONT、SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU),则
//修改系统调用的返回值为eax = -EINTR,即被信号中断的系统调用。
if ((eax == -ERESTARTSYS) && ((sa->sa_flags & SA_INTERRUPT) ||
signr < SIGCONT || signr > SIGTTOU))
*(&eax) = -EINTR;
else {
//否则就恢复进程寄存器eax在调用系统调用之前的值,并且把原程序指令指针回调2字节。即当返回
//用户程序时,让程序重新启动执行被信号中断的系统调用。
*(&eax) = orig_eax;
*(&eip) = old_eip -= 2;
}
}
sa_handler = (unsigned long) sa->sa_handler;
//如果信号句柄为SIG_IGN(1,默认忽略句柄)则不对信号进行处理而直接返回。
if (sa_handler==1)
return(1); /* Ignore, see if there are more signals... */
//如果句柄为SIG_DFL(0,默认处理),则根据具体的信号进行分别处理。
if (!sa_handler) {
switch (signr) {
case SIGCONT://如果信号是以下两个则也忽略并返回。
case SIGCHLD:
return(1); /* Ignore, ... */
//如果信号是以下4中信号之一,则把当前进程状态置为停止状态TASK_STOPPED。若当前进程父进程对
//SIGCHLD信号的sigaction处理标志SA_NOCLDSTOP(即当子进程停止执行或又继续执行
//时不要产生SIGCHLD信号)没有置为,那么就给父进程发送SIGCHLD信号。
case SIGSTOP:
case SIGTSTP:
case SIGTTIN:
case SIGTTOU:
current->state = TASK_STOPPED;
current->exit_code = signr;
if (!(current->p_pptr->sigaction[SIGCHLD-1].sa_flags &
SA_NOCLDSTOP))
current->p_pptr->signal |= (1<<(SIGCHLD-1));
return(1); /* Reschedule another event */
//如果信号是以下6种信号之一,那么若信号产生了core dump,则以退出码为signr|0x80调用
//do_exit()退出。否则退出码就是信号值。do_exit()的参数是返回码和程序提供的退出状态信息。
//可作为wait()或waitpid()函数的状态信息。参见sys/wait.h文件第13-18行。
//wait()或waitpid()利用这些宏就可以取得子进程的退出状态码或子进程终止的原因(信号)。
case SIGQUIT:
case SIGILL:
case SIGTRAP:
case SIGIOT:
case SIGFPE:
case SIGSEGV:
if (core_dump(signr))
do_exit(signr|0x80);
/* fall through */
default:
do_exit(signr);
}
}
/*
* 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)"的形式。另外,如果允许信号自己的处理句柄收到
//信号自己,则也需要将进程的阻塞码压入堆栈。
//这里请注意,使用如下方式(第193行)对普通C函数参数进行修噶是不起作用的。因为当函数返回
//时堆栈上的参数都会被调用者丢弃。这里之所以可以使用这种方式,是因为该函数是从汇编程序中被
//调用的 ,并且在函数返回后汇编程序并没有把调用do_signal()时的所有参数都丢弃。eip等仍然
//在堆栈中。
//sigaction结构的sa_mask字段给出了在当前信号句柄(信号描述符)程序需执行期间应该被屏蔽的
//信号集。同时,引起本信号句柄执行的信号也会被屏蔽。不过若sa_flags中使用了SA_NOMASK
//标志,那么引起本信号句柄执行的信号将不会被屏蔽掉。如果允许信号自己的处理句柄程序收到信号
//自己,则也需要将进程的信号阻塞码压入堆栈。
*(&eip) = sa_handler;//第193行
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中的码
return(0); /* Continue, execute handler */
}
8-7-3 进程信号说明
进程中的信号是用于进程之间通信的一种简单消息,通常是下表中的一个标号数值,并且 不携带任何其他的信号。例如当一个子进程终止或结束时,就会产生一个标号为18的SIGCHILD信号发送给父进程,以及通知父进程有关子进程的当前状态。
关于一个进程如何处理收到的信号,一般有两种做法:
(a)程序的纪念册不去处理,此时该信号会由系统相应的默认信号处理程序进行处理;
(b)进程使用自己的信号处理程序来处理了信号。
Linux0.12内核所支持的信号见表8-5。