第10章 信号

10.1 引言

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

UNIX系统的早期版本就已经提供信号机制,但是这些系统(如V7)所提供的信号模型并不可靠。信号可能丢失,而且在执行临界区代码时,进程很难关闭所选择的信号。4.3BSD 和 SVR3对信号模型都做了更改,增加了可靠信号机制。但是Berkeley和AT&T所做的更改之间并不兼容。幸运的是,POSIX.1对可靠信号例程进行了标准化,这正是本章所要说明的。

本章先对信号机制进行综述,并说明每种信号的一般用法。然后分析早期实现的问题。在分析存在的问题之后再说明解决这些问题的方法,这种安排有助于加深对改进机制的理解。本章也包含了很多并非完全正确的实例,这样做的目的是为了对其不足之处进行讨论。

10.2 信号概念

首先,每个信号都有一个名字。这些名字都以3个字符SIG开头。例如,SIGABRT是夭折信号,当进程调用abort函数时产生这种信号。SIGALRM是闹钟信号,由alarm函数设置的定时器超时后将产生此信号。V7 有 15 种不同的信号,SVR4 和 4.4BSD 均有 31 种不同的信号。FreeBSD 8.0支持32种信号,Mac OS X 10.6.8以及Linux 3.2.0都支持31种信号,而Solaris 10支持40种信号。但是,FreeBSD、Linux和Solaris作为实时扩展都支持另外的应用程序定义的信号。虽然本书不包括POSIX实时扩展(有关信息请参阅Gallmeister[1995]),但是SUSv4已经把实时信号接口移至基础规范说明中。

在头文件<signal.h>中,信号名都被定义为正整数常量(信号编号)。

实际上,实现将各信号定义在另一个头文件中,但是该头文件又包括在<signal.h>中。内核包括对用户级应用程序有意义的头文件,这被认为是一种不好的形式,所以如若应用程序和内核两者都需使用同一定义,那么就将有关信息放置在内核头文件中,然后用户级头文件再包括该内核头文件。于是,FreeBSD 8.0和Mac OS X 10.6.8将信号定义在<sys/signal.h>中,Linux 3.2.0将信号定义在<bits/signum.h>中,Solaris 10将信号定义在<sys/iso/signal_iso.h>中。

不存在编号为 0 的信号。在 10.9 节中将会看到,kill 函数对信号编号 0 有特殊的应用。POSIX.1将此种信号编号值称为空信号。

很多条件可以产生信号。

•当用户按某些终端键时,引发终端产生的信号。在终端上按 Delete 键(或者很多系统中的Ctrl+C键)通常产生中断信号(SIGINT)。这是停止一个已失去控制程序的方法。(第18章将说明此信号可被映射为终端上的任一字符。)

•硬件异常产生信号:除数为0、无效的内存引用等。这些条件通常由硬件检测到,并通知内核。然后内核为该条件发生时正在运行的进程产生适当的信号。例如,对执行一个无效内存引用的进程产生SIGSEGV信号。

•进程调用kill(2)函数可将任意信号发送给另一个进程或进程组。自然,对此有所限制:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。

•用户可用kill(1)命令将信号发送给其他进程。此命令只是kill函数的接口。常用此命令终止一个失控的后台进程。

•当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。这里指的不是硬件产生条件(如除以 0),而是软件条件。例如 SIGURG(在网络连接上传来带外的数据)、SIGPIPE(在管道的读进程已终止后,一个进程写此管道)以及 SIGALRM(进程所设置的定时器已经超时)。

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

在某个信号出现时,可以告诉内核按下列3种方式之一进行处理,我们称之为信号的处理或与信号相关的动作。

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

(2)捕捉信号。为了做到这一点,要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。例如,若正在编写一个命令解释器,它将用户的输入解释为命令并执行之,当用户用键盘产生中断信号时,很可能希望该命令解释器返回到主循环,终止正在为该用户执行的命令。如果捕捉到 SIGCHLD 信号,则表示一个子进程已经终止,所以此信号的捕捉函数可以调用waitpid以取得该子进程的进程ID以及它的终止状态。又例如,如果进程创建了临时文件,那么可能要为 SIGTERM 信号编写一个信号捕捉函数以清除临时文件(SIGTERM 是终止信号,kill 命令传送的系统默认信号是终止信号)。注意,不能捕捉SIGKILL和SIGSTOP信号。

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

图10-1列出了所有信号的名字,说明了哪些系统支持此信号以及对于这些信号的系统默认动作。在SUS 列中,“•”表示此种信号定义为基本POSIX.1 规范部分,“XSI”表示该信号定义在XSI扩展部分。

在系统默认动作列,“终止+core”表示在进程当前工作目录的core文件中复制了该进程的内存映像(该文件名为core,由此可以看出这种功能很久之前就是UNIX的一部分)。大多数UNIX系统调试程序都使用core文件检查进程终止时的状态。

图10-1 UNIX系统信号

产生core文件是大多数UNIX系统的实现功能。虽然该功能不是POSIX.1的组成部分,但在Single UNIX Specification XSI的扩展部分中,这一功能作为一个潜在的特定实现的动作被提及。

在不同的实现中,core 文件的名字可能不同。例如,在 FreeBSD 8.0 中,core 文件名为cmdname.core,其中cmdname是接收到信号的进程所执行的命令名。在Mac OS X 10.6.8中,core文件名是core.pid,其中,pid是接收到信号的进程的ID。(这些系统允许经sysctl参数配置core文件名。在Linux 3.2.0中,core文件名通过/proc/sys/kernel/core_pattern进行配置。)

大多数实现在相应进程的工作目录中包含core文件项;但Mac OS X将所有core文件都放置在/cores目录中。

在下列条件下不产生core文件:(a)进程是设置用户ID的,而且当前用户并非程序文件的所有者;(b)进程是设置组ID的,而且当前用户并非该程序文件的组所有者;(c)用户没有写当前工作目录的权限;(d)文件已存在,而且用户对该文件设有写权限;(e)文件太大(回忆7.11节中的RLIMIT_CORE限制)。core文件的权限(假定该文件在此之前并不存在)通常是用户读/写,但Mac OS X只设置为用户读。

在图10-1说明中的“硬件故障”对应于实现定义的硬件故障。这些名字中有很多取自UNIX系统早先在PDP-11上的实现。请查看你所使用系统的手册,以确切地弄清楚这些信号对应于哪些错误类型。

下面较详细地逐一说明这些信号。

SIGABRT 调用abort函数时(见10.17节)产生此信号。进程异常终止。

SIGALRM 当用alarm函数设置的定时器超时时,产生此信号。详细情况见10.10节。若由setitimer(2)函数设置的间隔时间已经超时时,也产生此信号。

SIGBUS 指示一个实现定义的硬件故障。当出现某些类型的内存故障时(如 14.8 节中说明的),实现常常产生此种信号。

SIGCANCEL 这是Solaris线程库内部使用的信号。它不适用于一般应用。

SIGCHLD 在一个进程终止或停止时,SIGCHLD信号被送给其父进程。按系统默认,将忽略此信号。如果父进程希望被告知其子进程的这种状态改变,则应捕捉此信号。信号捕捉函数中通常要调用一种wait函数以取得子进程ID和其终止状态。System V的早期版本有一个名为SIGCLD(无H)的类似信号。这一信号具有与其他信号不同的语义,SVR2的手册页警告在新的程序中尽量不要使用这种信号。(令人奇怪的是,在SVR3和SVR4版的手册页中,该警告消失了。)应用程序应当使用标准的SIGCHLD信号,但应了解,为了向后兼容,很多系统定义了与SIGCHLD等同的SIGCLD。如果有使用SIGCLD的软件,需要查阅系统手册,了解它具体的语义。10.7节将讨论这两个信号。

SIGCONT 此作业控制信号发送给需要继续运行,但当前处于停止状态的进程。如果接收到此信号的进程处于停止状态,则系统默认动作是使该进程继续运行;否则默认动作是忽略此信号。例如,全屏编辑程序在捕捉到此信号后,使用信号处理程序发出重新绘制终端屏幕的通知。关于进一步的情况见10.21节。

SIGEMT 指示一个实现定义的硬件故障。

EMT这一名字来自PDP-11的仿真器陷入(emulator trap)指令。并非所有平台都支持此信号。例如,Linux只对SPARC、MIPS和PA_RISC等系统结构支持SIGEMT。

SIGFPE 此信号表示一个算术运算异常,如除以0、浮点溢出等。

SIGFREEZE 此信号仅由Solaris定义。它用于通知进程在冻结系统状态之前需要采取特定动作,例如当系统进入休眠或挂起状态时可能需要做这种处理。

SIGHUP 如果终端接口检测到一个连接断开,则将此信号送给与该终端相关的控制进程(会话首进程)。见图9-13,此信号被送给session结构中s_leader字段所指向的进程。仅当终端的CLOCAL标志没有设置时,在上述条件下才产生此信号。(如果所连接的终端是本地的,则设置该终端的CLOCAL标志。它告诉终端驱动程序忽略所有调制解调器的状态行。第18章将说明如何设置此标志。)

SIGILL 此信号表示进程已执行一条非法硬件指令。

SIGINFO 这是一种BSD信号,当用户按状态键(一般采用Ctrl+T)时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程(见图 9-9)。此信号通常造成在终端上显示前台进程组中各进程的状态信息。

注意,接到此信号的会话首进程可能在后台,作为一个例子,请参见图9-7。这区别于由终端正常产生的几个信号(中断、退出和挂起),这些信号总是传递给前台进程组。

如果会话首进程终止,也产生此信号。在这种情况,此信号送给前台进程组中的每一个进程。

通常用此信号通知守护进程(见第13章)再次读取它们的配置文件。选用SIGHUP的理由是,守护进程不会有控制终端,通常决不会接收到这种信号。

4.3BSD的abort函数产生此信号。现在该函数产生SIGABRT信号。

虽然Alpha平台将SIGINFO定义为与SIGPWR具有相同值,但是Linux并不支持SIGINFO信号。这更多是因为需要对OSF/1开发的软件提供某种程度的兼容。

SIGINT 当用户按中断键(一般采用 Delete 或 Ctrl+C)时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程(见图9-9)。当一个进程在运行时失控,特别是它正在屏幕上产生大量不需要的输出时,常用此信号终止它。

SIGIO 此信号指示一个异步I/O事件。在14.5.2节中将对此进行讨论。

在图10-1中,对SIGIO的系统默认动作是终止或忽略。遗憾的是,这依赖于系统。在System V中,SIGIO与SIGPOLL相同,其默认动作是终止此进程。在BSD中,其默认动作是忽略此信号。

Linux 3.2.0和Solaris 10将SIGIO定义为与SIGPOLL具有相同值,所以默认行为是终止该进程。在FreeBSD 8.0和Mac OS X 10.6.8中,默认行为是忽略该信号。

SIGIOT 这指示一个实现定义的硬件故障。

IOT这个名字来自于PDP-11,它是PDP-11计算机“输入/输出TRAP”(input/output TRAP)指令的缩写。System V的早期版本,由abort函数产生此信号。该函数现在产生SIGABRT信号。

FreeBSD 8.0、Linux 3.2.0、Mac OS X 10.6.8和Solaris 10将SIGIOT定义为与SIGABRT具相同值。

SIGJVM1 Solaris上为Java虚拟机预留的一个信号。

SIGJVM2 Solaris上为Java虚拟机预留的另一个信号。

SIGKILL 这是两个不能被捕捉或忽略信号中的一个。它向系统管理员提供了一种可以杀死任一进程的可靠方法。

SIGLOST 运行在Solaris NFSv4客户端系统中的进程,恢复阶段不能重新获得锁,此时将由这个信号通知该进程。

SIGLWP 此信号由Solaris线程库内部使用,并不做一般使用。在FreeBSD中,SIGLWP是SIGTHR的别名。

SIGPIPE 如果在管道的读进程已终止时写管道,则产生此信号。15.2 节将说明管道。当类型为 SOCK_STREAM 的套接字已不再连接时,进程写该套接字也产生此信号。我们将在第16章说明套接字。

SIGPOLL 这个信号在SUSv4中已被标记为弃用,将来的标准可能会将此信号移除。当在一个可轮询设备上发生一个特定事件时产生此信号。14.4.2节将说明poll函数和此信号,它起源于SVR3,与BSD的SIGIO和SIGURG信号接近。在Linux和Solaris中,SIGPOLL定义为与SIGIO具有相同值。

SIGPROF 这个信号在SUSv4中已被标记为弃用,将来的标准可能会将此信号移除。当setitimer(2)函数设置的梗概统计间隔定时器(profiling interval timer)已经超时时产生此信号。

