10.1 引言
信号是软件中断,很多比较重要地应用程序都需处理信号。信号提供了一种处理异常事件地方法,例如,终端用户键入中断键,则会通过信号机制停止一个程序,或及早终止管道中的下一个程序。
本章先对信号机制进行综述,并说明每种信号的一般用法。然后分析早期实现的问题。在分析存在的问题之后再说明解决这些问题的方法,这种安排有助于加深对改进机制的理解。本章也包含了很多并非完全正确的实例,这样做的目的是为了对其不足之处进行讨论。
10.2 信号的概念
首先,每个信号都有一个名字。这些名字都以三个字符 SIG 开头。例如,SIGABRT 是夭折信号,当进程调用 abort 函数时产生这种信号。 SIGALRM 是闹钟信号,当由 alarm 函数设置的计时器超时后产生此信号。
在头文件
不存在编号为0的信号。在10.9节中将会看到,kill函数对信号编号0有特殊的应用。POSIX.1将此种信号编号值称为空信号。
很多条件可以产生信号:
(1)当用户按某些终端按键时,引发终端的信号。在终端上按 DELETE 键(或很多系统中的 Ctrl +C 键)通常产生中断信号(SIGINT)。这是停止一个已失去控制的程序的方法。
(2)硬件异常产生信号:除数0、无效的内存引用等等。这些条件通常由硬件检测到,并将其通知内核。然后内核为该条件发生时正在运行的进程产生适当的信号。例如,对执行一个无效内存引用的进程产生SIGSEGV信号。
(3)进程调用kill(2)函数可将 信号发送给另一个进程或进程组。自然,对此有所限制:接受信号进程和发送信号进程的所有者必须相同,或者发送信号进程的所有者必须是超级用户。
(4)用户可用 kill(1)命令将信号发送给其他进程。此命令只是 kill 函数的接口。常用此命令终止一个失控的后台进程。
(5)当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。这里指的不是硬件产生的条件(如除以0),而是软件条件。例如 SIGURG (在网络连接上传来带外数据时产生)、SIGPIPE(在管道的读进程已终止后,一个进程写此管道时产生),以及SIGALRM(进程所设置的闹钟时钟超时时产生)。
信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。进程不能简单地测试一个变量(例如errno)来判断是否出现了一个信号,而是必须告诉内核“在此 信号出现时,请执行下列操作”。
可以要求内核在某个信号出现时按照下列三种方式之一进行处理。我们称之为信号的处理或者信号相关的动作。
(1)忽略此信号。大多数信号都可以使用这种方式进行处理,但有两种信号却绝不能忽略。它们是 SIGKILL 和 SIGSTOP。这两种信号不能被忽略的原因是:它们向超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些硬件异常产生的信号(例如非法内存引用或者除以 0),则进程运行行为是未定义的。
(2)捕捉信号。为了做到这一点,要通知内核在某种信号发生时调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。例如,若正在运行一个命令解释器,它将用户的输入解释为命令并执行之,当用户用键盘产生中断信号时,很可能希望该命令解释器返回到主循环,终止正在为该用户执行的命令。如果捕捉到SIGCHLD信号,则表示一个子进程已经终止,所以此信号的捕捉函数可以调用 waitpid 以取得该子进程的进程 ID 以及它的终止状态。又例如,如果进程创建了临时文件,那么可能要为 SIGTERM信号编写一个信号捕捉函数以清除临时文件(SIGTERM是终止信号,Kill命令传送的系统默认信号是终止信号)。注意,不能捕捉 SIGKILL 和 SIGSTOP 信号。
(3)执行系统默认动作。表10-1给出了针对每一种信号的系统默认动作。注意,针对大多数信号的系统默认动作是终止进程。
在“默认动作”列中,“终止+core”表示在进程当前工作目录的core 文件中复制该进程的存储映像(该文件名为 core,由此可以看出这种功能很久以前就是 UNIX 的一部分)。大多数UNIX 调式程序都是用 core 文件以检查进程终止时的状态。
core文件是大多数 UNIX 系统的实现特征。大多数实现在相应进程的当前工作目录中存放 core 文件。
在下列条件下不产生 core 文件:(a)进程是设置用户ID的,而且当前用户并非程序文件的所有者,(b)进程是设置组ID的,而且当前用户并非该程序文件的组所有者,(c)用户没有写当前工作目录的权限,(d)文件已存在,而且用户对该文件没有写权限,(e)文件太大。core文件的权限(假定该文件在此之前并不存在)通常是用户读/写。
下面较详细地逐一说明这些信号。
SIGABRT 调用 abort 函数时产生此信号。进程异常终止。
SIGALRM 在用 alarm 函数设置地计时器超时时,产生此信号。若由 setitimer 函数设置的间隔超时时,也会产生此信号。
SIGBUS 指示一个实现定义的硬件故障。当出现某些类型的内存故障时,实现常常产生此种信号。
SIGCANCEL 这是 Solaris 线程库内部使用的信号,它不供一般应用。
SIGCHLD 在一个进程终止或停止时,将 SIGCHLD 信号发送给其父进程。按系统默认,将忽略此信号。如果父进程希望被告知其子进程的这种状态改变,则应捕捉此信号。信号捕捉函数中通常要调用一种 wait 函数以取得子进程 ID 和其终止状态。
SIGCONT 此作业控制信号被发送给需要继续运行,但当前处于停止状态的进程。如果接受到此信号的进程处于停止状态,则系统默认动作是使该进程继续运行,否则默认动作是忽略此信号。例如,全屏幕编辑器在捕捉到此信号后,使用信号处理程序发出重新绘制终端屏幕的通知。
SIGEMT 指示一个实现定义的硬件故障。
SIGFPE 此信号表示一个算术运算异常,例如除以0,浮点溢出等。
SIGINFO 当用户按中断键(Ctrl + C)时,终端驱动程序产生此信号并送至前台进程组中的每一个进程。当一个进程在运行时失控,特别是它正在屏幕上产生大量不需要的输出 时,常用此信号终止它。
SIGKILL 这是两个不能被捕捉或忽略的信号之一。它向系统管理员提供了一种可以杀死任一进程的可靠方法。
SIGPIPE 如果在写到管道时读进程已终止,则产生此信号。当类型为 SOCK_STREAM 套接字已不再连接时,进程写到该套接字也产生此信号。
SIGQUIT 当用户在终端上按退出键(Ctrl + \)时,产生此信号,并送至前台进程组中的所有进程。此信号不仅会终止前台进程组(如SIGINT所做的那样),同时还会产生一个 core 文件。
SIGSTOP 这是一个作业控制信号,用于停止一个进程。它类似于交互停止信号(STGTSTP)。但是SIGSTOP不能被捕捉或忽略。
SIGTERM 这是由 kill(1)命令发送的系统默认终止信号。
SIGTSTP 交互式停止信号,当用户在终端上按挂起键(Ctrl + Z)时,终端驱动程序产生此信号。该信号送至前台进程组中的所有进程。
不幸的是,停止(stop)这个术语具有不同的含义。当讨论作业控制和信号时,我们谈及停止和继续执行作业。但是,终端驱动程序一直使用术语“停止”表示用 Ctrl + S 字符停止终端输出,为了继续启动该终端输出,则用 Ctrl + Q 字符。为此,终端驱动程序称产生交互式停止信号的字符为挂起字符,而非停止字符。
SIGUSR1 这是一个用户定义的信号,可用于应用程序。
SIGUSR2 这是另一个用户定义的信号,与SIGUSR1相似,可用于应用程序。
10.3 signal 函数
UNIX 系统的信号机制最简单的接口是 signal 函数。
void (*signal(int signo, void (*func)(int))) (int);
signal函数因为不涉及多进程、进程组以及终端 I/O 等,所以它对信号的定义非常含糊,以至于对 UNIX 系统而言几乎毫无用处。
因为 signal 的语义与实现有关,所以最好使用 sigaction 函数代替 signal 函数。
signal 函数原型说明此函数需要两个参数,返回一个函数指针,而该指针所指向的函数无返回值(void)。第一个参数 signal 是一个整数,第二个参数是函数指针,它所指向的函数需要一个整形参数,无返回值。 signal 的返回值是一个函数地址,该函数有一个整形参数(即最后的 (int))。用自然语言来描述也就是要向信号处理程序传送一个整形参数,而它却无返回值。当调用 signal 设置信号处理程序时,第二个参数是指向该函数(也就是信号处理程序)的指针。 signal 的返回值则是指向之前的信号处理程序的指针。
本节开头所示的 signal 函数原型太复杂了,如果使用下面的 typedef 则可使其简单一些:
typedef void Sigfunc(int); Sigfunc *signal(int, Sigfunc *);
如果查看系统的头文件
#define SIG_ERR (void (*)())-1
#define SIG_DEL (void (*)())0
#define SIG_IGN (void (*)())1
程序清单10-1显示了一个简单的信号处理程序,它捕捉两个用户定义的信号并打印信号编号。
static void sig_usr(int); /* one handler for both signals */ int main(void) { if (signal(SIGUSR1, sig_usr) == SIG_ERR) err_sys("can't catch SIGUSR1"); if (signal(SIGUSR2, sig_usr) == SIG_ERR) err_sys("can't catch SIGUSR2"); for (;;) puase(); } static void sig_usr(int signo) /* argument is signal number */ { if (signo == SIGUSR1) printf("received SIGUSR1\n"); else if (signo == SIGUSR2) printf("received SIGUSR2\n"); else err_dump("received signal %d\n", signo); }
因为执行程序清单10-1的进程不捕捉 SIGTERM 信号,而针对该信号的系统默认动作是终止,所以当向该进程发送 SIGTERM 信号后,该进程就会终止。
1.程序启动
当执行一个程序时,所有信号的状态都是系统默认或忽略。通常所有信号都被设置为它们的默认动作,除非调用 exec 的进程忽略该信号。确切的讲, exec 函数将原先设置为要捕捉的信号都更改为他们的默认动作,其他信号的状态则不变(对于一个进程原先要捕捉的信号,当其执行一个新程序后,就自然不能再捕捉它了,因为信号捕捉函数的地址很可能在所执行的新程序文件中已无意义了)。
很多捕捉这两个信号的交互式程序具有下列形式的代码:
void sig_int(int), sig_quit(int);
if (signal(SIGINT, SIG_IGN) != SIG_IGT)
signal(SIGINT, sig_int);
if (signal(SIGQUIT, SIG_IGN) != SIG_IGN)
signal(SIGQUIT, sig_quit);
这样处理后,仅当信号当前未被忽略时,进程才会捕捉它们。
从 signal 的这两个调用中也可以看到这种函数的限制:不改变信号的处理方式就不能确定信号当前处理方式。我们将在本章的稍后部分说明使用 sigaction 函数可以确定一个信号的处理方式,而无需改变它。
2. 进程创建
当一个进程调用 fork 时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程的存储映像,所以信号捕捉函数 的地址在子进程中是有意义的。
10.4 不可靠的信号
在早期UNIX版本中,信号是不可靠的。不可靠在这里指的是,信号可能会丢失:一个信号发生了,但进程却可能一直不知道这一点。同时,进程对信号的控制能力也很差,它能捕捉信号或忽略它。有时用户希望通知内核阻塞一个信号:不要忽略该信号,在其发生时记住它,然后在进程做好准备时再通知它。这种阻塞信号的能力当时并不具备。
早期版本中的一个问题是在进程每次接受到信号对其进行处理时,随即将该信号动作复位为默认值这有一个经典实例,它与如何处理中断信号相关,其代码与下面所示的相似:
int sig_int(); ... signal(SIG, sig_int); /* establish handler */ ... sig_int() {
/************这里再次发生信号*************/ signal(SIGINT, sig_int); /* reestablish handler for next time */ ... /* process the signal ... */ }
这段代码的一个问题是:从信号发生之后到信号处理程序中调用 signal 函数之前这段时间中有一个时间窗口。在此段时间中,可能发生另一次中断信号。第二个信号会导致执行默认动作,而针对中断信号的默认动作是终止 该进程。
此外早期系统的另一个问题是:
int sig_int_flag; /* set nonzero when signal occur */ main() { int sig_int(); /* my signal handling function */ ... signal(SIGINT, sig_int); /* establish handler */ ... while (sig_int_flag == 0)
/*******************这里发生信号*****************/ pause(); /* go to sleep, waiting for signal */ ... } sig_int() { signal(SIGINT, sig_int); /* reestablish handler for next time */ sig_int_flag = 1; /* set flag for main loop to examine */ }
其中,进程调用 pause 函数使自己休眠,直至捕捉到一个信号。当捕捉到信号的,信号处理程序将标志 sig_int_flag 设置为非0值。从信号处理程序返回后,内核自动将该进程唤醒,它检测到该标志为非0,然后执行它所需做的工作。但是这里也有一个时间窗口,在此窗口中操作可能失误。如果在测试 sig_int_flag 之后和调用 pause 之前发生信号,则此进程在调用 pause 时入睡,并且长眠不醒(假定此信号不会再次产生)。
10.5 中断的系统调用
早期UNIX系统的一个特征是:如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回错误,其 errno 被设置为 EINTR。这样处理的理由是:因为一个信号发生了,进程捕捉到了它,这意味着已经发生了某种事情,所以是个应当唤醒阻塞的系统调用的好机会。
在这里,我们必须区分系统调用和函数。当捕捉到某个信号时,被中断的是内核中执行的系统调用。
为了支持这种特性,将系统调用分成两类:低速系统调用和其他系统调用。低速系统调用是可能会使进程永远阻塞的一类系统调用,它们包括:
(1)在读某些类型的文件(管道、终端设备已经网络设备)时,如果数据并不存在则可能会使调用者永远阻塞。
(2)在写这些类型的文件时,如果不能立即接受这些数据,则也可能会使调用者永远阻塞。
(3)打开某些类型文件,在某种条件发生之前也可能会使调用者阻塞(例如,打开终端设备,它要等待直到所连接的调制解调器应答了电话)。
(4)pause(按照定义,它使调用进程休眠直至捕捉到一个信号)和wait函数。
(5)某些 ioctl 操作。
(6)某些进程间通信函数(见第15章)
在这些低速系统调用中,一个值得注意的例外是与磁盘I/O有关的系统调用。虽然读、写一个磁盘文件可能暂时阻塞调用者(在磁盘驱动器将请求排入队列,然后在适当时间执行请求期间),但是除非发生硬件错误,I/O操作总会很快返回,并使调用者不再处于阻塞状态。
与被中断系统调用相关的问题是必须显示地处理出错返回。典型的代码序列(假定进行一个读操作,它被中断,我们希望重新启动它)可能如下所示:
again: if ((n = read(fd, buf, BUFFSIZE)) < 0) { if r(errno == EINR) goto again; /* just an interrupted system call */ /* handle other errors */ }
10.6 可重入函数
在信号处理程序中,不能判断捕捉到信号时进程在何处执行。如果进程正在执行malloc,在其堆中分配另外的存储空间,而此时由于捕捉到信号而插入执行该信号处理程序,其中又调用 malloc,这时会发生什么?又例如若进程正在执行 getpwnam这种将其结果存放在静态存储单元中的函数,其间插入执行信号处理程序,它又调用这样的函数,这时又会发生什么?在 malloc 例子中,可能会对进程造成破环,因为 malloc 通常为它所分配的存储区维护一个链接表,而插入执行信号处理程序时,进程可能正在更改此链接表。在 getpwnam 的例子中,返回给正常调用者的信息可能返回给信号处理程序的信息覆盖。
下表列出可重入函数
没有列如表中的大多数函数是不可重入的,其原因是:
(1)已知它们使用静态数据结构
(2)它们调用 malloc 或 free
(3)它们是标准 I/O 函数。
标准I/O库的很多实现都以不可重入方式使用全局数据结构。注意,即使在本书的某些实例中,信号处理程序也调用了 printf 函数,但这并不保证产生所期望的结果,信号处理程序可能中断主程序中的printf函数调用。
应当了解即使信号处理程序调用的是列于表10-3中的函数,但是由于每个线程只有一个errno变量(回忆1.7节对errno和线程程的讨论),所以信号处理程序可能会修改其原先值。考虑一个信号处理程序,它恰好在 main 刚刚设置 errno 之后被调用。例如,如果该信号处理程序调用 read函数,则它可能更改 errno 的值,从而取代了刚刚由 main 设置的值。因此,作为一个通用的规则,当在信号处理程序中调用表10-3中列出的函数时,应当在其前保存,在其后恢复 errno。(应当了解,经常被捕捉到的信号是 SIGCHLD,其信号处理程序通常要调用一种 wait 函数,而各种 wait 函数都能该变 errno。)
在程序清单中,信号处理程序 my_alarm 调用不可重入函数 getpwnam,而my_alarm每秒钟被调用一次。
// 在信号处理程序中调用不可重入函数 static void my_alarm(int signo) { struct passwd *rootptr; printf("in signal handler\n"); if ((rootptr = getpwnam("root")) == NULL) err_sys("getpwnam(root) error"); alarm(1); } int main(viod) { struct passwd *ptr; signal(SIGALRM, my_alarm); alarm(1); for (;;) { if ((ptr = getpwnam("sar")) == NULL) err_sys("getpwnam error"); if ((strcmp("ptr->pw_name", "sar")) != 0) printf("return value corrupted!, pw_name = %s\n", ptr->pw_name); } }
运行该程序时,其结果时随意的。
从此实例中可以看出,若信号处理程序中调用一个不可重入函数,其结构是不可预见的。
10.7 SIGCLD语义
Linux中 SIGCLD和SIGCHLD被定义为同一值。
10.8 可靠信号术语和语义
我们需要先定义一些讨论信号时用到的术语。首先,当引发信号的事件发生时,为进程产生一个信号(或向进程发送一个信号)。事件可以是硬件异常(例如,除以0)、软件条件(例如,alarm计时器超时)、终端产生的信号或调用 kill 函数。在产生了信号时,内核通常在进程表中设置一个某种形式的标志。
当对信号采取了这种动作时,我们说向进程递送了一个信号。在信号产生和递送之间的时间间隔内,称信号是未决的。
进程可以选用信号递送阻塞。如果为进程产生了一个选择为阻塞的信号,而且对该信号的动作是系统默认动作或捕捉该信号,则为进程将此信号保持为未决状态,直到该进程对此信号解除了阻塞,或者将对此信号的动作更改为忽略。内核在递送一个原来被阻塞的信号给进程时(而不是在产生该信号时),才决定对它的处理方式。于是进程在信号递送给它之前仍可改变对该信号的动作。进程调用 sigpending 函数(见10.13节)来判断哪些信号是设置为阻塞并处于未决状态的。
如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,那么将如何呢?POSIX.1允许系统递送该信号一次或多次。如果递送该信号多次,则称对这些信号进行了排队。但是除非支持POSIX.1实时扩展,否则大多数UNIX并不对信号排队。代之以UNIX内核只递送这种信号一次。
如果有多个信号要递送给一个进程,那么将如何呢?POSIX.1并没有规定这些信号的递送顺序。但是建议,在其他信号之前递送与进程当前状态有关的信号,例如SIGSEGV。
每个进程都有一个信号屏蔽字,它规定了当前要阻塞递送到该进程的信号集。对于每种可能的信号,该屏蔽字中都有一位与之对应。对于某种信号,若其对应位已设置,则它当前是被阻塞的。进程可以调用 sigprocmask 来检测和更改其当前信号屏蔽字。
信号数量可能会超过整形所包含的二进制位数。因此POSIX.1定义了一个新数据类型 sigset_t, 用于保存一个信号集。例如,信号屏蔽字就存放在这些信号集的一个种。
10.9 Kill和raise函数
kill函数将信号发送给进程或进程组,raise函数允许进程向自身发送信号。
int kill(pid_t pid, int signo); int raise(int signo);
kill的pid参数有4种不同的情况:
(1)pid>0 将该信号发送给进程ID为pid的进程
(2)pid==0 将该信号发送给与发送进程属于同一进程组的所有进程(这些进程的进程组ID等于发送进程的进程组ID),而且发送进程具有向这些进程发送信号的权限。这里用的术语“所有进程”不包括系统进程集。
(3)pid < 0 将该信号发送给其进程组ID等于pid的绝对值,而发送进程具有向其发送信号的权限。
(4)pid == -1 将该信号发送给发送进程有权限向它们发送信号的系统上的所有进程。
上面曾提及,进程信号发送给其他进程需要权限。超级用户可将信号发送给任一进程。对于非超级用户,其基本规则是发送者的实际或有效用户ID必须等于接收者的实际或有效用户ID。
在权限进行测试时也有一个特例:如果被发送的信号是 SIGCONT,则进程可将它发送给属于同一会话的任何其他进程。
如果调用 kill 为调用进程产生信号,而且此信号是不被阻塞的,那么在 kill 返回之前,就会将 signo 或者某个其他未决的非阻塞信号传送至该进程。(对于线程而言,还有一些附加条件)
10.10 alarm 和 pause 函数
使用 alarm 函数可以设置一个计时器,在将来某个指定的时间该计时器会超时。当计时器超时时,产生 SIGALRM 信号。如果不忽略或不捕捉此信号,则其默认动作是终止调用该 alarm 函数的进程。
unsigned int alarm(unsigned int seconds); 返回值:0或设置的闹钟时间的余留秒数
经过了指定的 seconds 秒后会产生 信号 SIGALRM。要了解的是,经过了指定的秒数后,信号由内核产生,由于进程调度的延迟,所以进程得到控制从而能够处理该信号还需一些时间。
每个进程只能有一个闹钟时钟。如果在调用 alarm 时,以前已为该进程设置过闹钟时钟,而且它还没有超过,则将该闹钟时钟的余留值作为本次 alarm 函数调用的值返回。已前登记的闹钟时钟则被新值代替。
如果有以前为进程登记的尚未超过的闹钟时钟,而且本次调用的 seconds 值是0,则取消以前的闹钟,其余留值仍作为 alarm 函数的返回值。
虽然 SIGALRM 的默认动作是终止进程,但是大多数使用闹钟的进程会捕捉此信号。如果此时进程要终止,则在终止之前它可以执行所需的清理操作。如果我们想捕捉 SIGALRM 信号,则必须在调用 alarm 之前设置该信号的处理程序。如果我们先调用 alarm,然后在我们能够设置 SIGALRM 处理程序之前已接收到该信号,那么进程将终止。
pause函数使调用进程挂起直至捕捉到一个信号
int pause(void);
只有执行了一个信号处理程序并从其返回时,pause才返回。在这种情况下,pause返回-1,并将 errno设置为 EINTR。
// sleep的简单而不完整的实现 static void sig_alarm(int signo) { /* nothing to do, just return to wake up the pause */ } unsigned int sleep1(unsigned int nsecs) { if (signal(SIGALRM, sig_alrm) == SIG_ERR) return(nsecs); alarm(nsecs); /* start the timer */ pause(); /* next caught signal wakes us up */ return (alarm(0)); /* turn off timer, return unslept time */ }
程序中 sleep1函数看起来与将在 10.19节说明的 sleep 函数类似,但这种简单实现有下列三个问题:
(1)如果在调用 sleep1 之前,调用者已设置了闹钟,则它会被 sleep1 函数中的第一次 alarm 调用擦除。可用下列方法更正这一点:检测第一次调用 alarm 的返回值,如其小于本次调用 alarm 的参数值,则只应等到上次设置的闹钟超时。如果上次设置闹钟的超时时间晚于本次设置值,则在 sleep1 函数返回之前,复位此闹钟,使其在上次闹钟的设定时间再次发生超时。
(2)该程序中修改了对 SIGALRM 的配置。如果编写了一个函数供其他函数调用,则在该函数被调用时先要保存原配置,在该函数返回前再恢复原配置。更正这一点的 方法是:保存signal函数的返回值,在返回前复位原配置。
(3)在第一次调用 alarm 和调用 pause 之间有一个竞争条件。在一个繁忙的系统中,可能 alarm 在调用 pause 之前超时,并调用了信号处理程序。如果发生这种情况,则在调用 pause 后,如果没有捕捉到其他信号,则调用者将永远被挂起。
对于 (3)的更正方法是使用 sigprocmask 和 sigsuspend ,10.19节将说明这种方法。
上面的实例的目的是告诉我们在涉及信号时需要有精细而周到的考虑。
除了用来实现 sleep 函数外, alarm 还常用于对可能阻塞的操作设置时间上限值。例如,程序中有一个读低速设备的可能阻塞的操作(见10.15节),我们希望超过一定时间量后就停止执行该操作。下列程序实现了这一点,它从标志输入读一行,然后将其写到标志输出上。
static void sig_alrm(int); int main(void) { int n; char line[MAXLINE]; if (signal(SIGALRM, sig_alrm) == SIG_ERR) err_sys("signal(SIGALRM) error"); alarm(10); if ((n = read(STDIN_FILENO, line, MAXLINE)) < 0) err_sys("read error"); alarm(0); write(STDOUT_FILENO, line, n); exit(0); } static void sig_alrm(int signo) { /* nothing to do, just return to interrupt the read */ }
这种代码序列在很多 UNIX 应用程序中都能见到,但是这种程序有两个问题:
(1)程序清单10-7具有与程序清单10-4相同的问题:第一次 alarm调用和 read 调用之间有一个竞争条件。如果内核在这两个函数调用之间使进程阻塞,而其时间长度又超过闹钟时间,则read可能永远阻塞。大多数这种类型的操作使用较长的闹钟时间,例如1分钟或者更长,使这种问题不会发生,但无论如果这是一个竞态条件。
(2)如果系统调用是自动重启动的,则当从 SIGALRM 信号处理程序返回时, read 并不被中断。在这种情形下,设置时间限制不起作用。
如果要对 I/O 操作设置时间限制,可以选择 使用 select或 poll 函数(14.5.1节和14.5.2节)。
10.11 信号集
我们需要有一个能够表示多个信号——信号集(signal set)的数据类型。我们将在诸如 sigprocmask(下一节中说明)之类的函数中使用这种数据类型,以便告诉内核不允许发生该信号集中的信号。如前所述,信号种类数目可能超过一个整形量所包含的位数,所以一般而言,不能用整形量中的一位代表一种信号,也就是不能用一个整形量表示信号集。POSIX.1定义了数据类型 sigset_t 以包含一个信号集,并且定义了下列五个处理信号集的函数。
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_t *set, int signo); 返回值:若真则返回1,若假则返回0,若出错则返回-1
sigemptyset初始化由set指向的信号集,清除其中所有信号。
sigfillset初始化set指向的信号集,将所有信号假如信号集,将其置一。
所有应用程序在使用信号集前,要对该信号集调用 sigemptyset 或sigfillset一次。这时因为C编译器将未赋初始值的外部和静态变量都初始化为0,而这时否与给定系统上信号集的实现相对应并不清楚。
10.12 sigprocmask 函数
10.8节曾提及一个进程的信号屏蔽字规定了当前阻塞而不能递送给该进程的信号集。调用函数 sigprocmask 可以检测或更改其信号屏蔽字,或者在一个步骤中同时执行这两个操作。
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
首先,若oset是非空指针,那么进程的当前信号屏蔽字通过 oset 返回。
其次,若 set 是一个非空指针,则参数 how 指示如何修改当前信号屏蔽字。表10-4说明了 how 可选用的值, SIG_BLOCK 是 "或"操作,而SIG_SETMASK 则是赋值操作。注意,不能阻塞 SIGKILL 和 SIGSTOP 信号。
在调用 sigprocmask 后如果有任何未决的、不再阻塞的信号,则在 sigprocmask 返回前,至少会将其中一个信号递送给该进程。
// 为进程打印信号屏蔽字 void pr_mask(const char *str) { sigset_t sigset; int errno_save; errno_save = errno; /* we can be called by signal handlers */ if (sigprocmask(0, NULL, &sigset) < 0) err_sys("sigprocmask error"); printf("%s", str); if (sigismember(&sigset, SIGINT)) printf("SIGINT"); if (sigismember(&sigset, SIGQUIT)) printf("SIGQUIT"); if (sigismember(&sigset, SIGUSR1)) printf("SIGUSR1"); /* remaining signals can go here */ printf("\n"); errno = errno_save; }
// 为进程打印信号屏蔽字 void pr_mask(const char *str) { sigset_t sigset; int errno_save; errno_save = errno; /* we can be called by signal handlers */ if (sigprocmask(0, NULL, &sigset) < 0) err_sys("sigprocmask error"); printf("%s", str); if (sigismember(&sigset, SIGINT)) printf("SIGINT"); if (sigismember(&sigset, SIGQUIT)) printf("SIGQUIT"); if (sigismember(&sigset, SIGUSR1)) printf("SIGUSR1"); /* remaining signals can go here */ printf("\n"); errno = errno_save; }
// 为进程打印信号屏蔽字 void pr_mask(const char *str) { sigset_t sigset; int errno_save; errno_save = errno; /* we can be called by signal handlers */ if (sigprocmask(0, NULL, &sigset) < 0) err_sys("sigprocmask error"); printf("%s", str); if (sigismember(&sigset, SIGINT)) printf("SIGINT"); if (sigismember(&sigset, SIGQUIT)) printf("SIGQUIT"); if (sigismember(&sigset, SIGUSR1)) printf("SIGUSR1"); /* remaining signals can go here */ printf("\n"); errno = errno_save; }
10.13 sigpending 函数
sigpending 函数返回信号集,其中的各个信号对于调用进程是阻塞的而不能递送,因而也一定是当前未决的。该信号集通过 set 参数返回。
int sigpending(sigset &set);
10.14 sigaction 函数
sigaction函数的功能是检查或修改与 指定信号相关联的处理动作(或同时执行这两种操作)。此函数取代了 signal 函数。
int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);