Linux是一种多用户多任务的操作系统,系统内会有多个进程存在。无论是操作系统与用户进程之间,还是用户进程之间,经常需要共享数据和交换信息。进程间相互通信的方法有多种,信号便是其中最为简单的一种,它用以指出某事件的发生。在Linux系统中,根据具体的的软硬件情况,内核程序会发出不同的信号来通知进程某个事件的发生。对于信号的发送,尽管可以由某些用户进程发出,但是大多数情况下,都是由内核程序在遇到以下几种特定情况的时候向进程发送的,例如:
1. 系统测出一个可能出现的硬件故障,如电源故障。
2. 程序出现异常行为,如企图使用该进程之外的存贮器。
3. 该进程的子进程已经终止。
4. 用户从终端向目标程序发出中断(BREAK)键、继续(CTRL-Q)键等。
当一个信号正在被处理时,所有同样的信号都将暂时搁置(注意,并没有删除),直到这个信号处理完成后,才加以理会。
当一个进程收到信号后,用下列方式之一做出反应∶
1.忽略该信号;
2.捕获该信号(即,内核在继续执行该进程之前先运行一个由用户定义的函数);
3.让内核执行与该信号相关的默认动作。
现在用一个例子来简要说明信号的发送、捕获和处理,通过它,你就可以对信号有一个大致的印象。例如,当某程序正在执行期间,如果发现它的运行有问题,我们可以用ctrl-c或delete键打断它的执行,这实际上就是向进程发送了一个中止信号。该进程收到这个中止信号后,可以根据事先的设定,对该信号做出相应的处理,如ctrl-c或delete键被定义为一个中止信号,进程接受到这个信号,便中途退出了。上面是用信号去中断另一个进程的实例。除此以外,内核还可以通过发信号来通知一个进程:它的子进程已经终止,或通知一个超时进程:它已被设置警报(alarm)。
接下来我们开始详细介绍Linux系统中的一些与信号相关的函数。
我们介绍的第一个函数是signal ()函数,它定义在ANSI 库中 ,该函数原型如下:
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是怎样“漂亮地”解决这个难题的。
POSIX信号
POSIX signal API提供了一种新的机制,它能够处理多个信号而不必中断当前进程。
对于POSIX的信号实现来说,当一个进程正在处理一个信号时,如果有其他的信号到达,那末这些“其他的”信号将被挂起,直到该handler返回为止。可是,当一个SIGx信号在挂起之际,如果又发来另一个SIGx信号,那么内核仅把一个信号递送给该进程——也就是说,有一个信号丢失了。实际上这算不上是个大问题,因为信号除了信号号码本身之外不传送任何的信息。因此,在一个非常短的周期内多次发送一个信号相当于只将它发送一次。
信号集
POSIX signal函数(在中)运行在用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指针作为第三个参数传递.
高级信号处理
在我们最后的讨论中,将涉及信号处理的一些高级话题,如信号挂起和等待一个信号等。
获得当前信号掩码
进程当前被阻塞的信号集被称作“信号掩码”。为了获得或改变当前信号掩码 ,可以调用 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将包含全部当前闭塞信号。
等待信号
基于事件的应用程序,总是等待一个信号的出现,直到信号出现才开始运行。为此,要使用中的pause()函数:
int pause(void);
直到一个信号已经投递给该进程之后,pause ()才返回。如果存在一个与信号匹配的handler, 那么handler将在pause ()返回之前执行。换句话说,你可以调用在中的sigsuspend (),如同下述∶
int sigsuspend(const sigset_t * tempmask);
sigsuspend ()暂停该进程直到一个信号已经被投递。但是不同于pause (),在等待一个信号出现之前,它临时设置该进程的掩码为tempmask。一旦信号出现,该进程的信号掩码就会恢复为sigsuspend ()被调用之前的值。同时,pause ()(它和 sigsuspend ()两者都返回- 1)将设定errno为EINTR.
结束语
在Linux这种多用户多任务的环境中,为了完成一个任务有时候需要多个进程协同工作,这必然牵扯到进程间的相互通信。本文给出了作为进程间通信的最简单的一种——信号的发送、捕获和处理的大致过程,并介绍了与其相关的几个常用的函数,希望阅读本文后能够使你加深对它的了解。
阅读(1894) | 评论(0) | 转发(2) |