SIGPWR 这是一种依赖于系统的信号。它主要用于具有不间断电源(UPS)的系统。如果电源失效,则UPS起作用,而且通常软件会接到通知。在这种情况下,系统依靠蓄电池电源继续运行,所以无须做任何处理。但是如果蓄电池也将不能支持工作,则软件通常会再次接到通知,此时,系统必项使其各部分都停止运行。这时应当发送 SIGPWR 信号。在大多数系统中,接到蓄电池电压过低信息的进程将信号SIGPWR发送给init进程,然后由init处理停机操作。

Solaris 10和有些Linux版本在inittab文件中有两个记录项用于此种目的:powerfail以及powerwait(或powerokwait)。

在图10-1中,我们将SIGPWR的默认动作标记为“终止或忽略”。遗憾的是,这种默认动作依赖于系统。Linux对此的默认动作是终止相关进程,而Solaris的默认动作是忽略该信号。

SIGQUIT 当用户在终端上按退出键(一般采用Ctrl+\)时,中断驱动程序产生此信号,并发送给前台进程组中的所有进程(见图9-9)。此信号不仅终止前台进程组(如SIGINT所做的那样),同时产生一个core文件。

SIGSEGV 指示进程进行了一次无效的内存引用(通常说明程序有错,比如访问了一个未经初始化的指针)。

名字SEGV代表“段违例”(segmentation violation)。

SIGSTKFLT 此信号仅由Linux定义。它出现在Linux的早期版本,企图用于数学协处理器的栈故障。该信号并非由内核产生,但仍保留以向后兼容。

SIGSTOP 这是一个作业控制信号,它停止一个进程。它类似于交互停止信号(SIGTSTP),但是SIGSTOP不能被捕捉或忽略。

SIGSYS 该信号指示一个无效的系统调用。由于某种未知原因,进程执行了一条机器指令,内核认为这是一条系统调用,但该指令指示系统调用类型的参数却是无效的。这种情况是可能发生的,例如,若用户编写了一道使用新系统调用的程序,然后运行该程序的二进制可执行代码,而所用的操作系统却是不支持该系统调用的较早版本,于是就出现上述情况。

SIGTERM 这是由kill(1)命令发送的系统默认终止信号。由于该信号是由应用程序捕获的,使用SIGTERM也让程序有机会在退出之前做好清理工作,从而优雅地终止(相对于SIGKILL而言。SIGKILL不能被捕捉或者忽略)。

SIGTHAW 此信号仅由Solaris定义。在被挂起的系统恢复时,该信号用于通知相关进程,它们需要采取特定的动作。

SIGTHR FreeBSD线程库预留的信号,它的值定义或与SIGLWP相同。

SIGTRAP 指示一个实现定义的硬件故障。

此信号名来自于PDP-11的TRAP指令。当执行断点指令时,实现常用此信号将控制转移至调试程序。

SIGTSTP 交互停止信号,当用户在终端上按挂起键(一般采用 Ctrl+Z)时,终端驱动程序产生此信号。该信号发送至前台进程组中的所有进程(参见图9-9)。

遗憾的是,停止具有不同的含义。当讨论作业控制和信号时,我们谈及停止和继续作业。但是,终端驱动程序一直使用术语“停止”表示用Ctrl+S字符终止终端输出,为了继续启动该终端输出,则用Ctrl+Q字符。为此,终端驱动程序称产生交互停止信号的字符为挂起字符,而非停止字符。

SIGTTIN当一个后台进程组进程试图读其控制终端时,终端驱动程序产生此信号(见9.8节中对此问题的讨论)。在下列例外情形下不产生此信号:(a)读进程忽略或阻塞此信号;(b)读进程所属的进程组是孤儿进程组,此时读操作返回出错,errno设置为EIO。

SIGTTOU 当一个后台进程组进程试图写其控制终端时,终端驱动程序产生此信号(见9.8节对此问题的讨论)。与上面所述的SIGTTIN信号不同,一个进程可以选择允许后台进程写控制终端。第18章将讨论如何更改此选项。

如果不允许后台进程写,则与SIGTTIN相似,也有两种特殊情况:(a)写进程忽略或阻塞此信号;(b)写进程所属进程组是孤儿进程组。在第2种情况下不产生此信号,写操作返回出错,errno设置为EIO。

不论是否允许后台进程写,一些除写以外的下列终端操作也能产生SIGTTOU信号,如tcsetattr、tcsendbreak、tcdrain、tcflush、tcflow以及tcsetpgrp。第18章将说明这些终端操作。

SIGURG 此信号通知进程已经发生一个紧急情况。在网络连接上接到带外的数据时,可选择地产生此信号。

SIGUSR1 这是一个用户定义的信号,可用于应用程序。

SIGUSR2 这是另一个用户定义的信号,与SIGUSR1相似,可用于应用程序。

SIGVTALRM 当一个由setitimer(2)函数设置的虚拟间隔时间已经超时时,产生此信号。

SIGWAITING 此信号由Solaris线程库内部使用,不做他用。

SIGWINCH 内核维持与每个终端或伪终端相关联窗口的大小。进程可以用ioctl函数(见18.12 节)得到或设置窗口的大小。如果进程用 ioctl 的设置窗口大小命令更改了窗口大小,则内核将SIGWINCH信号发送至前台进程组。

SIGXCPU Single UNIX Specification的XSI扩展支持资源限制的概念(见7.11节)。如果进程超过了其软CPU时间限制,则产生此信号。

在图10-1中,对于SIGXCPU的默认动作说明为“终止或终止+core”。该默认动作依赖于操作系统。Linux 3.2.0和Solaris 10支持的默认动作是终止并创建core文件;FreeBSD 8.0和Mac OS X 10.6.8支持的默认动作是终止且不产生core文件。Single UNIX Specification要求该默认动作是,异常终止该进程,是否创建core文件则留给实现决定。

SIGXFSZ 如果进程超过了其软文件长度限制(见7.11节),则产生此信号。

如同SIGXCPU一样,针对SIGXFSZ的默认动作依赖于操作系统。Linux 3.2.0和Solaris 10对此信号的默认动作是终止并创建core文件。FreeBSD 8.0和Mac OS X 10.6.8支持的默认动作是终止且不产生core文件。Single UNIX Specification要求该默认动作是异常终止该进程,是否创建core文件则留给实现决定。

SIGXRES 此信号仅由Solaris定义。可选择地使用此信号以通知进程超过了预配置的资源值。Solaris资源限制机制是一种通用设施,用于控制在独立应用集之间共享资源的使用。

10.3 函数signal

UNIX系统信号机制最简单的接口是signal函数。

#include <signal.h>

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

返回值:若成功,返回以前的信号处理配置;若出错,返回SIG_ERR

signal函数由ISO C定义。因为ISO C不涉及多进程、进程组以及终端I/O等,所以它对信号的定义非常含糊,以致于对UNIX系统而言几乎毫无用处。

从UNIX System V派生的实现支持signal函数,但该函数提供旧的不可靠信号语义(10.4节将说明这些旧的语义)。提供此函数主要是为了向后兼容要求此旧语义的应用程序,新应用程序不应使用这些不可靠信号。

4.4BSD 也提供 signal 函数,但它是按照 sigaction 函数定义的(10.14 节将说明sigaction 函数),所以在 4.4BSD 之下使用它提供新的可靠信号语义。目前大多数系统遵循这种策略,但Solaris 10沿用System V signal函数的语义。

因为signal的语义与实现有关,所以最好使用sigaction函数代替signal函数。在10.14节讨论sigaction函数时,提供了使用该函数的signal的一个实现。本书中的所有实例均使用图10-18中给出的signal函数,这样不管使用何种平台都可以有一致的语义。

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

signal 函数原型说明此函数要求两个参数,返回一个函数指针,而该指针所指向的函数无返回值(void)。第一个参数signo 是一个整型数,第二个参数是函数指针,它所指向的函数需要一个整型参数,无返回值。signal 的返回值是一个函数地址,该函数有一个整型参数(即最后的(int))。用自然语言来描述也就是要向信号处理程序传送一个整型参数,而它却无返回值。当调用signal设置信号处理程序时,第二个参数是指向该函数(也就是信号处理程序)的指针。signal的返回值则是指向在此之前的信号处理程序的指针。

很多系统用附加的依赖于实现的参数来调用信号处理程序。10.14节将对此做进一步说明。

本节开头所示的signal函数原型太复杂了,如果使用下面的typedef[Plauger 1992],则可使其简单一些。

typedef void Sigfunc(int);

然后,可将signal函数原型写成:

Sigfunc *signal(int, Sigfunc *);

我们已将此typedef包括在apue.h文件中(见附录B),并随本章中的函数一起使用。

如果查看系统的头文件<signal.h>,则很可能会找到下列形式的声明:

#define SIG_ERR (void (*)())-1

#define SIG_DFL (void (*)())0

#define SIG_IGN (void (*)())1

这些常量可用于表示“指向函数的指针,该函数要求一个整型参数,而且无返回值”。signal的第二个参数及其返回值就可用它们表示。这些常量所使用的3 个值不一定是−1、0 和1,但它们必须是3个值而决不能是任一函数的地址。大多数UNIX系统使用上面所示的值。

实例

图10-2给出了一个简单的信号处理程序,它捕捉两个用户定义的信号并打印信号编号。10.10节将说明pause函数,它使调用进程在接到一信号前挂起。

图10-2 捕捉SIGUSR1和SIGUSR2的简单程序

我们使该程序在后台运行,并且用kill(1)命令将信号发送给它。注意,在UNIX系统中,杀死(kill)这个术语是不恰当的。kill(1)命令和 kill(2)函数只是将一个信号发送给一个进程或进程组。该信号是否终止进程则取决于该信号的类型,以及进程是否安排了捕捉该信号。

$ ./a.out &        在后台启动进程

[1]   7216        作业控制shell打印作业编号和进程ID

$ kill -USR1 7216     向该进程发送SIGUSR1

received SIGUSR1

$ kill -USR2 7216     向该进程发送SIGUSR2

received SIGUSR2

$ kill 7216        向该进程发送SIGTERM

[1]+ Terminated ./a.out

因为执行图10-2程序的进程不捕捉SIGTERM信号,而对该信号的系统默认动作是终止,所以当向该进程发送SIGTERM信号后,该进程就终止。

1.程序启动

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

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

cc main.c &

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

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

void sig_int(int), sig_quit(int);

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

signal(SIGINT, sig_int);

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

signal(SIGQUIT, sig_quit);

这样处理后,仅当SIGINT和SIGQUIT当前未被忽略时,进程才会捕捉它们。

从signal的这两个调用中也可以看到这种函数的限制:不改变信号的处理方式就不能确定信号的当前处理方式。我们将在本章的稍后部分说明使用sigaction函数可以确定一个信号的处理方式,而无需改变它。

2.进程创建

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

10.4 不可靠的信号

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

4.2BSD对信号机制进行了更改,提供了被称为可靠信号的机制。然后,SVR3也修改了信号机制,提供了System V可靠信号机制。POSIX.1选择了BSD模型作为其标准化的基础。

早期版本中的一个问题是在进程每次接到信号对其进行处理时,随即将该信号动作重置为默认值(在前面运行图10-2程序时,每种信号只捕捉一次,从而回避了这一点)。在描述这些早期系统的编程书籍中,有一个经典实例,它与如何处理中断信号相关,其代码与下面所示的相似:

int sig_int(); /* my signal handling function */

signal(SIGINT, sig_int); /* establish handler */

sig_int()

{

signal(SIGINT, sig_int); /* reestablish handler for next time */

┇         /* process the signal ... */

}

这些早期版本的另一个问题是:在进程不希望某种信号发生时,它不能关闭该信号。进程能做的一切就是忽略该信号。有时希望通知系统“阻止下列信号发生,如果它们确实产生了,请记住它们。”能够显现这种缺陷的的一个经典实例是下列程序段,它捕捉一个信号,然后设置一个表示该信号已发生的标志:

int sig_int(); /* my signal handling function */

int sig_int_flag; /* set nonzero when signal occurs */

main()

{

}

signal(SIGINT, sig_int); /* establish handler */

while (sig_int_flag == 0)

pause(); /* go to sleep, waiting for signal */

sig_int()

(由于早期的C语言版本不支持ISO C的void数据类型,所以将信号处理程序声明为int类型。)这段代码的一个问题是:在信号发生之后到信号处理程序调用signal函数之间有一个时间窗口。在此段时间中,可能发生另一次中断信号。第二个信号会造成执行默认动作,而对中断信号的默认动作是终止该进程。这种类型的程序段在大多数情况下会正常工作,使得我们认为它们是正确无误的,而实际上却并非如此。

