信号

来源:互联网 发布:软件销售 编辑:程序博客网 时间:2024/04/30 13:57

1信号

信号是软件中断。很多比较重要的应用程序都需处理信号。信号提供了一种处理异步事件的方法,例如终端用户键入中断键,则会通过信号机构停止一个程序。

首先,每个信号都有一个名字。这些名字都以三个字符SIG开头。例如,SIGABRT是夭折信号,当进程调用abort函数时产生这种信号。SIGALR M是闹钟信号,当由alarm函数设置的时间已经超过后产生此信号。在头文件<signal.h>中,这些信号都被定义为正整数(信号编号)。没有一个信号其编号为0。在后面将会看到kill函数,对信号编号0有特殊的应用。

很多条件可以产生一个信号。1、当用户按某些终端键时,产生信号。2、硬件异常产生信号:除数为0、无效的存储访问等等。3、进程用kill(2)函数可将信号发送给另一个进程或进程组。4、用户可用kill(1)命令将信号发送给其他进程。5、当检测到某种软件条件已经发生,并将其通知有关进程时也产生信号。

信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。进程不能只是测试一个变量(例如errno)来判别是否发生了一个信号,而是必须告诉内核“在此信号发生时,请执行下列操作”。

可以要求系统在某个信号出现时按照下列三种方式中的一种进行操作。

(1) 忽略此信号。大多数信号都可使用这种方式进行处理,但有两种信号却决不能被忽略。它们是:SIGKILL和SIGSTOP。这两种信号不能被忽略的原因是:它们向超级用户提供一种使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(例如非法存储访问或除以0),则进程的行为是未定义的。

(2) 捕捉信号。为了做到这一点要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。例如,若编写一个命令解释器,当用户用键盘产生中断信号时,很可能希望返回到程序的主循环,终止系统正在为该用户执行的命令。如果捕捉到SIGCHLD信号,则表示子进程已经终止,所以此信号的捕捉函数可以调用waitpid以取得该子进程的进程ID以及它的终止状态。

 (3) 执行系统默认动作。apue表10-1给出了对每一种信号的系统默认动作。注意,对大多数信号的系统默认动作是终止该进程。

2  不可靠信号、signal

2.1 不可靠信号语义

在早期的UNIX版本中(例如V 7),信号是不可靠的。不可靠在这里指的是,信号可能会被丢失——一个信号发生了,但进程却决不会知道这一点。那时,进程对信号的控制能力也很低,它能捕捉信号或忽略它,但有些很需要的功能它却并不具备。例如,有时用户希望通知内核阻塞一信号——不要忽略该信号,在其发生时记住它,然后在进程作好了准备时再通知它。这种阻塞信号的能力当时并不具备。

在早期版本中进程每次处理信号时,随即将信号动作复置为默认值。因此在进入到信号处理程序后,首先要用signal函数以重新设置此信号处理程序(在信号被复位回其默认值时,它可能被丢失,立即重新设置可以减少此窗口)。因此会出现一下两个问题:

1、在信号发生之后到信号处理程序中调用signal函数之间有一个时间窗口。在此段时间中,可能发生另一次中断信号。第二个信号会造成执行默认动作,而对中断信号则是终止该进程。这种类型的程序段在大多数情况下会正常工作,使得我们认为它们正确,而实际上却并不是如此。

2、在进程不希望某种信号发生时,它不能关闭该信号。进程能做的就是忽略该信号。有时希望通知系统“阻止下列信号发生,如果它们确实产生了,请记住它们。”这种问题的一个经典实例是在apue中有一个程序段,它捕捉一个信号,然后设置一个表示该信号已发生的标志:但是这里也有一个时间窗口,可能使操作错误。如果在测试sig_int_flag之后,调用pause之前发生信号,则此进程可能会一直睡眠(假定此信号不再次产生)。于是,这次发生的信号也就丢失了。

2.2 signal

前面讲了不可靠信号的语义,而支持不可靠信号的接口就是signal。它也是UNIX中信号机制最简单的接口。

#include <signal.h>

void (*signal (int signo, void (*func)(int))) (int);

返回:成功则为以前的信号处理配置,若出错则为SIGERR

signo参数是apue表10-1中的信号名。func的值是:(a)常数SIG_IGN,或(b)常数SIG_DFL,或(c)当接到此信号后要调用的函数的地址。如果指定SIGIGN,则向内核表示忽略此信号。(记住有两个信号SIGKILL和SIGSTOP不能忽略。)如果指定SIG_DFL,则表示接到此信号后的动作是系统默认动作。当指定函数地址时,我们称此为捕捉此信号。我们称此函数为信号处理程序(signal handler)或信号捕捉函数(signal-catching function)。

