在之前大概的概述了进程之间的通信,下面笔者具体述说一下进程通信中最古老的一种通信方式之一---信号(Signals ),信号是用户进程之间通信和同步的一种原始机制,操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个)
一、 在一个信号的生命周期中有两个阶段:生成和传送。当一个事件发生时,需要通知一个进程,这时生成一个信号。当进程识别出信号的到来,就采取适当的动作来传送或处理信号。在信号到来和进程对信号进行处理之间,信号在进程上挂起(pending)。
信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。它可以在任何时候发给某一进程,而无需 知道该进程的状态。如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它为止;如果一个信号被进程设置为阻塞,则该信 号的传递被延迟,直到其阻塞被取消时才被传递给进程。内核为进程生产信号,来响应不同的事件,这些事件就是信号源。主要的信号源如下:
l 异常:进程运行过程中出现异常;
l 其它进程:一个进程可以向另一个或一组进程发送信号;
l 终端中断:Ctrl-C,Ctrl-\等;
l 作业控制:前台、后台进程的管理;
l 分配额:CPU超时或文件大小突破限制;
l 通知:通知进程某事件发生,如I/O就绪等;
l 报警:计时器到期。
每一个信号都有一个缺省动作,它是当进程没有给这个信号指定处理程序时,内核对信号的处理。有5种缺省的动作:
l 异常终止(abort):在进程的当前目录下,把进程的地址空间内容、寄存器内容保存到一个叫做core的文件中,而后终止进程。
l 退出(exit):不产生core文件,直接终止进程。
l 忽略(ignore):忽略该信号。
l 停止(stop):挂起该进程。
l 继续(continue):如果进程被挂起,则恢复进程的运行。否则,忽略信号。
进程可以对任何信号指定另一个动作或重载缺省动作,指定的新动作可以是忽略信号。进程也可以暂时地阻塞一个信号。因此进程可以选择对某种信号所采取的特定操作,这些操作包括:
l 忽略信号:进程可忽略产生的信号,但 SIGKILL 和 SIGSTOP 信号不能被忽略,必须处理(由进程自己或由内核处理)。进程可以忽略掉系统产生的大多数信号。
l 阻塞信号:进程可选择阻塞某些信号,即先将到来的某些信号记录下来,等到以后(解除阻塞后)再处理它。
l 由进程处理该信号:进程本身可在系统中注册处理信号的处理程序地址,当发出该信号时,由注册的处理程序处理信号。
l 由内核进行缺省处理:信号由内核的缺省处理程序处理,执行该信号的缺省动作。例如,进程接收到SIGFPE(浮点异常)的缺省动作是产生core并退出。大多数情况下,信号由内核处理。
需要指出的是,对信号的任何处理,包括终止进程,都必须由接收到信号的进程来执行。而进程要执行信号处理程序,就必须等到它真正运行时。因此,对信号的处理可能需要延迟一段时间。
信号没有固有的优先级。如果为一个进程同时产生了两个信号,这两个信号会以任意顺序出现在进程中并会按任意顺序被处理。另外,也没有机制用于区分同一种类的多个信号。如果进程在处理某个信号之前,又有相同的信号发出,则进程只能接收到一个信号。进程无法知道它接收了1个还是42个SIGCONT信号。
二、 数据结构。Linux用存放在进程的task_struct结构中的信息来实现信号机制,其中包括如下域:
int sigpending;
struct signal_struct *sig;
sigset_t signal, blocked;
struct signal_queue *sigqueue, **sigqueue_tail;
l sigpending是一个标记,表示该进程是否有待处理的信号。
l signal域是一个位图,表示该进程当前所有待处理的信号,每位表示一种信号。某位为1表示进程收到一个相应的信号。
l blocked域也是一个位图,放着该进程要阻塞的信号掩码,如果该位图的某位为1,表示它对应的信号目前正被进程阻塞。除了SIGSTOP和SIGKILL,所有的信号都可以被阻塞。如果产生了一个被阻塞的信号,它将一直保留等待处理,直到进程被解除阻塞。
l Sigqueue和sigqueue_tail描述了一个等待处理的信号队列,其中的每一项表示一个待处理信号的具体内容:siginfo_t。
l sig 是一个signal_struct结构,其中保存进程对每一种可能信号的处理信息,该结构的定义如下:
struct signal_struct {
atomic_t count;
struct k_sigaction action[_NSIG];
spinlock_t siglock;
};
其关键是action数组,它记录进程对每一种信号的处理信息。其中:
struct k_sigaction {
struct sigaction sa;
};
struct sigaction {
__sighandler_t sa_handler;
unsigned long sa_flags;
void (*sa_restorer)(void);
sigset_t sa_mask; /* mask last for extensibility */
};
数据结构sigaction中描述的是一个信号处理程序的相关信息,其中:sa_handler是信号处理程序的入口地址,当进程要处理该信号时,它调用这里指出的处理程序;sa_flags是一个标志,告诉Linux该进程是希望忽略这个信号还是让内核处理它;sa_mask是一个阻塞掩码,表示当该处理程序运行时,进程对信号的阻塞情况。即当该信号处理程序运行时,系统要用sa_mask替换进程blocked域的值。
三、 修改信号处理程序。进程通过执行系统调用sys_signal(定义在kernel/signal.c)可以改变缺省的信号处理例程,这些调用同时改变相应信号的sa_flags和sa_mask。sys_signal的定义如下:
unsigned long sys_signal(int sig, __sighandler_t handler)
其中sig是信号类型,handler是该信号的新处理程序。该函数所做的工作非常简单,即将signal_struct 结构中的action [sig-1]处的信号处理程序换成handler,同时将该处的老处理程序返回给用户。
四、发送信号。向一个进程发送信号由函数send_sig_info完成。该函数的定义如下:
int send_sig_info(int sig, struct siginfo *info, struct task_struct *t)
l 所做的主要工作是设置进程t的signal位图中信号sig所对应的位。
l 如果该信号没有被阻塞(位图blocked中的sig位为0),则将进程t的sigpending域设为1,表示该进程有待处理的信号。
l 对有些信号,仅仅在signal上设置一位无法将信号的内容完全传达给接收进程,此时就需要用另外一个数据结构来记录这些附加信息。Linux用数据结构signal_queue和siginfo_t来描述这些附加信息。数据结构signal_queue的定义如下:
struct signal_queue
{
struct signal_queue *next;
siginfo_t info;
};
其中的siginfo_t是一个比较复杂的数据结构,它表示的是随着信号一起传递的附加信息,其中的内容随信号种类的不同而不同。如SIGCHLD是子进程用来通知父进程自己要终止的一个信号,该信号就要有附加信息告诉父进程自己的pid、状态等信息。信号处理程序使用该附加信息对相应的信号做适当的处理。
发送信号所做的第三个工作是为信号的附加信息创建一个signal_queue数据结构,将信息内容记录在该结构的info域中,并将该结构挂在进程t的待处理信号信息结构队列中(由sigqueue和sigqueue_tail表示)。
并非系统中所有的进程都可以向其它每一个进程发送信号。事实上,只有内核和超级用户可以向任一进程发送信号,普通进程只可以向拥有相同uid和gid的进程或者在相同进程组中的进程发送信号。如上所述,通过设置进程task_struct数据结构中signal域中的适当位来产生信号。如果进程不阻塞该信号,而且它正在等待但是可以中断(状态是Interruptible),那么它的状态被改为Running并被放到运行队列,以此来唤醒该进程。这样调度程序在系统下次调度的时候会把它当作一个运行的候选。如果接收信号的进程处于其它状态(如TASK_UNINTERRUPTIBLE),则只做标记,不立刻唤醒进程。如果需要缺省的处理,Linux可以将对信号的处理优化。例如,如果信号SIGWINCH(X window改变焦点)发生并且使用的是缺省处理程序,则不需要做任何事情。
五、 处理信号。信号不会一产生就立刻出现在进程中,事实上,它们必须等待直到进程下次运行。在进程从系统调用返回到用户态之前,在进程从中断返回到用户态之前,系统都要检查进程的sigpending标记,如果它非0,说明进程有待处理的信号,于是系统就调用函数do_signal去处理它接收到的信号。这看起来好像非常不可靠,但是,系统中的每一个进程都总是在调用系统调用(如向终端写一个字符等),也总在被中断(如时钟中断等),所以进程处理信号的机会很多。如果愿意,进程可以选择等待信号,它可以在Interruptible状态下挂起,直到有了一个信号到来被唤醒。Linux信号处理代码为每一个当前未阻塞的信号检查sigaction结构,以确定如何处理它。
函数do_signal的定义如下:
int do_signal(struct pt_regs *regs, sigset_t *oldset)
该函数根据当前进程的signal域,确定进程收到了那些信号。对进程收到的每一个信号,从进程的信号等待队列中找到该信号对应的附加信息,从进程的sig域的action数组中找到信号的处理程序及其相关的信息,然后,处理信号。
如果信号处理程序被设置为缺省动作,则内核会处理它。如SIGSTOP信号的缺省处理是把当前进程的状态改为Stopped,然后运行调度程序,选择一个新的进程来运行。SIGFPE信号的缺省动作是让当前进程产生core(core dump),然后让它退出。
如果进程自己设置了信号处理程序,则系统调用该处理程序,处理信号。
有一点必须注意:当前进程运行在核心态,并正准备返回到用户态。因此系统对信号处理程序的调用方法与通常对子程序的调用方法不同,它利用当前进程的堆栈和寄存器。进程的程序计数器被设为它的信号处理程序的首地址,处理程序的参数被加到调用框架结构(call frame )中或者通过寄存器传递。当进程恢复运行的时候就象信号处理程序是正常的子程序调用一样。
Linux是POSIX兼容的,所以进程可以指定当调用特定的信号处理程序的时候要阻塞的信号。这意味着在调用进程的信号处理程序的时候改变blocked掩码。当信号处理程序结束的时候,blocked掩码必须恢复到它的初始值。因此,Linux在收到信号的进程的堆栈中增加了一个对整理例程的调用,该例程用于把blocked掩码恢复到初始值。Linux也优化了这种情况:如果同时有几个信号处理例程需要调用,就把它们堆积在一起,每次退出一个处理例程的时候就调用下一个,直到最后才调用整理例程。
六、 除了上述的操作以外,Linux还提供了另外几种对信号的操作,如sys_sigsuspend、sys_rt_sigsuspend、sys_sigaction、sys_sigpending 、sys_sigprocmask 、sys_sigaltstack、sys_sigreturn、sys_rt_sigreturn等,此处不再介绍。
信号最初的设计目的主要是用来处理错误,内核把进程运行过程中的异常情况和硬件的信息通过信号通知进程。如果进程没有指定对这些信号的处理程序,则内核处理它们,通常是终止进程。作为一种IPC机制,信号有一些局限:
l 信号的花销太大。发送信号要做系统调用;内核要中断接收进程、要管理它的堆栈、要调用处理程序、要恢复被中断的进程等。
l 信号种类有限,只有31种,而且信号能传递的信息量十分有限。
l 信号没有优先级,也没有次数的概念。
所以,信号对于事件通知很有效,但对于复杂的交互操作却难以胜任。