{

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。这样处理是因为一个信号发生了,进程捕捉到它,这意味着已经发生了某种事情,所以是个好机会应当唤醒阻塞的系统调用。

在这里,我们必须区分系统调用和函数。当捕捉到某个信号时,被中断的是内核中执行的系统调用。

为了支持这种特性,将系统调用分成两类:低速系统调用和其他系统调用。低速系统调用是可能会使进程永远阻塞的一类系统调用,包括:

•如果某些类型文件(如读管道、终端设备和网络设备)的数据不存在,则读操作可能会使调用者永远阻塞;

•如果这些数据不能被相同的类型文件立即接受,则写操作可能会使调用者永远阻塞;

•在某种条件发生之前打开某些类型文件,可能会发生阻塞(例如要打开一个终端设备,需要先等待与之连接的调制解调器应答);

•pause函数(按照定义,它使调用进程休眠直至捕捉到一个信号)和wait函数;

•某些ioctl操作;

•某些进程间通信函数(见第15章)。

在这些低速系统调用中,一个值得注意的例外是与磁盘I/O有关的系统调用。虽然读、写一个磁盘文件可能暂时阻塞调用者(在磁盘驱动程序将请求排入队列,然后在适当时间执行请求期间),但是除非发生硬件错误,I/O操作总会很快返回,并使调用者不再处于阻塞状态。

可以用中断系统调用这种方法来处理的一个例子是:一个进程启动了读终端操作,而使用该终端设备的用户却离开该终端很长时间。在这种情况下,进程可能处于阻塞状态几个小时甚至数天,除非系统停机,否则一直如此。

对于中断的read、write系统调用,POSIX.1的语义在该标准的2001版有所改变。对于如何处理已 read、write 部分数据量的相应系统调用,早期版本允许实现自行选择。如若 read系统调用已接收并传送数据至应用程序缓冲区,但尚未接收到应用程序请求的全部数据,此时被中断,操作系统可以认为该系统调用失败,并将 errno 设置为 EINTR;另一种处理方式是允许该系统调用成功返回,返回值是已接收到的数据量。与此类似,如若write巳传输了应用程序缓冲区中的部分数据,然后被中断,操作系统可以认为该系统调用失败,并将errno设置为EINTR;另一种处理方式是允许该系统调用成功返回,返回值是已写部分的数据量。历史上,从System V派生的实现将这种系统调用视为失败,而 BSD 派生的实现则处理为部分成功返回。2001 版 POSIX.1标准采用BSD风格的语义。

与被中断的系统调用相关的问题是必须显式地处理出错返回。典型的代码序列(假定进行一个读操作,它被中断,我们希望重新启动它)如下:

again:

if ((n = read(fd, buf, BUFFSIZE)) < 0) {

if (errno == EINTR)

goto again; /* just an interrupted system call */

/* handle other errors */

}

为了帮助应用程序使其不必处理被中断的系统调用,4.2BSD引进了某些被中断系统调用的自动重启动。自动重启动的系统调用包括:ioctl、read、readv、write、writev、wait 和waitpid。如前所述,其中前5个函数只有对低速设备进行操作时才会被信号中断。而wait和waitpid 在捕捉到信号时总是被中断。因为这种自动重启动的处理方式也会带来问题,某些应用程序并不希望这些函数被中断后重启动。为此4.3BSD允许进程基于每个信号禁用此功能。

POSIX.1 要求只有中断信号的SA_RESTART标志有效时,实现才重启动系统调用。在10.14节将看到,sigaction函数使用这个标志允许应用程序请求重启动被中断的系统调用。

历史上,使用signal函数建立信号处理程序时,对于如何处理被中断的系统调用,各种实现的做法各不相同。System V的默认工作方式是从不重启动系统调用。而BSD则重启动被信号中断的系统调用。FreeBSD 8.0、Linux 3.2.0和Mac OS X 10.6.8中,当信号处理程序是用signal函数时,被中断的系统调用会重启动。但 Solaris 10 的默认方式是出错返回,将 errno 设置为EINTR。使用用户自己实现的signal函数(见图10-18)可以避免必须处理这些差异的麻烦。

4.2BSD引入自动重启动功能的一个理由是:有时用户并不知道所使用的输入、输出设备是否是低速设备。如果我们编写的程序可以用交互方式运行,则它可能读、写终端低速设备。如果在程序中捕捉信号,而且系统并不提供重启动功能,则对每次读、写系统调用就要进行是否出错返回的测试,如果是被中断的,则再调用读、写系统调用。

图10-3列出了几种实现所提供的与信号有关的函数及它们的语义。

图10-3 几种信号实现所提供的功能

应当了解,其他厂商提供的UNIX系统可能不同于图10-3中所示的情况。例如,SunOS 4.1.2中的sigaction默认方式是重启动被中断的系统调用,这与列在图10-3中的各平台不同。

在图10-18中,提供了我们自己的signal函数版本,它自动地尝试重启动被中断的系统调用(除 SIGALRM信号外)。在图10-19中则提供了另一个函数signal_intr,它不进行重启动。

在14.4节说明select和poll函数时,还将更多涉及被中断的系统调用。

10.6 可重入函数

进程捕捉到信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如果从信号处理程序返回(例如没有调用 exit 或longjmp),则继续执行在捕捉到信号时进程正在执行的正常指令序列(这类似于发生硬件中断时所做的)。但在信号处理程序中,不能判断捕捉到信号时进程执行到何处。如果进程正在执行malloc,在其堆中分配另外的存储空间,而此时由于捕捉到信号而插入执行该信号处理程序,其中又调用malloc,这时会发生什么?又例如,若进程正在执行getpwnam(见6.2节)这种将其结果存放在静态存储单元中的函数,其间插入执行信号处理程序,它又调用这样的函数,这时又会发生什么呢?在malloc例子中,可能会对进程造成破坏,因为malloc通常为它所分配的存储区维护一个链表,而插入执行信号处理程序时,进程可能正在更改此链表。在getpwnam的例子中,返回给正常调用者的信息可能会被返回给信号处理程序的信息覆盖。

图10-4 信号处理程序可以调用的可重入函数

Single UNIX Specification说明了在信号处理程序中保证调用安全的函数。这些函数是可重入的并被称为是异步信号安全的(async-signal safe)。除了可重入以外,在信号处理操作期间,它会阻塞任何会引起不一致的信号发送。图10-4列出了这些异步信号安全的函数。没有列入图10-4中的大多数函数是不可重入的,因为(a)已知它们使用静态数据结构;(b)它们调用 malloc 或free;(c)它们是标准I/O函数。标准I/O库的很多实现都以不可重入方式使用全局数据结构。注意,虽然在本书的某些实例中,信号处理程序也调用了printf函数,但这并不保证产生所期望的结果,信号处理程序可能中断主程序中的printf函数调用。

应当了解,即使信号处理程序调用的是图10-4中的函数,但是由于每个线程只有一个errno变量(回忆1.7节对errno和线程的讨论),所以信号处理程序可能会修改其原先值。考虑一个信号处理程序,它恰好在main刚设置errno之后被调用。如果该信号处理程序调用read这类函数,则它可能更改errno的值,从而取代了刚由main设置的值。因此,作为一个通用的规则,当在信号处理程序中调用图10-4中的函数时,应当在调用前保存errno,在调用后恢复errno。(应当了解,经常被捕捉到的信号是SIGCHLD,其信号处理程序通常要调用一种wait函数,而各种wait函数都能改变errno。)

注意,图10-4没有包括longjmp(7.10节)和siglongjmp(10.15节)。这是因为主例程以非可重入方式正在更新一个数据结构时可能产生信号。如果不是从信号处理程序返回而是调用siglongjmp,那么该数据结构可能是部分更新的。如果应用程序将要做更新全局数据结构这样的事情,而同时要捕捉某些信号,而这些信号的处理程序又会引起执行siglongjmp,则在更新这种数据结构时要阻塞此类信号。

实例

图10-5给出了一段程序,这段程序从信号处理程序my_alarm调用非可重入函数getpwnam,而my_alarm每秒钟被调用一次。10.10节中将说明alarm函数。在该程序中调用alarm函数使得每秒产生一次SIGALRM信号。

图10-5 在信号处理程序中调用不可再入函数

运行该程序时,其结果具有随机性。通常,在信号处理程序经多次迭代返回时,该程序将由SIGSEGV信号终止。检查core文件,从中可以看到main函数已调用getpwnam,但当getpwnam调用free时,信号处理程序中断了它的运行,并调用getpwnam,进而再次调用free。在信号处理程序调用free而主程序也在调用free时,malloc和free维护的数据结构就出现了损坏,偶然,此程序会运行若干秒,然后因产生 SIGSEGV 信号而终止。在捕捉到信号后,若main函数仍正确运行,其返回值却有时错误,有时正确。

从此实例中可以看出,如果在信号处理程序中调用一个非可重入函数,则其结果是不可预知的。

10.7 SIGCLD语义

SIGCLD和SIGCHLD这两个信号很容易被混淆。SIGCLD(没有H)是System V的一个信号名,其语义与名为SIGCHLD的BSD信号不同。POSIX.1采用BSD的SIGCHLD信号。

BSD的SIGCHLD信号语义与其他信号的语义相类似。子进程状态改变后产生此信号,父进程需要调用一个wait函数以检测发生了什么。

System V处理SIGCLD信号的方式不同于其他信号。如果用signal或sigset(早期设置信号配置的,与SRV3兼容的函数)设置信号配置,则基于SVR4的系统继承了这一具有问题色彩的传统(即兼容性限制)。对于SIGCLD的早期处理方式是:

(1)如果进程明确地将该信号的配置设置为SIG_IGN,则调用进程的子进程将不产生僵死进程。注意,这与其默认动作(SIG_DFL)“忽略”(见图10-1)不同。子进程在终止时,将其状态丢弃。如果调用进程随后调用一个wait函数,那么它将阻塞直到所有子进程都终止,然后该wait会返回−1,并将其errno设置为ECHILD。(此信号的默认配置是忽略,但这不会使上述语义起作用。必须将其配置明确指定为SIG_IGN才可以。)

POSIX.1 并未说明在 SIGCHLD 被忽略时应产生的后果,所以这种行为是允许的。Single UNIX Specification的XSI扩展选项要求对于SIGCHLD支持这种行为。

如果SIGCHLD被忽略,4.4BSD总是产生僵死进程。如果要避免僵死进程,则必须等待子进程。在SVR4中,如果调用signal或sigset将SIGCHLD的配置设置为忽略,则决不会产生僵死进程。本书讨论的4种平台在此方面都追随SVR4的行为。

使用sigaction可设置SA_NOCLDWAIT标志(见图10-6)以避免进程僵死。本书讨论的4种平台都支持这一点。

(2)如果将SIGCLD的配置设置为捕捉,则内核立即检查是否有子进程准备好被等待,如果是这样,则调用SIGCLD处理程序。

第2种方式改变了为此信号编写处理程序的方法,这一点可在下面的实例中看到。

实例

10.4节曾提到,进入信号处理程序后,首先要调用signal函数以重新设置此信号处理程序(在信号被重置为其默认值时,它可能会丢失,立即重新设置可以减少此窗口时间)。图10-6展示了这一点。但此程序不能在某些传统的 System V 平台上正常工作。程序一行行地不断重复输出“SIGCLD received”,最后进程用完其栈空间并异常终止。

图10-6 不能正常工作的System V SIGCLD处理程序

因为基于BSD的系统通常并不支持早期System V的SIGCLD语义,所以FreeBSD 8.0和Mac OS X 10.6.8 并没有出现此问题。Linux 3.2.0 也没有出现此问题,其原因是,虽然 SIGCLD 和SIGCHLD 定义为相同的值,但当一个进程安排捕捉 SIGCHLD,并且已经有进程准备好由其父进

程等待时,该系统并不调用SIGCHLD信号的处理程序。Solaris 10在此种情况时确实调用该信号处理程序,但在内核中增加了避免此问题的代码。

虽然本书说明的所有4种平台都解决了这一问题,但是应当意识到没有解决这一问题的平台(如AIX)依然存在。

此程序的问题是:在信号处理程序的开始处调用signal,按照上述第2种方式,内核检查是否有需要等待的子进程(因为我们正在处理一个SIGCLD信号,所以确实有这种子进程),所以它产生另一个对信号处理程序的调用。信号处理程序调用 signal,整个过程再次重复。

为了解决这一问题,应当在调用wait取到子进程的终止状态后再调用signal。此时仅当其他子进程终止,内核才会再次产生此种信号。

如果为SIGCHLD建立了一个信号处理程序,又存在一个已终止但父进程尚未等待它的进程,则是否会产生信号?POSIX.1 对此没有做说明。这就允许前面所述的工作方式。但是,POSIX.1在信号发生时并没有将信号处理重置为其默认值(假定正调用POSIX.1的sigaction函数设置其配置),于是在SIGCHLD处理程序中也就不必再为该信号指定一个信号处理程序。

务必了解你所用的系统实现中 SIGCHLD 信号的语义。也应了解在某些系统中#define SIGCHLD为SIGCLD或反之。更改这种信号的名字使你可以编译为另一个系统编写的程序,但是如果这一程序使用该信号的另一种语义,程序有可能不会正常工作。

在本书说明的4种平台上,只有Linux 3.2.0和Solaris 10定义了SIGCLD,SIGCLD等同于SIGCHLD。

10.8 可靠信号术语和语义

我们需要先定义一些在讨论信号时会用到的术语。首先,当造成信号的事件发生时,为进程产生一个信号(或向一个进程发送一个信号)。事件可以是硬件异常(如除以 0)、软件条件(如alarm 定时器超时)、终端产生的信号或调用kill 函数。当一个信号产生时,内核通常在进程表中以某种形式设置一个标志。

当对信号采取了这种动作时,我们说向进程递送了一个信号。在信号产生(generation)和递送(delivery)之间的时间间隔内,称信号是未决的(pending)。

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

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

SUSv4 中,实时信号功能已经移至基础规范的实时扩展部分。随着时间的推移,更多的系统即使不支持实时扩展,也会支持信号排队。我们将在10.20节中进一步讨论排队信号。

SVR2 的手册页称,在进程执行 SIGCLD 信号处理程序期间,该信号是用排队方式处理的,虽然在概念层次这可能是真的,但实际并非如此。内核是按照 10.7 节中所述方式产生此信号。SVR3的手册页对此做了修改,它指明在进程执行SIGCLD信号处理程序期间,忽略SIGCLD信号。SVR4手册页删除了有关部分。

AT&T[1990e]中的SVR4 sigaction(2)手册页称SA_SIGINFO标志(见图10-16)使信号可靠地排队,这是不正确的。表面上内核部分地实现了此功能,但在 SVR4 中并不起作用。令人不可思议的是,SVID(System V接口定义)对这种可靠队列并未做同样的声明。

如果有多个信号要递送给一个进程,那将如何呢?POSIX.1并没有规定这些信号的递送顺序。但是POSIX.1基础部分建议:在其他信号之前递送与进程当前状态有关的信号,如SIGSEGV。

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

信号编号可能会超过一个整型所包含的二进制位数,因此 POSIX.1 定义了一个新数据类型sigset_t,它可以容纳一个信号集。例如,信号屏蔽字就存放在其中一个信号集中。10.11节将说明对信号集进行操作的5个函数。

10.9 函数kill和raise

kill函数将信号发送给进程或进程组。raise函数则允许进程向自身发送信号。

raise最初是由ISO C定义的。后来,为了与ISO C标准保持一致,POSIX.1也包括了该函数。但是POSIX.1扩展了raise的规范,使其可处理线程(12.8中讨论线程如何与信号交互)。

因为ISO C并不涉及多进程,所以它不能定义以进程ID作为其参数(如kill函数)的函数。

#include <signal.h>

int kill(pid_t pid, int signo);

int raise(int signo);

两个函数返回值:若成功,返回0;若出错,返回−1

调用

raise(signo);

等价于调用

kill(getpid(), signo);

kill的pid参数有以下4种不同的情况。

pid > 0 将该信号发送给进程ID为pid的进程。

pid == 0 将该信号发送给与发送进程属于同一进程组的所有进程(这些进程的进程组 ID等于发送进程的进程组 ID),而且发送进程具有权限向这些进程发送信号。这里用的术语“所有进程”不包括实现定义的系统进程集。对于大多数UNIX系统,系统进程集包括内核进程和init(pid为1)。

pid < 0 将该信号发送给其进程组ID等于pid绝对值,而且发送进程具有权限向其发送信号的所有进程。如前所述,所有进程并不包括系统进程集中的进程。

pid == −1 将该信号发送给发送进程有权限向它们发送信号的所有进程。如前所述,所有进程不包括系统进程集中的进程。

如前所述,进程将信号发送给其他进程需要权限。超级用户可将信号发送给任一进程。对于非超级用户,其基本规则是发送者的实际用户 ID 或有效用户 ID 必须等于接收者的实际用户 ID或有效用户ID。如果实现支持_POSIX_SAVED_IDS(如POSIX.1现在要求的那样),则检查接收者的保存设置用户ID(而不是有效用户ID)。在对权限进行测试时也有一个特例:如果被发送的信号是SIGCONT,则进程可将它发送给属于同一会话的任一其他进程。

POSIX.1将信号编号0定义为空信号。如果signo参数是0,则kill仍执行正常的错误检查,但不发送信号。这常被用来确定一个特定进程是否仍然存在。如果向一个并不存在的进程发送空信号,则kill返回−1,errno被设置为ESRCH。但是,应当注意,UNIX系统在经过一定时间后会重新使用进程ID,所以一个现有的具有所给定进程ID的进程并不一定就是你所想要的进程。

还应理解的是,测试进程是否存在的操作不是原子操作。在kill向调用者返回测试结果时,原来已存在的被测试进程此时可能已经终止,所以这种测试并无多大价值。

如果调用kill为调用进程产生信号,而且此信号是不被阻塞的,那么在kill返回之前, signo或者某个其他未决的、非阻塞信号被传送至该进程。(对于线程而言,还有一些附加条件;详细情况见12.8节。)

10.10 函数alarm和pause

使用alarm函数可以设置一个定时器(闹钟时间),在将来的某个时刻该定时器会超时。当定时器超时时,产生 SIGALRM 信号。如果忽略或不捕捉此信号,则其默认动作是终止调用该alarm函数的进程。

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

返回值:0或以前设置的闹钟时间的余留秒数

参数seconds的值是产生信号SIGALRM需要经过的时钟秒数。当这一时刻到达时,信号由内核产生,由于进程调度的延迟,所以进程得到控制从而能够处理该信号还需要一个时间间隔。

早期的UNIX系统实现曾提出警告,这种信号可能比预定值提前1 s发送。POSIX.1则不允许这样做。

每个进程只能有一个闹钟时间。如果在调用alarm时,之前已为该进程注册的闹钟时间还没有超时,则该闹钟时间的余留值作为本次alarm函数调用的值返回。以前注册的闹钟时间则被新值代替。

如果有以前注册的尚未超过的闹钟时间,而且本次调用的seconds值是0,则取消以前的闹钟时间,其余留值仍作为alarm函数的返回值。

虽然 SIGALRM 的默认动作是终止进程,但是大多数使用闹钟的进程捕捉此信号。如果此时进程要终止,则在终止之前它可以执行所需的清理操作。如果我们想捕捉 SIGALRM 信号,则必须在调用 alarm 之前安装该信号的处理程序。如果我们先调用 alarm,然后在我们能够安装SIGALRM处理程序之前已接到该信号,那么进程将终止。

pause函数使调用进程挂起直至捕捉到一个信号。

#include <unistd.h>

int pause(void);

返回值:−1,errno设置为EINTR

只有执行了一个信号处理程序并从其返回时,pause才返回。在这种情况下,pause返回−1, errno设置为EINTR。

实例

使用alarm和pause,进程可使自己休眠一段指定的时间。图10-7中的sleep1函数看似提供了这种功能(其实这里面存在问题,我们很快就会看到)。

图10-7 sleep简化而不完整的实现

程序中的sleep1函数看起来与将在10.19节中说明的sleep函数类似,但这种简单实现有以下3个问题。

(1)如果在调用sleep1之前,调用者已设置了闹钟,则它被sleep1函数中的第一次alarm调用擦除。可用下列方法更正这一点:检查第一次调用 alarm 的返回值,如其值小于本次调用alarm的参数值,则只应等到已有的闹钟超时。如果之前设置的闹钟超时时间晚于本次设置值,则在sleep1函数返回之前,重置此闹钟,使其在之前闹钟的设定时间再次发生超时。

(2)该程序中修改了对 SIGALRM 的配置。如果编写了一个函数供其他函数调用,则在该函数被调用时先要保存原配置,在该函数返回前再恢复原配置。更正这一点的方法是:保存signal函数的返回值,在返回前重置原配置。

(3)在第一次调用alarm和pause之间有一个竞争条件。在一个繁忙的系统中,可能alarm在调用pause之前超时,并调用了信号处理程序。如果发生了这种情况,则在调用pause后,如果没有捕捉到其他信号,调用者将永远被挂起。

sleep的早期实现与图10-7程序类似,但更正了第1个和第2个问题。有两种方法可以更正第3个问题。第一种方法是使用setjmp,下一个实例将说明这种方法。另一种方法是使用sigprocmask和sigsuspend,10.19节将说明这种方法。

实例

SVR2中的sleep实现使用了setjmp和longjmp(见7.10节),以避免前一个实例的第3个问题中说明的竞争条件。此函数的一个简化版本称为sleep2,示于图10-8中(为了缩短实例程序的长度,程序中没有处理上面所说的第1个和第2个问题)。

图10-8 sleep的另一个不完善的实现

在此函数中,已避免了图10-7中具有的竞争条件。即使pause 从未执行,在发生SIGALRM时,sleep2函数也返回。

但是,sleep2函数中却有另一个难以察觉的问题,它涉及与其他信号的交互。如果SIGALRM中断了某个其他信号处理程序,则调用longjmp会提早终止该信号处理程序。图10-9显示了这种情况。SIGINT 处理程序中包含了for 循环语句,它在作者所用系统上的执行时间超过5s,也就是大于sleep2的参数值,这正是我们想要的。整型变量k说明为volatile,这样就阻止了优化编译程序去除循环语句。

图10-9 在一个捕捉其他信号的程序中调用sleep2

执行图10-9中的程序,可以通过键入中断字符来中断休眠,运行结果如下:

$ ./a.out

^C          键入中断字符

sig_int starting

sleep2 returned: 0

从中可见sleep2函数所引起的longjmp使另一个信号处理程序sig_int提早终止,即使它未完成也会如此。如果将SVR2的sleep函数与其他信号处理程序一起使用,就可能碰到这种情况。见习题10.3。

sleep1 和sleep2 函数的这两个实例是告诉我们在涉及信号时需要有精细而周到的考虑。下面几节将说明解决这些问题的方法,使我们能够可靠地、在不影响其他代码段的情况下处理信号。

实例

除了用来实现sleep函数外,alarm还常用于对可能阻塞的操作设置时间上限值。例如,程序中有一个读低速设备的可能阻塞的操作(见 10.5 节),我们希望超过一定时间量后就停止执行该操作。图10-10实现了这一点,它从标准输入读一行,然后将其写到标准输出上。

图10-10 带时间限制调用read

这种代码序列在很多UNIX应用程序中都能见到,但是这种程序有两个问题:

(1)图10-10中的程序具有与图10-7 中的程序相同的问题:在第一次alarm 调用和read调用之间有一个竞争条件。如果内核在这两个函数调用之间使进程阻塞,不能占用处理机运行,而其时间长度又超过闹钟时间,则read可能永远阻塞。大多数这种类型的操作使用较长的闹钟时间,例如1分钟或更长一点,使这种问题不会发生,但无论如何这是一个竞争条件。

(2)如果系统调用是自动重启动的,则当从SIGALRM信号处理程序返回时,read并不被中断。在这种情形下,设置时间限制不起作用。

在这里我们确实需要中断慢速系统调用。我们将在10.14节对此进行详细讨论。

实例

让我们用 longjmp 再实现前面的实例。使用这种方法无需担心一个慢速的系统调用是否被中断,见图10-11。

图10-11 使用longjmp,带时间限制调用read

不管系统是否重新启动被中断的系统调用,该程序都会如所预期的那样工作。但是要知道,该程序仍旧有和图10-8中的程序相同的与其他信号处理程序交互的问题。

如果要对I/O操作设置时间限制,则如上所示可以使用longjmp,当然也要清楚它可能有与其他信号处理程序交互的问题。另一种选择是使用select或poll函数,14.4.1节和14.4.2节将对它们进行说明。

10.11 信号集

我们需要有一个能表示多个信号——信号集(signal set)的数据类型。我们将在sigprocmask (下一节中说明)类函数中使用这种数据类型,以便告诉内核不允许发生该信号集中的信号。如前所述,不同的信号的编号可能超过一个整型量所包含的位数,所以一般而言,不能用整型量中的一位代表一种信号,也就是不能用一个整型量表示信号集。POSIX.1定义数据类型sigset_t以包含一个信号集,并且定义了下列5个处理信号集的函数。

#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);

4个函数返回值:若成功,返回0;若出错,返回−1

int sigismember(const sigset_t *set, int signo);

返回值:若真,返回1;若假,返回0

函数sigemptyset初始化由set指向的信号集,清除其中所有信号。函数sigfillset初始化由set指向的信号集,使其包括所有信号。所有应用程序在使用信号集前,要对该信号集调用sigemptyset或sigfillset一次。这是因为C编译程序将不赋初值的外部变量和静态变量都初始化为0,而这是否与给定系统上信号集的实现相对应却并不清楚。

一旦已经初始化了一个信号集,以后就可在该信号集中增、删特定的信号。函数 sigaddset将一个信号添加到已有的信号集中,sigdelset 则从信号集中删除一个信号。对所有以信号集作为参数的函数,总是以信号集地址作为向其传送的参数。

实现

如果实现的信号数目少于一个整型量所包含的位数,则可用一位代表一个信号的方法实现信号集。例如,本书的后续部分都假定一种实现有31种信号和32位整型。sigemptyset函数将整型设置为0, sigfillset函数则将整型中的各位都设置为1。这两个函数可以在<signal.h>头文件中实现为宏:

#define sigemptyset(ptr) (*(ptr) = 0)

#define sigfillset(ptr) (*(ptr) = ~(sigset_t)0, 0)

注意,除了设置信号集中各位为1外,sigfillset必须返回0,所以使用C语言的逗号算符,它将逗号算符后的值作为表达式的值返回。

使用这种实现,sigaddset 开启一位(将该位设置为 1),sigdelset 则关闭一位(将该位设置为0);sigismember测试一个指定的位。因为没有信号编号为0,所以从信号编号中减1以得到要处理位的位编号数。图10-12给出了这些函数的实现。

图10-12 sigaddset、sigdelset和sigismember的实现

也可将这3个函数在<signal.h>中实现为各一行的宏,但是POSIX.1要求检查信号编号参数的有效性,如果无效则设置errno。在宏中实现这一点比函数要难。

10.12 函数sigprocmask

10.8节曾提及一个进程的信号屏蔽字规定了当前阻塞而不能递送给该进程的信号集。调用函数sigprocmask可以检测或更改,或同时进行检测和更改进程的信号屏蔽字。

#include <signal.h>

int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

返回值:若成功,返回0;若出错,返回−1

首先,若oset是非空指针,那么进程的当前信号屏蔽字通过oset返回。

其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。图10-13说明了how可选的值。SIG_BLOCK是或操作,而SIG_SETMASK则是赋值操作。注意,不能阻塞SIGKILL和SIGSTOP信号。

图10-13 用sigprocmask更改当前信号屏蔽字的方法

如果set是个空指针,则不改变该进程的信号屏蔽字,how的值也无意义。

在调用sigprocmask后如果有任何未决的、不再阻塞的信号,则在sigprocmask返回前,至少将其中之一递送给该进程。

sigprocmask 是仅为单线程进程定义的。处理多线程进程中信号的屏蔽使用另一个函数。我们将在12.8节中对此进行讨论。

实例

图10-14程序是一个函数,它打印调用进程信号屏蔽字中的信号名。图10-20中的程序和图10-22中的程序将调用此函数。

图10-14 为进程打印信号屏蔽字

为了节省空间,没有对图10-1中列出的每一种信号测试该屏蔽字(见习题10.9)。

10.13 函数sigpending

sigpending函数返回一信号集,对于调用进程而言,其中的各信号是阻塞不能递送的,因而也一定是当前未决的。该信号集通过set参数返回。

#include <signal.h>

int sigpending(sigset_t *set);

返回值:若成功,返回0;若出错,返回−1

实例

图10-15展示了很多前面说明过的信号功能。

图10-15 信号设置和sigprocmask实例

进程阻塞SIGQUIT信号,保存了当前信号屏蔽字(以便以后恢复),然后休眠5秒。在此期间所产生的退出信号SIGQUIT都被阻塞,不递送至该进程,直到该信号不再被阻塞。在5秒休眠结束后,检查该信号是否是未决的,然后将SIGQUIT设置为不再阻塞。

注意,在设置 SIGQUIT 为阻塞时,我们保存了老的屏蔽字。为了解除对该信号的阻塞,用老的屏蔽字重新设置了进程信号屏蔽字(SIG_SETMASK)。另一种方法是用SIG_UNBLOCK使阻塞的信号不再阻塞。但是,应当了解如果编写一个可能由其他人使用的函数,而且需要在函数中阻塞一个信号,则不能用SIG_UNBLOCK简单地解除对此信号的阻塞,这是因为此函数的调用者在调用本函数之前可能也阻塞了此信号。在这种情况下必须使用SIG_SETMASK将信号屏蔽字恢复为先前的值,这样也就能继续阻塞该信号。10.18节的system函数部分有这样的一个例子。

在休眠期间如果产生了退出信号,那么此时该信号是未决的,但是不再受阻塞,所以在sigprocmask 返回之前,它被递送到调用进程。从程序的输出中可以看到这一点:SIGQUIT 处理程序(sig_quit)中的printf语句先执行,然后再执行sigprocmask之后的printf语句。

然后该进程再休眠5秒。如果在此期间再产生退出信号,那么因为在上次捕捉到该信号时,已将其处理方式设置为默认动作,所以这一次它就会使该进程终止。在下列输出中,当我们在终端键入退出字符Ctrl+\时,终端打印^\(终端退出字符):

$ ./a.out

^\             产生信号一次(在5s之内)

SIGQUIT pending      从sleep返回后

caught SIGQUIT       在信号处理程序中

SIGQUIT unblocked     从sigprocmask返回后

^\Quit(coredump)      再次产生信号

$ ./a.out

^\^\^\^\^\^\^\^\^\^\    产生信号10次(在5 s之内)

SIGQUIT pending

caught SIGQUIT       只产生信号一次

SIGQUIT unblocked

^\Quit(coredump)      再产生信号

shell发现其子进程异常终止时输出QUIT(coredump)信息。注意,第二次运行该程序时,在进程休眠期间使SIGQUIT信号产生了10次,但是解除了对该信号的阻塞后,只向进程传送一次SIGQUIT。从中可以看出在此系统上没有将信号进行排队。

10.14 函数sigaction

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

#include <signal.h>

int sigaction(int signo, const struct sigaction *restrict act,

struct sigaction *restrict oact);

返回值:若成功,返回0;若出错,返回−1

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

struct sigaction {

void   (*sa_handler)(int); /* addr of signal handler, */

/* or SIG_IGN, or SIG_DFL */

sigset_t sa_mask;       /* additional signals to block */

int   sa_flags;      /* signal options, Figure 10.16 */

/* alternate handler */

void  (*sa_sigaction)(int, siginfo_t *, void *);

};

当更改信号动作时,如果 sa_handler 字段包含一个信号捕捉函数的地址(不是常量SIG_IGN或SIG_DFL),则sa_mask字段说明了一个信号集,在调用该信号捕捉函数之前,这一信号集要加到进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原先值。这样,在调用信号处理程序时就能阻塞某些信号。在信号处理程序被调用时,操作系统建立的新信号屏蔽字包括正被递送的信号。因此保证了在处理一个给定的信号时,如果这种信号再次发生,那么它会被阻塞到对前一个信号的处理结束为止。回忆10.8节,若同一种信号多次发生,通常并不将它们加入队列,所以如果在某种信号被阻塞时,它发生了5次,那么对这种信号解除阻塞后,其信号处理函数通常只会被调用一次(上一个例子已经说明了这种特性)。

一旦对给定的信号设置了一个动作,那么在调用sigaction显式地改变它之前,该设置就一直有效。这种处理方式与早期的不可靠信号机制不同,符合POSIX.1在这方面的要求。

act结构的sa_flags字段指定对信号进行处理的各个选项。图10-16详细列出了这些选项的意义。若该标志已定义在基本 POSIX.1 标准中,那么 SUS 列包含“•”;若该标志定义在基本POSIX.1标准的XSI扩展中,那么该列包含“XSI”。

图10-16 处理每个信号的可选标志(sa_flags)

sa_sigaction字段是一个替代的信号处理程序,在sigaction结构中使用了SA_SIGINFO标志时,使用该信号处理程序。对于sa_sigaction字段和sa_handler字段两者,实现可能使用同一存储区,所以应用只能一次使用这两个字段中的一个。

通常,按下列方式调用信号处理程序:

void handler(int signo);

但是,如果设置了SA_SIGINFO标志,那么按下列方式调用信号处理程序:

void handler(int signo, siginfo_t *info, void *context);

siginfo结构包含了信号产生原因的有关信息。该结构的大致样式如下所示。符合POSIX.1的所有实现必须至少包括si_signo和si_code成员。另外,符合XSI的实现至少应包含下列字段:

struct siginfo {

int      si_signo; /* signal number */

int      si_errno; /* if nonzero, errno value from <errno.h> */

int      si_code;  /* additional info (depends on signal) */

pid_t     si_pid;   /* sending process ID */

uid_t     si_uid;   /* sending process real user ID */

void    *si_addr;  /* address that caused the fault */

int      si_status; /* exit value or signal number */

union sigval si_value; /* application-specific value */

/* possibly other fields also */

};

sigval联合包含下列字段:

int sival_int;

void *sival_ptr;

应用程序在递送信号时,在si_value.sival_int中传递一个整型数或者在si_value.sival_ptr中传递一个指针值。

图10-17示出了对于各种信号的si_code值,这些信号是由Single UNIX Specification定义的。注意,实现可定义附加的代码值。

若信号是SIGCHLD,则将设置si_pid、si_status和si_uid字段。若信号是SIGBUS、SIGILL、SIGFPE或SIGSEGV,则si_addr包含造成故障的根源地址,该地址可能并不准确。si_errno字段包含错误编号,它对应于造成信号产生的条件,并由实现定义。

信号处理程序的context参数是无类型指针,它可被强制类型转换为ucontext_t结构类型,该结构标识信号传递时进程的上下文。该结构至少包含下列字段:

ucontext_t *uc_link;    /* pointer to context resumed when */

sigset_t  uc_sigmask;  /* signals blocked when this context */

stack_t   uc_stack;   /* stack used by this context */

/* this context returns */

/* is active */

mcontext_t uc_mcontext; /* machine-specific representation of */

/* saved context */

uc_stack字段描述了当前上下文使用的栈,至少包括下列成员:

void *ss_sp;       /* stack base or pointer */

size_t ss_size;      /* stack size */

int  ss_flags;      /* flags */

当实现支持实时信号扩展时,用SA_SIGINFO标志建立的信号处理程序将造成信号可靠地排队。一些保留信号可由实时应用使用。如果信号由sigqueue函数产生,那么siginfo结构能包含应用特有的数据(参见10.20节)。

实例:signal函数

现在用sigaction实现signal函数。很多平台都是这样做的(POSIX.1的基础阐述部分也说明这是POSIX所希望的)。另一方面,有些系统支持老的不可靠信号语义signal函数,其目的是实现二进制向后兼容。除非特殊地要求老的不可靠语义(为了向后兼容),否则应当使用下面的 signal 实现,或者直接调用 sigaction(可以在调用 sigaction 时指定SA_RESETHAND和SA_NODEFER选项以实现老语义的signal函数)。本书中所有调用signal的实例均调用图10-18中实现的函数。

图10-17 siginfo_t代码值

图10-18 用sigaction实现的signal函数

注意,必须用sigemptyset函数初始化act结构的sa_mask成员。不能保证act.sa_mask=0会做同样的事情。

对除SIGALRM以外的所有信号,我们都有意尝试设置SA_RESTART标志,于是被这些信号中断的系统调用都能自动重启动。不希望重启动由 SIGALRM 信号中断的系统调用的原因是:我们希望对I/O操作可以设置时间限制(请回忆有关图10-10的讨论)。

某些早期系统(如SunOS)定义了SA_INTERRUPT标志。这些系统的默认方式是重新启动被中断的系统调用,而指定此标志则使系统调用被中断后不再重启动。Linux定义SA_INTERRUPT标志,以便与使用该标志的应用程序兼容。但是,如若信号处理程序是用sigaction设置的,那么其默认方式是不重新启动系统调用。Single UNIX Specification的XSI扩展规定,除非说明了SA_RESTART标志,否则sigaction函数不再重启动被中断的系统调用。

实例:signal_intr函数

图10-19给出的是signal函数的另一种版本,它力图阻止被中断的系统调用重启动。

图10-19 signal_intr函数

如果系统定义了SA_INTERRUPT标志,那么为了提高可移植性,我们在sa_flags中增加该标志,这样也就阻止了被中断的系统调用的重启动。

10.15 函数sigsetjmp和siglongjmp

7.10 节说明了用于非局部转移的 setjmp 和 longjmp 函数。在信号处理程序中经常调用longjmp函数以返回到程序的主循环中,而不是从该处理程序返回。图10-8和图10-11中已经出现了这种情况。

但是,调用longjmp有一个问题。当捕捉到一个信号时,进入信号捕捉函数,此时当前信号被自动地加到进程的信号屏蔽字中。这阻止了后来产生的这种信号中断该信号处理程序。如果用longjmp跳出信号处理程序,那么,对此进程的信号屏蔽字会发生什么呢?

在FreeBSD 8.0和Mac OS X 10.6.8中,setjmp和longjmp保存和恢复信号屏蔽字。但是, Linux 3.2.0和Solaris 10并不执行这种操作,虽然Linux支持提供BSD行为的选项。FreeBSD 8.0和Mac OS X 10.6.8提供函数_setjmp和_longjmp,它们也不保存和恢复信号屏蔽字。

为了允许两种形式并存,POSIX.1并没有指定setjmp和longjmp对信号屏蔽字的作用,而是定义了两个新函数sigsetjmp和siglongjmp。在信号处理程序中进行非局部转移时应当使用这两个函数。

#include <setjmp.h>

int sigsetjmp(sigjmp_buf env, int savemask);

返回值:若直接调用,返回0;若从siglongjmp调用返回,则返回非0

void siglongjmp(sigjmp_buf env, int val);

这两个函数和 setjmp、longjmp 之间的唯一区别是 sigsetjmp 增加了一个参数。如果savemask非0,则sigsetjmp在env中保存进程的当前信号屏蔽字。调用siglongjmp时,如果带非0 savemask的sigsetjmp调用已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字。

实例

图10-20中的程序演示了在信号处理程序被调用时,系统所设置的信号屏蔽字如何自动地包括刚被捕捉到的信号。此程序也示例说明了如何使用sigsetjmp和siglongjmp函数。

图10-20 信号屏蔽、sigsetjmp和siglongjmp实例

此程序演示了另一种技术,只要在信号处理程序中调用 siglongjmp 就应使用这种技术。仅在调用sigsetjmp之后才将变量canjump设置为非0值。在信号处理程序中检测此变量,仅当它为非0值时才调用siglongjmp。这提供了一种保护机制,使得在jmpbuf(跳转缓冲)尚未由sigsetjmp 初始化时,防止调用信号处理程序。(在本程序中,siglongjmp 之后程序很快就结束,但是在较大的程序中,在 siglongjmp 之后的较长一段时间内,信号处理程序可能仍旧被设置)。在一般的C代码中(不是信号处理程序),对于longjmp并不需要这种保护措施。但是,因为信号可能在任何时候发生,所以在信号处理程序中,需要这种保护措施。

在程序中使用了数据类型sig_atomic_t,这是由ISO C标准定义的变量类型,在写这种类型变量时不会被中断。这意味着在具有虚拟存储器的系统上,这种变量不会跨越页边界,可以用一条机器指令对其进行访问。这种类型的变量总是包括ISO类型修饰符volatile,其原因是:该变量将由两个不同的控制线程——main 函数和异步执行的信号处理程序访问。图10-21显示了此程序的执行时间顺序。可将图10-21分成三部分:左面部分(对应于main),中间部分(sig_usr1)和右面部分(sig_alrm)。在进程执行左面部分时,信号屏蔽字是 0(没有信号是阻塞的)。而执行中间部分时,其信号屏蔽字是SIGUSR1。执行右面部分时,信号屏蔽字是SIGUSR1|SIGALRM。

图10-21 处理两个信号的实例程序的时间顺序

执行图10-20程序,得到下面的输出:

$ ./a.out &             在后台启动进程

starting main:

[1]  531              作业控制shell打印其进程ID

$ kill -USR1 531          向该进程发送SIGUSR1

starting sig_usr1: SIGUSR1

$ in sig_alrm: SIGUSR1 SIGALRM

finishing sig_usr1: SIGUSR1

ending main:

键入回车

[1] + Done     ./a.out &

该输出与我们所期望的相同:当调用一个信号处理程序时,被捕捉到的信号加到进程的当前信号屏蔽字中。当从信号处理程序返回时,恢复原来的屏蔽字。另外,siglongjmp 恢复了由sigsetjmp所保存的信号屏蔽字。

如果在Linux中将图10-20程序中的sigsetjmp和siglongjmp分别替换成setjmp和longjmp(在FreeBSD中,则替换成_setjmp和_longjmp),则最后一行输出变成:

ending main: SIGUSR1

这意味着在调用 setjmp之后执行 main 函数时,其 SIGUSR1 是阻塞的。这多半不是我们所希望的。

10.16 函数sigsuspend

上面已经说明,更改进程的信号屏蔽字可以阻塞所选择的信号,或解除对它们的阻塞。使用这种技术可以保护不希望由信号中断的代码临界区。如果希望对一个信号解除阻塞,然后pause以等待以前被阻塞的信号发生,则又将如何呢?假定信号是SIGINT,实现这一点的一种不正确的方法是:

sigset_t  newmask, oldmask;

sigemptyset(&newmask);

sigaddset(&newmask, SIGINT);

/* block SIGINT and save current signal mask */

if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)

