7.2.5进程与信号的关系
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 值。