技术开发 频道

Linux编程:信号篇


【IT168 技术文档】

第1节 信号

信号基本原理

    Linux是一种多用户多任务的操作系统,系统内会有多个进程存在。无论是操作系统与用户进程之间,还是用户进程之间,经常需要共享数据和交换信息。进程间相互通信的方法有多种,信号便是其中最为简单的一种,它用以指出某事件的发生。在Linux系统中,根据具体的的软硬件情况,内核程序会发出不同的信号来通知进程某个事件的发生。对于信号的发送,尽管可以由某些用户进程发出,但是大多数情况下,都是由内核程序在遇到以下几种特定情况的时候向进程发送的,例如:

1. 系统测出一个可能出现的硬件故障,如电源故障。

2. 程序出现异常行为,如企图使用该进程之外的存贮器。

3. 该进程的子进程已经终止。

4. 用户从终端向目标程序发出中断(BREAK)键、继续(CTRL-Q)键等。

    当一个信号正在被处理时,所有同样的信号都将暂时搁置(注意,并没有删除),直到这个信号处理完成后,才加以理会。

    当一个进程收到信号后,用下列方式之一做出反应∶

1.忽略该信号;

2.捕获该信号(即,内核在继续执行该进程之前先运行一个由用户定义的函数);

3.让内核执行与该信号相关的默认动作。

    现在用一个例子来简要说明信号的发送、捕获和处理,通过它,你就可以对信号有一个大致的印象。例如,当某程序正在执行期间,如果发现它的运行有问题,我们可以用ctrl-c或delete键打断它的执行,这实际上就是向进程发送了一个中止信号。该进程收到这个中止信号后,可以根据事先的设定,对该信号做出相应的处理,如ctrl-c或delete键被定义为一个中止信号,进程接受到这个信号,便中途退出了。上面是用信号去中断另一个进程的实例。除此以外,内核还可以通过发信号来通知一个进程:它的子进程已经终止,或通知一个超时进程:它已被设置警报(alarm)。

    接下来我们开始详细介绍Linux系统中的一些与信号相关的函数。

    我们介绍的第一个函数是signal ()函数,它定义在ANSI <signal.h>库中 ,该函数原型如下:

void * signal(int signum, void * handler);

    它的第一个参数是将要处理的信号。第二参数是一个指针,该指针指向以下类型的涵数∶

void func();

    当信号signum产生时,内核会尽快执行handler函数。一旦handler返回,内核便从中断点继续执行进程。第二参数可以取两个特殊值:SIG_IGN和SIG_DFL。SIG_IGN用以指出该信号应该被忽略;SIG_DFL用以指示,内核收到信号后将执行默认动作。尽管一个进程不能捕获SIGSTOP和SIGKILL信号,但是内核可以执行与该信号有关的默认动作作为替代,这些默认动作分别是暂停进程和终止进程。


重发信号

    当一个正在为SIGx运行handler的进程收到另一个SIGx信号时,它应该如何处理?通常,人们会希望内核中断该进程而再一次运行handler。为此,就要求无论何时调用handler,它都必须完全可用——即使当时她正在运行也必须如此。也就是说,要求handler必须是“可重入的(re - entrant)”。然而,设计可重入的handlers是件相当复杂的事情,因此, linux没有采用此种方案。

    对于重发信号问题,起初的解决方案是:在执行用户定义的handler之前,重新将handler设置为SIG_DFL。然而,事实证明这是一个拙劣的解决方案,因为当两个信号迅速出现时,每个都给以不同地处理。内核为第一个信号执行handler而为第二个执行默认动作。这样,第二信号有时会导致该进程终止。因此,这个实现被称作"不可靠的信号( unreliable signals) "。

    在下一节中,我们将看到POSIX signal API是怎样“漂亮地”解决这个难题的。

第2节 POSIX信号

    POSIX signal API提供了一种新的机制,它能够处理多个信号而不必中断当前进程。

    对于POSIX的信号实现来说,当一个进程正在处理一个信号时,如果有其他的信号到达,那末这些“其他的”信号将被挂起,直到该handler返回为止。可是,当一个SIGx信号在挂起之际,如果又发来另一个SIGx信号,那么内核仅把一个信号递送给该进程——也就是说,有一个信号丢失了。实际上这算不上是个大问题,因为信号除了信号号码本身之外不传送任何的信息。因此,在一个非常短的周期内多次发送一个信号相当于只将它发送一次。

信号集

    POSIX signal函数(在<signal.h>中)运行在用sigset_t数据类型封装的信号集之上。这里是他们的原型和说明∶

int sigemptyset(sigset_t * pset); -- 将pset中的信号全部清除

int sigfillset(sigset_t * pset); -- 将全部有效的信号填入pset

int sigaddset(sigset_t * pset, int signum); -- 将信号signum添加到pset

int sigdelset(sigset_t * pset, int signum); -- 从pset中删除信号signum。

int sigismember(const sigset_t * pset, int signum); -- 如果signum属于pset,返回一个非零值;否则为0。

记录Handler

    为记录handler,需要调用sigaction()函数,原型如下∶

int sigaction(int signum, struct sigaction * act, struct sigaction * prev);

    sigaction ()的作用是为信号signum设置handler。内核对signum的处理将在参数act中加以描述,sigaction类型如下:

struct sigaction {
sighanlder_t sa_hanlder; sigset_t sa_mask; unsigned long sa_flags; void (*sa_restorer)(void); /*从来不用* /};

    sa_hanlder是一个指针,它指向以下类型的函数∶

void handler (int signum);

    另外,它还有两个特值:SIG_DFL和SIG_IGN。sa_mask域包含了所有当handler运行时内核将阻塞的信号。注意,不管sa_mask的值为何,正在被处理的信号总是被阻塞。当然,通过适当设定sa_flags域的flags,你还是可以逾越这个特性的。通过逐位或运算,这些flags可以取下列一个或多个值:

1.SA_NOCLDSTOP - -确保父进程当它的一个子进程停止时不会收到一个SIGCHLD信号。
2.SA_NOMASK - -逾越该信号的默认阻塞,如果它的handler当前正在执行的话。这些flag能模拟ANSI的不可靠的信号。
3.SA_ONESHOT -重新设置signum的handler为SIG_DFL。这些flag模拟ANSI signal ()函数的行为
4.SA_RESTART - -当handler退出时,确保syscall重新启动。

    如果sigactions的最后的参数不是NULL,在sigaction ()被调用之前用signum的序列填入。为了能够获得当前的信号序列而不改变它,应该把NULL作为第二个参数传递,然后将正确的sigaction指针作为第三个参数传递。

第3节 高级信号处理

    在我们最后的讨论中,将涉及信号处理的一些高级话题,如信号挂起和等待一个信号等。


    获得当前信号掩码

    进程当前被阻塞的信号集被称作“信号掩码”。为了获得或改变当前信号掩码 ,可以调用 sigprocmask ()函数∶

int sigprocmask(int mode, const sigset_t * newmask, sigset_t

oldmask);
    第一个参数可以取下列值∶

SIG_BLOCK - -将newmask中的全部信号加入到当前信号掩码中。
SIG_UNBLOCK - -从当前信号掩码中,将newmask的所有信号全部删除
SIG_SETMASK -仅阻塞newmask内的信号;将其余的信号解除阻塞。

    如果oldmask不是NULL,那么就把当前的信号掩码( sigprocmask被调用之前)拷贝给它。下列代码可用于检索一个进程的当前信号掩码∶

sigset_t oldmask;
sigemptyset(&oldmask); sigprocmask(SIG_BLOCK, NULL, &oldmask);


阻塞信号

    我们经常需要在一段程序代码中阻塞进入的信号,而这段代码和信号的handler却可能在同时修改数据。对于一个SIGALRM信号,考虑下面的handler∶

char *Flags=someString; /*一个全局字符串*/
void handleAlarm(int sig){ free(someString); someString=NULL; }

    当程序正在处理Flags时,如果一个SIGALRM信号被送达,这时将发生什么?

for (i=0; i<10; ++i)
Flags[i]=CLEAR;

    后果是灾难性的——循环语句正在往其中写数据,而handler却释放了Flags。为避免上述情况,在进入循环之前我们必须阻塞SIGALRM,过后再为其解除阻塞。

sigset_t alrm;
sigemptyset(&alrm); sigaddset(&alrm, SIGALRM); sigprocmask(SIG_BLOCK, &alrm, NULL); /*阻塞*/ for (i=0; i<10; ++i) /*安全地处理Flags*/ Flags[i]=OK; sigprocmask(SIG_UNBLOCK, &alrm, NULL);/*解除阻塞*/

    为了列出所有当前挂起的信号,可以调用sigpending ()函数∶

int sigpending(sigset_t * pending);

    调用sigpending ()后, pending将包含全部当前闭塞信号。

等待信号

    基于事件的应用程序,总是等待一个信号的出现,直到信号出现才开始运行。为此,要使用<unistd.h>中的pause()函数:

int pause(void);

    直到一个信号已经投递给该进程之后,pause ()才返回。如果存在一个与信号匹配的handler, 那么handler将在pause ()返回之前执行。换句话说,你可以调用在<signal.h>中的sigsuspend (),如同下述∶

int sigsuspend(const sigset_t * tempmask);

    sigsuspend ()暂停该进程直到一个信号已经被投递。但是不同于pause (),在等待一个信号出现之前,它临时设置该进程的掩码为tempmask。一旦信号出现,该进程的信号掩码就会恢复为sigsuspend ()被调用之前的值。同时,pause ()(它和 sigsuspend ()两者都返回- 1)将设定errno为EINTR。


结束语

    在Linux这种多用户多任务的环境中,为了完成一个任务有时候需要多个进程协同工作,这必然牵扯到进程间的相互通信。本文给出了作为进程间通信的最简单的一种——信号的发送、捕获和处理的大致过程,并介绍了与其相关的几个常用的函数,希望阅读本文后能够使你加深对它的了解。

关于作者

韩波,自由撰稿人,有近十年的 C 语言编程经验,主要感兴趣的领域为 TCP/IP 协议以及 Linux 内核。个人认为自由撰稿人的价值在于:在不影响问题实质的前提下,用一种通俗的,易于理解的方式来阐述自己的见解。

0
相关文章