err_sys("SIG_BLOCK error");

/* critical region of code */

/* restore signal mask, which unblocks SIGINT */

if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)

err_sys("SIG_SETMASK error");

/* window is open */

pause(); /* wait for signal to occur */

/* continue processing */

如果在信号阻塞时,产生了信号,那么该信号的传递就被推迟直到对它解除了阻塞。对应用程序而言,该信号好像发生在解除对SIGINT的阻塞和pause之间(取决于内核如何实现信号)。如果发生了这种情况,或者如果在解除阻塞时刻和 pause 之间确实发生了信号,那么就会产生问题。因为可能不会再见到该信号,所以从这种意义上讲,在此时间窗口中发生的信号丢失了,这样就使得pause永远阻塞。这是早期的不可靠信号机制的另一个问题。

为了纠正此问题,需要在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。这种功能是由sigsuspend函数所提供的。

#include <signal.h>

int sigsuspend(const sigset_t *sigmask);

返回值:−1,并将errno设置为EINTR

进程的信号屏蔽字设置为由sigmask指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且该进程的信号屏蔽字设置为调用sigsuspend之前的值。

注意,此函数没有成功返回值。如果它返回到调用者,则总是返回−1,并将 errno 设置为EINTR(表示一个被中断的系统调用)。

实例