apue清单10-1显示了一个简单的信号处理程序,它捕捉两个用户定义的信号并打印信号编号。我们使该程序在后台运行,并且用kill(1)命令将信号送给它。注意,在UNIX中,杀死(kill)这个术语是不恰当的。kill(1)命令和kill(2)函数只是将一个信号送给一个进程或进程组。该信号是否终止该进程则取决于该信号的类型,以及该进程是否安排了捕捉该信号。

当向该进程发送SIGTERM信号后,该进程就终止,因为它不捕捉此信号,而对此信号的系统默认动作是终止。

2.3 exec

当执行一个程序时,所有信号的状态都是系统默认或忽略。通常所有信号都被设置为系统默认动作,除非调用exec的进程忽略该信号。比较特殊的是,exec函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不变(一个进程原先要捕捉的信号,当其执行一个新程序后,就自然地不能再捕捉了,因为信号捕捉函数的地址很可能在所执行的新程序文件中已无意义)。

我们经常会碰到的一个具体例子是一个交互shell如何处理对后台进程的中断和退出信号。对于一个非作业控制shell,当在后台执行一个进程时,例如:

cc main.c &

shell自动将后台进程中对中断和退出信号的处理方式设置为忽略。于是,当按中断键时就不会影响到后台进程。如果没有这样的处理,那么当按中断键时,它不但终止前台进程,也终止所有后台进程。

很多捕捉这两个信号的交互程序具有下列形式的代码:

int sig_int(), sig_quit();

if (signal(SIGINT, SIG_IGN) != SIG_IGN)

signal(SIGINT, sig_int);

if (signal(SIGQUIT, SIG_IGN) != SIG_IGN)

signal(SIGQUIT, sig_quit);

这样处理后,仅当SIGINT和SIGQUIT当前并不忽略,进程才捕捉它们。

2.4 fork

当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程存储图像,所以信号捕捉函数的地址在子进程中是有意义的。

3  可靠信号、信号集

3.1 可靠信号语义

前面讨论了不可靠信号,现在来看可靠信号。我们需要定义一些在讨论信号时会用到的术语。首先,当造成信号的事件发生时,为进程产生一个信号(或向一个进程发送一个信号)。当对信号做了这种动作时,我们说向一个进程递送了一个信号。在信号产生(generation)和递送(delivery)之间的时间间隔内,称信号未决(pending)。

进程可以选用“信号递送阻塞”。如果为进程产生了一个选择为阻塞的信号,而且对该信号的动作是系统默认动作或捕捉该信号,则为该进程将此信号保持为未决状态,直到该进程( a )对此信号解除了阻塞,或者( b )将对此信号的动作更改为忽略。当递送一个原来被阻塞的信号给进程时,而不是在产生该信号时,内核才决定对它的处理方式。于是进程在信号递送给它之前仍可改变对它的动作。进程调用sigpending函数来判断哪些信号是设置为阻塞并处于未决状态的。

如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,那么将如何呢? POSIX.1允许系统递送该信号一次或多次。如果递送该信号多次,则称这些信号排了队。但是大多数UNIX并不对信号排队。代之以,UNIX内核只递送这种信号一次。

每个进程都有一个信号屏蔽字,它规定了当前要阻塞递送到该进程的信号集。对于每种可能的信号,该屏蔽字中都有一位与之对应。对于某种信号,若其对应位已设置,则它当前是被阻塞的。进程可以调用sigprocmask来检测和更改其当前信号屏蔽字。

为什么要花这么多篇幅介绍信号递送和阻塞,因为这就是可靠信号与不可靠信号区别的关键所在。先看不可靠信号原因的第二点:“阻止下列信号发生,如果它们确实产生了,请记住它们。”这其实就是信号阻塞的意义。有了信号阻塞,这个要求便可以实现。再看第一点:“不可靠信号进程每次处理信号时,随即将信号动作复置为默认值,于是在信号发生之后到信号处理程序中调用signal函数之间有一个时间窗口。”这个时间窗口造成了不确定性。而在可靠信号的接口函数sigaction中,在信号处理程序被调用时,如果这种信号再次发生,它是会被阻塞的,直到信号处理函数返回为止。这里提前说面,后面还会详细分析。

3.2 信号集

我们需要有一个能表示多个信号——信号集(signal set)的数据类型。将在sigprocmask(下一节中说明)这样的函数中使用这种数据类型,以告诉内核不允许发生该信号集中的信号。POSIX.1定义数据类型sigset_t以包含一个信号集,并且定义了下列五个处理信号集的函数。

#include <signal.h>

int sigemptyset(sigset_t set)* ;

int sigfillset(sigset_t set)* ;

int sigaddset(sigset_t set,* int signo) ;

int sigdelset(sigset_t set,* int signo) ;

