sigaction()系统调用的实现较具代表性,它的主要功能为设置信号处理程序,其原型为:
int sys_sigaction(int signum, const struct
sigaction * action,
struct sigaction * oldaction)
其中,sigaction数据结构在include/asm/signal.h中定义,其格式为:
struct sigaction {
__sighandler_t sa_handler;
sigset_t sa_mask;
unsigned long
sa_flags;
void (*sa_restorer)(void);
};
其中__sighandler_t 定义为:
typedef void (*__sighandler_t) (int);
在这个结构中, sa_handler为指向处理函数的指针,sa_mask是信号掩码,当该信号signum出现时,这个掩码就被逻辑或到接收进程的信号掩码中。当信号处理程序执行时,这个掩码保持有效。Sa_flags域是几个位标志的逻辑或(OR)组合,其中两个主要的标志是:
SA_ONESHOT 当信号出现时,将信号操作置为默认操作;
SA_NOMASK 忽略sigaction结构的sa_mask域。
Linux中定义的信号处理的三种类型为:
#define SIG_DFL ((__sighandler_t)0)
/* 缺省的信号处理*/
#define SIG_IGN ((__sighandler_t)1) /*忽略这个信号 */
#define SIG_ERR ((__sighandler_t)-1) /*从信号返回错误 */
下面是sigaction()系统调用在内核中实现的代码及解释。
int sys_sigaction(int signum, const struct sigaction *
action,
struct sigaction * oldaction)
{
struct sigaction new_sa, *p;
if
(signum<1 || signum>32)
return
-EINVAL;
/* 信号的值不在1~32之间,则出错 */
if
(signum==SIGKILL || signum==SIGSTOP)
return -EINVAL;
/*SIGKILL和SIGSTOP不能设置信号处理程序 */
p = signum - 1 +
current->sig->action;
/*在当前进程中,指向信号signum的action的指针 */
if (action) {
int err =
verify_area(VERIFY_READ, action, sizeof(*action));
/* 验证给action在用户空间分配的地址的有效性 */
if (err)
return err;
memcpy_fromfs(&new_sa, action,
sizeof(struct sigaction));
/* 把actoin的内容从用户空间拷贝到内核空间*/
new_sa.sa_mask |= _S(signum);
/* 把信号signum加到掩码中 */
if (new_sa.sa_flags & SA_NOMASK)
new_sa.sa_mask
&= ~_S(signum);
/* 如果标志为SA_NOMASK,当信号signum出现时,将它的操作置为默认操作 */
new_sa.sa_mask
&= _BLOCKABLE;
/* 不能阻塞SIGKILL和SIGSTOP */
if (new_sa.sa_handler != SIG_DFL &&
new_sa.sa_handler !=SIG_IGN)
{
err = verify_area(VERIFY_READ, new_sa.sa_handler, 1);
/* 当处理程序不是信号默认的处理操作,并且signum信号不能被忽略时, 验证给信号处理程序分配空间的有效性 */
if (err)
return err; }
}
if (oldaction) {
int err = verify_area(VERIFY_WRITE, oldaction, sizeof(*oldaction));
if (err)
return err;
memcpy_tofs(oldaction, p, sizeof(struct
sigaction));
/* 恢复原来的信号处理程序 */
}
if (action) {
*p =
new_sa;
check_pending(signum);
}
return
0;
}
Linux可以将各种信号发送给程序,以表示程序故障、用户请求的中断、其它各种情况等。通过对sigaction()系统调用源代码的分析,有助于灵活应用信号的系统调用。
Linux 内核中不存在任何机制用来区分不同信号的优先级。也就是说,当同时有多个信号发出时,进程可能会以任意顺序接收到信号并进行处理。另外,如果进程在处理某个信号之前,又有相同的信号发出,则进程只能接收到一个信号。产生上述现象的原因与内核对信号的实现有关。
系统在 task_struct 结构中利用两个域分别记录当前挂起的信号(signal)以及当前阻塞的信号(blocked)。挂起的信号指尚未进行处理的信号。阻塞的信号指进程当前不处理的信号,如果产生了某个当前被阻塞的信号,则该信号会一直保持挂起,直到该信号不再被阻塞为止。除了 SIGKILL 和 SIGSTOP 信号外,所有的信号均可以被阻塞,信号的阻塞可通过系统调用sigprocmask()实现。每个进程的 task_struct 结构中还包含了一个指向 sigaction 结构数组的指针,该结构数组中的信息实际指定了进程处理所有信号的方式。如果某个 sigaction 结构中包含有处理信号的例程地址,则由该处理例程处理该信号;反之,则根据结构中的一个标志或者由内核进行默认处理,或者只是忽略该信号。通过系统调用sigaction(),进程可以修改 sigaction 结构数组的信息,从而指定进程处理信号的方式。
进程不能向系统中所有的进程发送信号,一般而言,除系统和超级用户外,普通进程只能向具有相同 uid 和 gid 的进程,或者处于同一进程组的进程发送信号。当有信号产生时,内核将进程 task_struct 的 signal 字中的相应位设置为 1。系统不对置位之前该位已经为 1的情况进行处理,因而进程无法接收到前一次信号。如果进程当前没有阻塞该信号,并且进程正处于可中断的等待状态(INTERRUPTIBLE),则内核将该进程的状态改变为运行(RUNNING),并放置在运行队列中。这样,调度程序在进行调度时,就有可能选择该进程运行,从而可以让进程处理该信号。
发送给某个进程的信号并不会立即得到处理,相反,只有该进程再次运行时,才有机会处理该信号。每次进程从系统调用中退出时,内核会检查它的 signal 和 block 字段,如果有任何一个未被阻塞的信号发出,内核就根据 sigaction 结构数组中的信息进行处理。处理过程如下:
1. 检查对应的
sigaction 结构,如果该信号不是 SIGKILL 或
SIGSTOP 信号,且被忽略,则不处理该信号。
2. 如果该信号利用默认的处理程序处理,则由内核处理该信号,否则转向第 3 步。
3. 该信号由进程自己的处理程序处理,内核将修改当前进程的调用堆栈,并将进程的程序计数寄存器修改为信号处理程序的入口地址。此后,指令将跳转到信号处理程序,当从信号处理程序中返回时,实际就返回了进程的用户模式部分。
Linux 是 POSIX 兼容的,因此,进程在处理某个信号时,还可以修改进程的 blocked 掩码。但是,当信号处理程序返回时,blocked 值必须恢复为原有的掩码值,这一任务由内核的sigaction()函数完成。Linux 在进程的调用堆栈帧中添加了对清理程序的调用,该清理程序可以恢复原有的 blocked 掩码值。当内核在处理信号时,可能同时有多个信号需要由用户处理程序处理,这时,Linux 内核可以将所有的信号处理程序地址推入堆栈中,而当所有的信号处理完毕后,调用清理程序恢复原先的 blocked 值。