图10-22显示了保护代码临界区,使其不被特定信号中断的正确方法。

图10-22 保护临界区不被信号中断

注意,当sigsuspend返回时,它将信号屏蔽字设置为调用它之前的值。在本例中,SIGINT信号将被阻塞。因此将信号屏蔽恢复为之前保存的值(oldmask)。

运行图10-22中的程序得到下面的输出:

$ ./a.out

program start:

in critical region: SIGINT

^C                键入中断字符

in sig_int: SIGINT SIGUSR1

after return from sigsuspend: SIGINT

program exit:

在调用sigsuspend时,将SIGUSRI信号加到了进程信号屏蔽字中,所以当运行该信号处理程序时,我们得知信号屏蔽字已经改变了。从中可见,在 sigsuspend 返回时,它将信号屏蔽字恢复为调用它之前的值。

实例

sigsuspend的另一种应用是等待一个信号处理程序设置一个全局变量。图10-23中的程序用于捕捉中断信号和退出信号,但是希望仅当捕捉到退出信号时,才唤醒主例程。

图10-23 用sigsuspend等待一个全局变量被设置

此程序的样本输出是:

$ ./a.out

^C        键入中断字符

interrupt

^C        再次键入中断字符

interrupt

^C        再一次

interrupt

^\ $       用退出符终止