四个函数返回:若成功则为0,若出错则为-1

int sigismember(const sigset_set t, *int signo) ;

返回:若真则为1,若假则为0

函数sigemptyset初始化由set指向的信号集,使排除其中所有信号。函数sigfillset初始化由set指向的信号集,使其包括所有信号。所有应用程序在使用信号集前,要对该信号集调用sigemptyset或sigfillset一次。一旦已经初始化了一个信号集,以后就可在该信号集中增、删特定的信号。函数sigaddset将一个信号添加到现存集中, sigdelset则从信号集中删除一个信号。对所有以信号集作为参数的函数,我们总是以信号集地址作为传送参数。

4 sigprocmasksigpendingsigaction

这三个函数便是用来支持可靠信号的主要接口。

4.1 sigprocmask

# include <signal.h>

int sigprocmask(int how, const sigset_t *se t, sigset_t *oset) ;

返回:若成功则为0,若出错则为-1

首先, oset是非空指针,进程的当前信号屏蔽字通过oset返回。其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。下图说明了how可选用的值。

如果set是个空指针,则不改变该进程的信号屏蔽字, how的值也无意义。如果在调用sigprocmask后有任何未决的、不再阻塞的信号,则在sigprocmask返回前,至少将其中之一递送给该进程。

4.2 sigpending

sigpending返回对于调用进程被阻塞不能递送和当前未决的信号集。该信号集通过set参数返回。

#include <signal.h>

int sigpending(sigset_t *se t) ;

返回:若成功则为0,若出错则为-1

4.3 sigaction

sigaction函数的功能是检查或修改(或两者)与指定信号相关联的处理动作。此函数取代了UNIX早期版本使用的signal函数。

#include <signal.h>

int sigaction(int signo, const struct sigaction *act, struct sigaction *oact) ;

返回:若成功则为0,若出错则为- 1

其中,参数signo是要检测或修改具体动作的信号的编号数。若act指针非空,则要修改其动作。如果oact指针非空,则系统返回该信号的原先动作。此函数使用下列结构:

struct sigaction {

void (*sa_handler)();  /* addr of signal handler,or SIG_IGN, or SIG_DFL */

sigset_t sa_mask;  /* additional signals to block */

int sa_flags;  /* signal options, Table 10-5 */

} ;

当更改信号动作时,如果sa_handler指向一个信号捕捉函数(不是常数SIG_IGN或SIG_DFL),则sa_mask字段说明了一个信号集,在调用信号捕捉函数之前,该信号集要加到进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原先值。这样,在调用信号处理程序时就能阻塞某些信号。在信号处理程序被调用时,系统建立的新信号屏蔽字会自动包括正被递送的信号。因此保证了在处理一个给定的信号时,如果这种信号再次发生,那么它会被阻塞到对前一个信号的处理结束为止。这是对前述可靠信号语义的印证。

可以用sigaction实现signal函数。很多平台都是这样做的。有些系统提供老的不可靠信号语义的signal函数是为了向后兼容,除非明确要求旧的不可靠语义,否则应该使用用sigaction实现的signal,或是直接调用sigaction。

4.4 综合分析

apue清单10-11使用了很多前面说明过的信号功能。进程阻塞了SIGQUIT信号,保存了当前信号屏蔽字(以便以后恢复),然后睡眠5秒钟。在此期间所产生的退出信号都被阻塞,不递送至该进程,直到该信号不再被阻塞。在5秒睡眠结束后,检查是否有信号未决,然后将SIGQUIT设置为不再阻塞。

在睡眠期间如果产生了退出信号,那么此时该信号是未决的,但是不再受阻塞,所以在sigprocmask返回之前,它被递送到本进程。从程序的输出中可以看到这一点:SIGQUIT处理程序(sigquit)中的printf语句先执行,然后再执行sigprocmask之后的printf语句。然后该进程再睡眠5秒钟。如果在此期间再产生退出信号,那么它就会使该进程终止,因为在上次捕捉到该信号时,已将其处理方式设置为默认动作。

所以这里要明确这一点:阻塞的概念和忽略信号是不同的。当进程忽略一个信号时,信号会被传递出去但进程会将信号丢弃。而阻塞的信号不会将信号传递出去,信号被进程解除阻塞之前,信号只是暂时被阻止传递,因此不会影响进程的行为。意思是说如果接受到该信号,暂时悬挂,一旦悬挂取消了就立刻执行该信号对应的操作。sigpending 可以查询悬挂着的信号。所以,执行sigprocmask(SIG_SETMASK,&oldmask,0)后,会立即执行sig_quit函数。所以是先打印caught SIGQUIT,再打印SIGQUIT unblocked

5 killraisealarmpausesigsetjmpsiglongjmpsigsuspend

原创粉丝点击