考虑到支持ISO C的非POSIX系统与POSIX系统两者之间的可移植性,在一个信号处理程序中唯一应当做的是为sig_atomic_t类型的变量赋一个值。POSIX.1规定得更多一些,它详细说明了在一个信号处理程序中可以安全地调用的函数列表(见图10-4),但是如果这样来编写代码,则它们可能不会正确地在非POSIX系统上运行。

实例

可以用信号实现父、子进程之间的同步,这是信号应用的另一个实例。图 10-24 给出了 8.9节中提到的5个例程的实现,它们是TELLWAIT、TELL_PARENT、TELL_CHILD、WAIT_PARENT和WAIT_CHILD。

图10-24 父子进程可用来实现同步的例程

其中使用了两个用户定义的信号:SIGUSR1由父进程发送给子进程,SIGUSR2由子进程发送给父进程。图15-7显示了使用管道的这5个函数的另一种实现。

如果在等待信号发生时希望去休眠,则使用 sigsuspend 函数是非常适当的(正如在前面两个例子中所示),但是如果在等待信号期间希望调用其他系统函数,那么将会怎样呢?遗憾的是,在单线程环境下对此问题没有妥善的解决方法。如果可以使用多线程,则可专门安排一个线程处理信号(见12.8节中的讨论)。

如果不使用线程,那么我们能尽力做到最好的是,当信号发生时,在信号捕捉程序中对一个全局变量置1。例如,若我们捕捉SIGINT和SIGALRM这两种信号,并用signal_intr函数设置这两个信号的处理程序,使得它们中断任一被阻塞的慢速系统调用。当进程阻塞在调用read函数等待慢速设备输入时,很可能发生这两种信号(如果设置闹钟以阻止永远等待输入,那么对于SIGALRM信号,这种情况尤其会发生)。处理这种问题的代码类似于下面所示:

if (intr_flag)    /* flag set by our SIGINT handler */

handle_intr();

if (alrm_flag)    /* flag set by our SIGALRM handler */

handle_alrm();

/* signals occurring in here are lost */

while (read( ... ) < 0) {

if (errno == EINTR) {

if (alrm_flag)

handle_alrm();

else if (intr_flag)

handle_intr();

} else {

/* some other error */

}

} else if (n == 0) {

/* end of file */

} else {

/* process input */

}

在调用read之前测试各全局标志,如果read返回一个中断的系统调用错误,则再次进行测试。如果在前两个if语句和后随的read 调用之间捕捉到两个信号中的任意一个,则问题就发生了。正如代码中的注释所指出的,在此处发生的信号丢失了。调用信号处理程序,它们设置了相应的全局变量,但是read决不会返回(除非某些数据已准备好可读)。

我们希望实现下列操作步骤。

(1)阻塞SIGINT和SIGALRM。

(2)测试两个全局变量以判别是否发生了一个信号,如果已发生则对此进行处理。

(3)调用 read(或任何其他系统函数)并解除对这两个信号的阻塞,这两个操作应当是一个原子操作。

仅当第(3)步是pause操作时,sigsuspend函数才能帮助我们。

10.17 函数abort

前面已提及abort函数的功能是使程序异常终止。

#include <stdlib.h>

void abort(void);

此函数不返回值

此函数将SIGABRT信号发送给调用进程(进程不应忽略此信号)。ISO C规定,调用abort将向主机环境递送一个未成功终止的通知,其方法是调用raise(SIGABRT)函数。

ISO C要求若捕捉到此信号而且相应信号处理程序返回,abort仍不会返回到其调用者。如果捕捉到此信号,则信号处理程序不能返回的唯一方法是它调用exit、_exit、_Exit、longjmp或siglongjmp(10.15节讨论了longjmp和siglongjmp之间的区别)。POSIX.1也说明abort并不理会进程对此信号的阻塞和忽略。

让进程捕捉 SIGABRT 的意图是:在进程终止之前由其执行所需的清理操作。如果进程并不在信号处理程序中终止自己,POSIX.1声明当信号处理程序返回时,abort终止该进程。

ISO C针对此函数的规范将下列问题留由实现决定:是否要冲洗输出流以及是否要删除临时文件(见5.13节)。 POSIX.1的要求则更进一步,它要求如果abort调用终止进程,则它对所有打开标准I/O流的效果应当与进程终止前对每个流调用fclose相同。

System V的早期版本中,abort函数产生SIGIOT信号。更进一步,进程忽略此信号或者捕捉它并从信号处理程序返回,这都是可能的,在返回情况下,abort返回到它的调用者。

4.3BSD产生SIGILL信号。在此之前,该函数解除对此信号的阻塞,将其配置恢复为SIG_DFL (终止并创建core文件)。这阻止一个进程忽略或捕捉此信号。

历史上,abort的各种实现在如何处理标准I/O流方面是并不相同的。对于保护性的程序设计以及为提高可移植性,如果希望冲洗标准 I/O 流,则在调用 abort 之前要执行这种操作。在err_dump函数中实现了这一点(见附录B)。

因为大多数UNIX系统tmpfile(临时文件)的实现在创建该文件之后立即调用unlink,所以ISO C关于临时文件的警告通常与我们无关。

实例

图10-25中的abort函数是按POSIX.1说明实现的。

图10-25 abort的POSIX.1实现

首先查看是否将执行默认动作,若是则冲洗所有标准I/O流。这并不等价于对所有打开的流调用fclose(因为只冲洗,并不关闭它们),但是当进程终止时,系统会关闭所有打开的文件。如果进程捕捉此信号并返回,那么因为进程可能产生了更多的输出,所以再一次冲洗所有的流。不进行冲洗处理的唯一条件是如果进程捕捉此信号,然后调用_exit或_Exit。在这种情况下,任何未冲洗的内存中的标准I/O缓存都被丢弃。我们假定捕捉此信号,而且_exit或_Exit的调用者并不想要冲洗缓冲区。

回忆10.9节,如果调用kill使其为调用者产生信号,并且如果该信号是不被阻塞的(图10-25中的程序保证做到这一点),则在kill返回前该信号(或某个未决、未阻塞的信号)就被传送给了该进程。我们阻塞除SIGABRT外的所有信号,这样就可知如果对kill的调用返回了,则该进程一定已捕捉到该信号,并且也从该信号处理程序返回。

10.18 函数system

8.13节已经有了一个system函数的实现,但是该版本并不执行任何信号处理。POSIX.1要求system忽略SIGINT和SIGQUIT,阻塞SIGCHLD。在给出一个正确地处理这些信号的一个版本之前,先说明为什么要考虑信号处理。

实例

图10-26中的程序使用8.13节中的system版本,用其调用ed(1)编辑器。(ed编辑器很久以来就是UNIX的组成部分。在这里使用它的原因是:它是捕捉中断和退出信号的交互式程序。若从shell调用ed,并键入中断字符,则它捕捉中断信号并打印问号。ed程序对退出信号的处理方式设置为忽略。)

图10-26中的程序用于捕捉SIGINT和SIGCHLD信号。若调用它则可得:

$ ./a.out

a           将正文追加至编辑器缓冲区

Here is one line of text

.           行首的点停止追加方式

1,$p          打印缓冲区中的第一行至最后一行,以便观察其内容

Here is one line of text

w temp.foo      将缓冲区写至一文件

25           编辑器称写了25个字节

q           离开编辑器

caught SIGCHLD

当编辑器终止时,系统向父进程(a.out进程)发送SIGCHLD信号。父进程捕捉它,执行其处理程序sig_chid,然后从信号处理程序返回。但是若父进程正捕捉 SIGCHLD 信号(因为它创建了子进程,所以应当这样做以便了解它的子进程在何时终止),那么正在执行system函数时,应当阻塞对父进程递送SIGCHLD信号。实际上,这就是POSIX.1所说明的。否则,当system创建的子进程结束时,system 的调用者可能错误地认为,它自己的一个子进程结束了。于是,调用者将会调用一种wait函数以获得子进程的终止状态,这样就阻止了system函数获得子进程的终止状态,并将其作为它的返回值。

图10-26 用syetem调用ed编辑器

如果再次执行该程序,在这次运行时将一个中断信号传送给编辑器,则可得:

$ ./a.out

a         将正文追加至编辑器缓冲区

hello, world

.         行首的点停止追加方式

1,$p       打印缓冲区中的第一行至最后一行,以便观察其内容

hello, world

w temp.foo    将缓冲区写至一文件

13        编辑器称写了13个字节

^C        键入中断符

?         编辑器捕捉信号,打印问号

caught SIGINT   父进程执行同一操作

q         离开编辑器

caught SIGCHLD

回忆9.6节可知,键入中断字符可使中断信号传送给前台进程组中的所有进程。图10-27展示了编辑器正在运行时的各个进程的关系。

图10-27 图10-26程序运行时的前台和后台进程组

在这一实例中,SIGINT被送给3个前台进程(shell进程忽略此信号)。从输出中可见,a.out进程和ed进程捕捉该信号。但是,当用system运行另一个程序时,不应使父、子进程两者都捕捉终端产生的两个信号:中断和退出。这两个信号只应发送给正在运行的程序:子进程。因为由system执行的命令可能是交互式命令(如本例中的ed),以及因为system的调用者在程序执行时放弃了控制,等待该执行程序的结束,所以system的调用者就不应接收这两个终端产生的信号。这就是为什么POSIX.1规定system的调用者在等待命令完成时应当忽略这两个信号的原因。

实例

图10-28中的程序是system函数的另一个实现,它进行了所要求的信号处理。

图10-28 system函数的POSIX.1正确实现

如果将图10-26中的程序与system函数的这一实现相链接,那么所产生的二进制代码与上一个有缺陷的程序相比较,存在如下差别。

(1)当我们键入中断字符或退出字符时,不向调用进程发送信号。

(2)当 ed 命令终止时,不向调用进程发送 SIGCHLD 信号。作为替代,在程序末尾的sigprocmask 调用对 SIGCHLD 信号解除阻塞之前,SIGCHLD 信号一直被阻塞。而对sigprocmask函数的这一次调用是在system函数调用waitpid获取子进程的终止状态之后。

POSIX.1说明,在SIGCHLD未决期间,如若wait或waitpid返回了子进程的状态,那么SIGCHLD信号不应递送给该父进程,除非另一个子进程的状态也可用。FreeBSD 8.0、Mac OS X 10.6.8和Solaris 10都实现了这种语义,而Linux 3.2.0没有实现这种语义,在system函数调用了waitpid 后,SIGCHLD 保持为未决;当解除了对此信号的阻塞后,它被递送至调用者。如果我们在图10-26的sig_chld函数中调用wait,Linux系统将返回−1,并将errno设置为ECHILD,因为system函数已取到子进程的终止状态。

很多较早的书中使用下列程序段,它忽略中断和退出信号:

if ( (pid = fork()) < 0){

err_sys("fork error");

}else if (pid == 0) {

/* child */

execl(...);

_exit(127);

}

/* parent */

old_intr = signal(SIGINT, SIG_IGN);

old_quit = signal(SIGQUIT, SIG_IGN);

waitpid(pid, &status, 0)

signal(SIGINT, old_intr);

signal(SIGQUIT, old_quit);

这段代码的问题是:在fork之后不能保证父进程还是子进程先运行。如果子进程先运行,父进程在一段时间后再运行,那么在父进程将中断信号的处理更改为忽略之前,就可能产生这种信号。由于这种原因,图10-28中在fork之前就改变对该信号的配置。

注意,子进程在调用execl之前要先恢复这两个信号的处理。如同8.10节中所说明的一样,这就允许在调用者配置的基础上,execl可将它们的配置更改为默认值。

system的返回值

注意system的返回值,它是shell的终止状态,但shell的终止状态并不总是执行命令字符串进程的终止状态。图8-23中有一些例子,其结果正是我们所期望的。如果执行一条如date那样的简单命令,其终止状态是0。执行shell命令exit 44,则得终止状态44。在信号方面又如何呢?

运行图8-24程序,并向正在执行的命令发送一些信号:

$ tsys "sleep 30"

^Cnormal termination, exit status = 130   键入中断符

$ tsys "sleep 30"

^\sh: 946 Quit                键入退出符

normal termination, exit status = 131

当用中断信号终止sleep时,pr_exit函数(见图8-5)认为它正常终止。当用退出符杀死sleep进程时,会发生同样的事情。终止状态130、131又是怎样得到的呢?原来Bourne shell有一个在其文档中没有说清楚的特性,其终止状态是128加上一个信号编号,该信号终止了正在执行的命令。用交互方式使用shell可以看到这一点。

$ sh              确保运行Bourne shell

$ sh -c "sleep 30"

^C               键入中断符

$ echo $?            打印最后一条命令的终止状态

130

$ sh -c "sleep 30"

^\sh: 962 Quit - core dumped 键入退出符

$ echo $?            打印最后一条命令的终止状态

131

$ exit             离开Bourne shell

在所使用的系统中,SIGINT的值为2,SIGQUIT的值为3,于是给出shell终止状态130、131。

再试一个类似的例子,这一次将一个信号直接送给shell,然后观察system返回什么:

$ tsys "sleep 30" &      这一次在后台启动它

9257

$ ps -f             查看进程ID

UID PID PPID TTY TIME CMD

sar 9260 949 pts/5 0:00 ps -f

sar 9258 9257 pts/5 0:00 sh -c sleep 30

sar 949 947 pts/5 0:01 /bin/sh

sar 9257 949 pts/5 0:00 tsys sleep 30

sar 9259 9258 pts/5 0:00 sleep 30

$ kill -KILL 9258       杀死shell自身

abnormal termination, signal number = 9从中可见,仅当shell本身异常终止时,system的返回值才报告一个异常终止。

其他的shell在处理终端产生的信号(如SIGINT和SIGQUIT)时表现出来的行为各不相同。例如在bash和dash中,键入中断或退出符会导致带有对应信号编号的表示异常终止的退出状态。但是,如果发现正在执行sleep的进程并直接给它发送信号,这样信号只会到达单个进程而不是整个前台进程组。这些shell与Bourne shell类似,以正常终止状态128加上信号编号退出。

在编写使用system函数的程序时,一定要正确地解释返回值。如果直接调用fork、exec和wait,则终止状态与调用system是不同的。

10.19 函数sleep、nanosleep和clock_nanosleep

在本书的很多例子中都已使用了sheep函数,在图10-7程序和图10-8程序中有两个sleep的实现,但它们都是有缺陷的。

#include <unistd.h>

unsigned int sleep(unsigned int seconds);

返回值:0或未休眠完的秒数

此函数使调用进程被挂起直到满足下面两个条件之一。

(1)已经过了seconds所指定的墙上时钟时间。

(2)调用进程捕捉到一个信号并从信号处理程序返回。

如同alarm信号一样,由于其他系统活动,实际返回时间比所要求的会迟一些。

在第1种情形,返回值是0。当由于捕捉到某个信号sleep提早返回时(第2种情形),返回值是未休眠完的秒数(所要求的时间减去实际休眠时间)。

尽管sleep可以用alarm函数(见10.10节)实现,但这并不是必需的。如果使用alarm,则这两个函数之间可能相互影响。POSIX.1 标准对这些相互影响并未做任何说明。例如,若先调用alarm(10),过了3秒后又调用sleep(5),那么将如何呢?sleep将在5秒后返回(假定在这段时间内没有捕捉到另一个信号),但是否在2秒后又产生另一个SIGALRM信号呢?此细节与具体实现有关。

FreeBSD 8.0、Linux 3.2.0、Mac OS X 10.6.8和Solaris 10用nanosleep函数实现sleep,使sleep具体实现与信号和闹钟定时器相互独立。考虑到可移植性,不应对sleep的实现进行任何假定,但是如果混合调用sleep和其他与时间有关的函数,则需了解它们之间可能产生的交互。

实例

图10-29给出的是一个POSIX.1 sleep函数的实现。此函数是图10-7程序的修改版,它可靠地处理信号,避免了早期实现中的竞争条件,但是仍未处理与以前设置的闹钟的交互作用(正如前面提到的,POSIX.1并未显式地对这些交互进行定义)。

图10-29 sleep的可靠实现

与图10-7相比,为了可靠地实现sleep,图10-29的代码比较长。程序中没有使用任何形式的非局部转移(如图10-8中为了避免在alarm和pause之间的竞争条件所做的那样),所以对处理SIGALRM信号期间可能执行的其他信号处理程序没有任何影响。

nanosleep函数与sleep函数类似,但提供了纳秒级的精度。

#include <time.h>

int nanosleep(const struct timespec *reqtp, struct timespec *remtp);

返回值:若休眠到要求的时间,返回0;若出错,返回−1

这个函数挂起调用进程,直到要求的时间已经超时或者某个信号中断了该函数。reqtp参数用秒和纳秒指定了需要休眠的时间长度。如果某个信号中断了休眠间隔,进程并没有终止,remtp参数指向的 timespec 结构就会被设置为未休眠完的时间长度。如果对未休眠完的时间并不感兴趣,可以把该参数置为NULL。

如果系统并不支持纳秒这一精度,要求的时间就会取整。因为nanosleep函数并不涉及产生任何信号,所以不需要担心与其他函数的交互。

nanosleep函数过去属于Single UNIX Specification的定时器选项,现已被移至SUSv4的基础部分。

随着多个系统时钟的引入(回忆 6.10 节),需要使用相对于特定时钟的延迟时间来挂起调用线程。clock_nanosleep函数提供了这种功能。

#include <time.h>

int clock_nanosleep(clockid_t clock_id, int flags,

const struct timespec *reqtp, struct timespec *remtp);

返回值:若休眠要求的时间,返回0;若出错,返回错误码

clock_id参数指定了计算延迟时间基于的时钟。时钟标识符列于图6-8中。flags参数用于控制延迟是相对的还是绝对的。flags为0时表示休眠时间是相对的(例如,希望休眠的时间长度),如果flags值设置为TIMER_ABSTIME,表示休眠时间是绝对的(例如,希望休眠到时钟到达某个特定的时间)。

其他的参数reqtp和remtp,与nanosleep函数中的相同。但是,使用绝对时间时,remtp参数未使用,因为没有必要。在时钟到达指定的绝对时间值以前,可以为其他的clock_nanosleep调用复用reqtp参数相同的值。

注意,除了出错返回,调用

clock_nanosleep(CLOCK_REALTIME, 0, reqtp, remtp);

和调用

nanosleep(reqtp, remtp);

的效果是相同的。使用相对休眠的问题是有些应用对休眠长度有精度要求,相对休眠时间会导致实际休眠时间比要求的长。例如,某个应用程序希望按固定的时间间隔执行任务,就必须获取当前时间,计算下次执行任务的时间,然后调用nanosleep。在获取当前时间和调用nanosleep之间,处理器调度和抢占可能会导致相对休眠时间超过实际需要的时间间隔。即便分时进程调度程序对休眠时间结束后是否会马上执行用户任务并没有给出保证,使用绝对时间还是改善了精度。

在Single UNIX Specification的早期版本中,clock_nanosleep函数属于时钟选择选项,在SUSv4中,该函数已移至基础部分。

10.20 函数sigqueue

在10.8节中,我们介绍了大部分UNIX系统不对信号排队。在POSIX.1的实时扩展中,有些系统开始增加对信号排队的支持。在SUSv4中,排队信号功能已从实时扩展部分移至基础说明部分。

通常一个信号带有一个位信息:信号本身。除了对信号排队以外,这些扩展允许应用程序在递交信号时传递更多的信息(回忆10.14节)。这些信息嵌入在siginfo结构中。除了系统提供的信息,应用程序还可以向信号处理程序传递整数或者指向包含更多信息的缓冲区指针。

使用排队信号必须做以下几个操作。

(1)使用sigaction函数安装信号处理程序时指定SA_SIGINFO标志。如果没有给出这个标志,信号会延迟,但信号是否进入队列要取决于具体实现。

(2)在sigaction结构的sa_sigaction成员中(而不是通常的sa_handler字段)提供信号处理程序。实现可能允许用户使用sa_handler字段,但不能获取sigqueue函数发送出来的额外信息。

(3)使用sigqueue函数发送信号。

#include <signal.h>

int sigqueue(pid_t pid, int signo, const union sigval value);

返回值:若成功,返回0;若出错,返回−1

sigqueue函数只能把信号发送给单个进程,可以使用value参数向信号处理程序传递整数和指针值,除此之外,sigqueue函数与kill函数类似。

信号不能被无限排队。回忆图2-9和图2-11中的SIGQUEUE_MAX限制。到达相应的限制以后,sigqueue就会失败,将errno设为EAGAIN。

随着实时信号的增强,引入了用于应用程序的独立信号集。这些信号的编号在SIGRTMIN~SIGRTMAX之间,包括这两个限制值。注意,这些信号的默认行为是终止进程。

图10-30总结了排队信号在本书不同的实现中的行为上的差异。

Mac OS X 10.6.8并不支持sigqueue或者实时信号。在Solaris 10中,sigqueue在实时库librt中。

图10-30 不同平台上排队信号的行为

10.21 作业控制信号

在图10-1所示的信号中,POSIX.1认为有以下6个与作业控制有关。

SIGCHLD 子进程已停止或终止。

SIGCONT 如果进程已停止,则使其继续运行。

SIGSTOP 停止信号(不能被捕捉或忽略)。

SIGTSTP 交互式停止信号。

SIGTTIN 后台进程组成员读控制终端。

SIGTTOU 后台进程组成员写控制终端。

除SIGCHLD以外,大多数应用程序并不处理这些信号,交互式shell则通常会处理这些信号的所有工作。当键入挂起字符(通常是Ctrl+Z)时,SIGTSTP被送至前台进程组的所有进程。当我们通知shell在前台或后台恢复运行一个作业时,shell向该作业中的所有进程发送SIGCONT信号。与此类似,如果向一个进程递送了SIGTTIN或SIGTTOU信号,则根据系统默认的方式,停止此进程,作业控制shell了解到这一点后就通知我们。

一个例外是管理终端的进程,例如,vi(1)编辑器。当用户要挂起它时,它需要能了解到这一点,这样就能将终端状态恢复到 vi 启动时的情况。另外,当在前台恢复它时,它需要将终端状态设置回它所希望的状态,并需要重新绘制终端屏幕。可以在下面的例子中观察到与 vi 类似的程序是如何处理这种情况的。

在作业控制信号间有某些交互。当对一个进程产生 4 种停止信号(SIGTSTP、SIGSTOP、SIGTTIN或SIGTTOU)中的任意一种时,对该进程的任一未决SIGCONT信号就被丢弃。与此类似,当对一个进程产生SIGCONT信号时,对同一进程的任一未决停止信号被丢弃。

注意,如果进程是停止的,则SIGCONT的默认动作是继续该进程;否则忽略此信号。通常,对该信号无需做任何事情。当对一个停止的进程产生一个 SIGCONT 信号时,该进程就继续,即使该信号是被阻塞或忽略的也是如此。

实例

图10-31中的程序演示了当一个程序处理作业控制时通常所使用的规范代码序列。该程序只是将其标准输入复制到其标准输出,而在信号处理程序中以注释形式给出了管理屏幕的程序所执行的典型操作。

图10-31 如何处理SIGTSTP

当图10-31中的程序启动时,仅当SIGTSTP信号的配置是SIG_DFL,它才安排捕捉该信号。其理由是:当此程序由不支持作业控制的shell(如/bin/sh)启动时,此信号的配置应当设置为SIG_IGN。实际上,shell并不显式地忽略此信号,而是由init将这3个作业控制信号SIGTSTP、SIGTTIN和SIGTTOU设置为SIG_IGN。然后,这种配置由所有登录shell继承。只有作业控制shell才应将这3个信号重新设置为SIG_DFL。

当键入挂起字符时,进程接到 SIGTSTP 信号,然后调用该信号处理程序。此时,应当进行与终端有关的处理:将光标移到左下角、恢复终端工作方式等。在将SIGTSTP重置为默认值(停止该进程),并且解除了对此信号的阻塞之后,进程向自己发送同一信号SIGTSTP。因为正在处理 SIGTSTP 信号,而在捕捉该信号期间系统自动地阻塞它,所以应当解除对此信号的阻塞。到达这一点时,系统停止该进程。仅当某个进程(通常是正响应一个交互式fg命令的作业控制shell)向该进程发送一个 SIGCONT 信号时,该进程才继续。我们不捕捉 SIGCONT 信号。该信号的默认配置是继续运行停止的进程,当此发生时,此程序如同从 kill 函数返回一样继续运行。当此程序继续运行时,将SIGTSTP信号重置为捕捉,并且做我们所希望做的终端处理(如重新绘制屏幕)。

10.22 信号名和编号

本节介绍如何在信号编号和信号名之间进行映射。某些系统提供数组

extern char *sys_siglist[];

数组下标是信号编号,数组中的元素是指向信号名符串的指针。

FreeBSD 8.0、Linux 3.2.0和Mac OS X 10.6.8都提供这种信号名数组。Solaris 10也提供信号名数组,但该数组名是_sys_siglist。

可以使用psignal函数可移植地打印与信号编号对应的字符串。

#include <signal.h>

void psignal(int signo, const char *msg);

字符串msg(通常是程序名)输出到标准错误文件,后面跟随一个冒号和一个空格,再后面对该信号的说明,最后是一个换行符。如果msg为NULL,只有信号说明部分输出到标准错误文件,该函数类似于perror(1.7节)。

如果在sigaction信号处理程序中有siginfo结构,可以使用psiginfo函数打印信号信息。

#include <signal.h>

void psiginfo(const siginfo_t *info, const char *msg);

它的工作方式与 psignal 函数类似。虽然这个函数访问除信号编号以外的更多信息,但不同的平台输出的这些额外信息可能有所不同。

如果只需要信号的字符描述部分,也不需要把它写到标准错误文件中(如可以写到日志文件中),可以使用strsignal函数,它类似于strerror(另见1.7节)。

#include <string.h>

char *strsignal(int signo);

返回值:指向描述该信号的字符串的指针

给出一个信号编号,strsignal 将返回描述该信号的字符串。应用程序可用该字符串打印关于接收到信号的出错消息。

本书讨论的所有平台都提供psignal和strsignal函数,但相互之间有些差别。在Solaris 10中,若信号编号无效,strsignal将返回一个空指针,而FreeBSD 8.0、Linux 3.2.0和Mac OS X 10.6.8则返回一个字符串,它指出信号编号是不可识别的。

只有Linux 3.2.0和Solaris 10支持psiginfo函数。

Solaris提供一对函数,一个函数将信号编号映射为信号名,另一个则反之。

#include <signal.h>

int sig2str(int signo, char *str);

int str2sig(const char *str, int *signop);

两个函数的返回值:若成功,返回0;若出错,返回−1

在编写交互式程序,其中需接收和打印信号名和信号编号时,这两个函数是有用的。

sig2str函数将给定信号编号翻译成字符串,并将结果存放在str指向的存储区。调用者必须保证该存储区足够大,可以保存最长字符串,包括终止 null 字节。Solaris 在<signal.h>中包含了常量SIG2STR_MAX,它定义了最大字符串长度。该字符串包括不带“SIG”前缀的信号名。例如,SIGKILL被翻译为字符串“KILL”,并存放在str指向的存储缓冲区中。

str2sig 函数将给出的信号名翻译成信号编号。该信号编号存放在signop指向的整型中。名字要么是不带“SIG”前缀的信号名,要么是表示十进制信号编号的字符串(如“9”)。

注意,sig2str和str2sig与常用的函数做法不同,当它们失败时,并不设置errno。

10.23 小结

信号用于大多数复杂的应用程序中。理解进行信号处理的原因和方式对于高级UNIX编程极其重要。本章对UNIX信号进行了详细而且比较深入的介绍。首先说明了早期信号实现的问题以及它们是如何显现出来的。然后介绍了POSIX.1的可靠信号概念以及所有相关的函数。在此基础上提供了abort、system和sleep函数的POSIX.1实现。最后以观察分析作业控制信号以及信号名和信号编号之间的转换结束。

习题

10.1 删除图10-2程序中的for(;;)语句,结果会怎样?为什么?

10.2 实现10.22节中说明的sig2str函数。

10.3 画出运行图10-9程序时的栈帧情况。

10.4 图10-11程序中利用setjmp和longjmp设置I/O操作的超时,下面的代码也常见用于此种目的:

signal(SIGALRM, sig_alrm);

alarm(60);

if (setjmp(env_alrm) != 0) {

/* handle timeout */

.

}

..

这段代码有什么错误?

10.5 仅使用一个定时器(alarm或较高精度的setitimer),构造一组函数,使得进程在该单一定时器基础上可以设置任一数量的定时器。

10.6 编写一段程序测试图 10-24中父进程和子进程的同步函数,要求进程创建一个文件并向文件写一个整数 0,然后,进程调用 fork,接着,父进程和子进程交替增加文件中的计数器值,每次计数器值增加1时,打印是哪一个进程(子进程或父进程)进行了该增加1操作。

10.7 在图10-25中,若调用者捕捉了SIGABRT并从该信号处理程序中返回,为什么不是仅仅调用_exit,而要恢复其默认设置并再次调用kill?

10.8 为什么在siginfo结构(见10.14节)的si_uid字段中包括实际用户ID而非有效用户ID?

10.9 重写图10-14中的函数,要求它处理图10-1中的所有信号,每次循环处理当前信号屏蔽字中的一个信号(并不是对每一个可能的信号都循环一次)。

10.10 编写一段程序,要求在一个无限循环中调用sleep(60)函数,每5分钟(即5次循环)取当前的日期和时间,并打印tm_sec字段。将程序执行一晚上,请解释其结果。有些程序,如cron守护进程,每分钟运行一次,它是如何处理这类工作的?

10.11 修改图3-5的程序,要求:(a)将BUFFSIZE改为100;(b)用signal_intr函数捕捉SIGXFSZ信号量并打印消息,然后从信号处理程序中返回;(c)如果没有写满请求的字节数,则打印write的返回值。将软资源限制RLIMIT_FSIZE(见7.11节)更改为1 024字节(在shell中设置软资源限制,如果不行就直接在程序中调用setrlimit),然后复制一个大于1 024字节的文件,在各种不同的系统上运行新程序,其结果如何?为什么?

10.12 编写一段调用fwrite的程序,它使用一个较大的缓冲区(约1 GB),调用fwrite前调用alarm使得1 s以后产生信号。在信号处理程序中打印捕捉到的信号,然后返回。fwrite可以完成吗?结果如何?