第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已经把实时信号接 口移至基础规范说明中。
在头文件
实际上,实现将各信号定义在另一个头文件中,但是该头文件又包 括在
不存在编号为 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种方式之一进行处理, 我们称之为信号的处理或与信号相关的动作。
(2)捕捉信号。为了做到这一点,要通知内核在某种信号发生 时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进 行的处理。例如,若正在编写一个命令解释器,它将用户的输入解释为 命令并执行之,当用户用键盘产生中断信号时,很可能希望该命令解释 器返回到主循环,终止正在为该用户执行的命令。如果捕捉到 SIGCHLD 信号,则表示一个子进程已经终止,所以此信号的捕捉函数 可以调用waitpid以取得该子进程的进程ID以及它的终止状态。又例如, 如果进程创建了临时文件,那么可能要为 SIGTERM 信号编写一个信号 捕捉函数以清除临时文件(SIGTERM 是终止信号,kill 命令传送的系统 默认信号是终止信号)。注意,不能捕捉SIGKILL和SIGSTOP信号。
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 指示一个实现定义的硬件故障。
SIGFPE 此信号表示一个算术运算异常,如除以0、浮点溢出等。
SIGFREEZE 此信号仅由Solaris定义。它用于通知进程在冻结系统 状态之前需要采取特定动作,例如当系统进入休眠或挂起状态时可能需 要做这种处理。
SIGHUP 如果终端接口检测到一个连接断开,则将此信号送给与该
终端相关的控制进程(会话首进程)。见图9-13,此信号被送给session 结构中s_leader字段所指向的进程。仅当终端的CLOCAL标志没有设置 时,在上述条件下才产生此信号。(如果所连接的终端是本地的,则设 置该终端的CLOCAL标志。它告诉终端驱动程序忽略所有调制解调器的 状态行。第18章将说明如何设置此标志。)
SIGILL 此信号表示进程已执行一条非法硬件指令。
SIGINFO 这是一种BSD信号,当用户按状态键(一般采用Ctrl+T) 时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程(见 图 9-9)。此信号通常造成在终端上显示前台进程组中各进程的状态信 息。
SIGINT 当用户按中断键(一般采用 Delete 或 Ctrl+C)时,终端驱 动程序产生此信号并发送至前台进程组中的每一个进程(见图9-9)。 当一个进程在运行时失控,特别是它正在屏幕上产生大量不需要的输出 时,常用此信号终止它。
SIGKILL 这是两个不能被捕捉或忽略信号中的一个。它向系统管理
员提供了一种可以杀死任一进程的可靠方法。
SIGQUIT 当用户在终端上按退出键(一般采用Ctrl+)时,中断驱 动程序产生此信号,并发送给前台进程组中的所有进程(见图9-9)。 此信号不仅终止前台进程组(如SIGINT所做的那样),同时产生一个 core文件。
SIGSEGV 指示进程进行了一次无效的内存引用(通常说明程序有 错,比如访问了一个未经初始化的指针)。
SIGSTOP 这是一个作业控制信号,它停止一个进程。它类似于交 互停止信号(SIGTSTP),但是SIGSTOP不能被捕捉或忽略。
SIGKILL 和SIGSTOP 不能被忽略和捕捉
SIGTERM 这是由kill(1)命令发送的系统默认终止信号。由于该信号 是由应用程序捕获的,使用SIGTERM也让程序有机会在退出之前做好 清理工作,从而优雅地终止(相对于SIGKILL而言。SIGKILL不能被捕 捉或者忽略)。
10.3 signal函数
函数signal
UNIX系统信号机制最简单的接口是signal函数。
#include
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 函数定义
所以在 4.4BSD 之下使用它提供 新的可靠信号语义。目前大多数系统遵循这种策略,但Solaris 10沿用 System V signal函数的语义。
signo参数是图10-1中的信号名。func的值是常量SIG_IGN、常量 SIG_DFL或当接到此信号后要调用的函数的地址。如果指定SIG_IGN, 则向内核表示忽略此信号(记住有两个信号SIGKILL和SIGSTOP不能忽略
当指定函数地址时,则在信号发生时,调 用该函数,我们称这种处理为捕捉该信号,称此函数为信号处理程序 (signal handler)或信号捕捉函数(signal-catching function)。
#include
#include
#include
#include
void add(int x) {
std::cout << "use signal slover" << std::endl;
exit(0);
}
int main() {
/*----------------------------------- test signal ----------------------------------------*/
void (*f)(int x) = add;
signal(SIGINT, f);
for (int i = 0; i < 10; ++i) {
std::cout << "hello" << std::endl;
sleep(1);
}
return 0;
}
我们可以捕捉信号进行处理,或者忽略信号
捕捉信号SIGTSTP
#include
#include
#include
#include
void add(int x) {
std::cout << "use signal slover" << std::endl;
std::cout << "catch SIGTSTP" << std::endl;
exit(0);
}
int main() {
/*----------------------------------- test signal ----------------------------------------*/
void (*f)(int x) = add;
signal(SIGTSTP, f);
for (int i = 0; i < 10; ++i) {
std::cout << "hello" << std::endl;
sleep(1);
}
return 0;
}
运行时像进程kill -TSTP pid或者输入ctrl + z即可捕捉到
或者忽略指定信号(SIGKILL 和SIGSTOP不能忽略)
10.5 中断的系统调用
早期UNIX系统的一个特性是:如果进程在执行一个低速系统调用 而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该 系统调用返回出错,其errno设置为EINTR。这样处理是因为一个信号发 生了,进程捕捉到它,这意味着已经发生了某种事情,所以是个好机会 应当唤醒阻塞的系统调用。
在这里,我们必须区分系统调用和函数。当捕捉到某个信号时,被 中断的是内核中执行的系统调用。
为了支持这种特性,将系统调用分成两类:低速系统调用和其他系 统调用。低速系统调用是可能会使进程永远阻塞的一类系统调用,包 括:
•如果某些类型文件(如读管道、终端设备和网络设备)的数据不 存在,则读操作可能会使调用者永远阻塞;
•如果这些数据不能被相同的类型文件立即接受,则写操作可能会 使调用者永远阻塞;
•在某种条件发生之前打开某些类型文件,可能会发生阻塞(例如 要打开一个终端设备,需要先等待与之连接的调制解调器应答);
•pause函数(按照定义,它使调用进程休眠直至捕捉到一个信号) 和wait函数;
•某些ioctl操作;
•某些进程间通信函数
在这些低速系统调用中,一个值得注意的例外是与磁盘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列出了几种实现所提供的与信号有关的函数及它们的语义。
应当了解,其他厂商提供的UNIX系统可能不同于图10-3中所示的 情况。例如,SunOS 4.1.2中的sigaction默认方式是重启动被中断的系统 调用,这与列在图10-3中的各平台不同。
在图10-18中,提供了我们自己的signal函数版本,它自动地尝试重 启动被中断的系统调用(除 SIGALRM信号外)。在图10-19中则提供了 另一个函数signal_intr,它不进行重启动。
在14.4节说明select和poll函数时,还将更多涉及被中断的系统调 用。
进程捕捉到信号并对其进行处理时,进程正在执行的正常指令序列 就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如 果从信号处理程序返回(例如没有调用 exit 或longjmp),则继续执行在 捕捉到信号时进程正在执行的正常指令序列(这类似于发生硬件中断时 所做的)。但在信号处理程序中,不能判断捕捉到信号时进程执行到何 处。如果进程正在执行malloc,在其堆中分配另外的存储空间,而此时 由于捕捉到信号而插入执行该信号处理程序,其中又调用malloc,这时 会发生什么?又例如,若进程正在执行getpwnam(见6.2节)这种将其结 果存放在静态存储单元中的函数,其间插入执行信号处理程序,它又调 用这样的函数,这时又会发生什么呢?在malloc例子中,可能会对进程 造成破坏,因为malloc通常为它所分配的存储区维护一个链表,而插入 执行信号处理程序时,进程可能正在更改此链表。在getpwnam的例子 中,返回给正常调用者的信息可能会被返回给信号处理程序的信息覆 盖。
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.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才可以。)
我们需要先定义一些在讨论信号时会用到的术语。首先,当造成信 号的事件发生时,为进程产生一个信号(或向一个进程发送一个信 号)。事件可以是硬件异常(如除以 0)、软件条件(如alarm 定时器超 时)、终端产生的信号或调用kill 函数。当一个信号产生时,内核通常 在进程表中以某种形式设置一个标志。
当对信号采取了这种动作时,我们说向进程递送了一个信号。在信 号产生(generation)和递送(delivery)之间的时间间隔内,称信号是未 决的(pending)。
进程可以选用“阻塞信号递送”。如果为进程产生了一个阻塞的信 号,而且对该信号的动作是系统默认动作或捕捉该信号,则为该进程将 此信号保持为未决状态,直到该进程对此信号解除了阻塞,或者将对此 信号的动作更改为忽略。内核在递送一个原来被阻塞的信号给进程时 (而不是在产生该信号时),才决定对它的处理方式。于是进程在信号 递送给它之前仍可改变对该信号的动作。进程调用sigpending函数(见 10.13节)来判定哪些信号是设置为阻塞并处于未决状态的。
如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,那 么将如何呢?POSIX.1允许系统递送该信号一次或多次。如果递送该信 号多次,则称这些信号进行了排队。但是除非支持POSIX.1实时扩展, 否则大多数UNIX并不对信号排队,而是只递送这种信号一次。
10.9 函数kill和raise
kill函数将信号发送给进程或进程组。raise函数则允许进程向自身发 送信号。
raise最初是由ISO C定义的。后来,为了与ISO C标准保持一致, POSIX.1也包括了该函数。但是POSIX.1扩展了raise的规范,使其可处 理线程(12.8中讨论线程如何与信号交互)。
因为ISO C并不涉及多进程,所以它不能定义以进程ID作为其参数 (如kill函数)的函数。
#include
int kill(pid_t pid, int signo); int raise(int signo);
调用raise(signo);
等价于调用两个函数返回值:若成功,返回0;若出错,返回−1
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,则进程可将它发送 给属于同一会话的任一其他进程。
10.11 信号集
我们需要有一个能表示多个信号——信号集(signal set)的数据类 型。我们将在sigprocmask (下一节中说明)类函数中使用这种数据类 型,以便告诉内核不允许发生该信号集中的信号。如前所述,不同的信 号的编号可能超过一个整型量所包含的位数,所以一般而言,不能用整 型量中的一位代表一种信号,也就是不能用一个整型量表示信号集。 POSIX.1定义数据类型sigset_t以包含一个信号集,并且定义了下列5个处 理信号集的函数。
include
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。这两个函数可以在
#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 函数sigprocmask
10.8节曾提及一个进程的信号屏蔽字规定了当前阻塞而不能递送给 该进程的信号集。调用函数sigprocmask可以检测或更改,或同时进行检 测和更改进程的信号屏蔽字。
#include
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 函数sigpending
sigpending函数返回一信号集,对于调用进程而言,其中的各信号是 阻塞不能递送的,因而也一定是当前未决的。该信号集通过set参数返 回。
#include
int sigpending(sigset_t *set);
返回值:若成功,返回0;若出错,返回−1
#include
#include
#include
#include
#include
static void sig_quit(int signo) {
std::cout << "sigprocess, sigpending" << std::endl;
}
int main() {
sigset_t newmask, oldmask, pendmask;
if(signal(SIGTSTP, sig_quit) == SIG_ERR) {
printf("signal err");
}
/* Block SIGQUIT. */
sigemptyset(&newmask);
sigaddset(&newmask, SIGTSTP);
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
printf("SIG Block error\n");
}
sleep(5);
if(sigpending(&pendmask) < 0) {
printf("sigpending error");
}
if(sigismember(&pendmask, SIGTSTP)) {
printf("sigpending : SIGTSTP");
}
}
10.14 函数sigaction
sigaction函数的功能是检查或修改(或检查并修改)与指定信号相 关联的处理动作。此函数取代了UNIX早期版本使用的signal函数。在本 节末尾用sigaction函数实现了signal。
#include
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 */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”。
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
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节)。
10.16sigsuspend
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
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.17 abort
10.17 函数abort
前面已提及abort函数的功能是使程序异常终止。
#include
void abort(void);
此函数不返回值 此函数将SIGABRT信号发送给调用进程(进程不应忽略此信号)。
ISO C规定,调用abort将向主机环境递送一个未成功终止的通知,其方 法是调用raise(SIGABRT)函数。
10.18 system 函数
10.19 sleep nanosleep和clock_nanosleep
在本书的很多例子中都已使用了sheep函数,在图10-7程序和图10-8 程序中有两个sleep的实现,但它们都是有缺陷的。
#include
unsigned int sleep(unsigned int seconds);
返回值:0或未休眠完的秒数 此函数使调用进程被挂起直到满足下面两个条件之一。
(1)已经过了seconds所指定的墙上时钟时间。 (2)调用进程捕捉到一个信号并从信号处理程序返回。
#include
int nanosleep(const struct timespec *reqtp, struct timespec *remtp);
返回值:若休眠到要求的时间,返回0;若出错,返回−1 这个函数挂起调用进程,直到要求的时间已经超时或者某个信号中 断了该函数。reqtp参数用秒和纳秒指定了需要休眠的时间长度。如果某
个信号中断了休眠间隔,进程并没有终止,remtp参数指向的 timespec 结构就会被设置为未休眠完的时间长度。如果对未休眠完的时间并不感 兴趣,可以把该参数置为NULL。
如果系统并不支持纳秒这一精度,要求的时间就会取整。因为 nanosleep函数并不涉及产生任何信号,所以不需要担心与其他函数的交 互。
随着多个系统时钟的引入(回忆 6.10 节),需要使用相对于特定时 钟的延迟时间来挂起调用线程。clock_nanosleep函数提供了这种功能。
#include
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之间,处理器调度和抢占可能会导致相对休眠时间超过实际需 要的时间间隔。即便分时进程调度程序对休眠时间结束后是否会马上执 行用户任务并没有给出保证,使用绝对时间还是改善了精度。
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
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.22 信号名和编号
本节介绍如何在信号编号和信号名之间进行映射。某些系统提供数
组
extern char *sys_siglist[];
数组下标是信号编号,数组中的元素是指向信号名符串的指针。 FreeBSD 8.0、Linux 3.2.0和Mac OS X 10.6.8都提供这种信号名
数组。Solaris 10也提供信号名数组,但该数组名是_sys_siglist。
可以使用psignal函数可移植地打印与信号编号对应的字符串。
#include
void psignal(int signo, const char *msg);
字符串msg(通常是程序名)输出到标准错误文件,后面跟随一个
冒号和一个空格,再后面对该信号的说明,最后是一个换行符。如果 msg为NULL,只有信号说明部分输出到标准错误文件,该函数类似于 perror(1.7节)。
如果在sigaction信号处理程序中有siginfo结构,可以使用psiginfo函数 打印信号信息。
#include
void psiginfo(const siginfo_t *info, const char *msg);
它的工作方式与 psignal 函数类似。虽然这个函数访问除信号编号以
外的更多信息,但不同的平台输出的这些额外信息可能有所不同。 如果只需要信号的字符描述部分,也不需要把它写到标准错误文件
中(如可以写到日志文件中),可以使用strsignal函数,它类似于 strerror(另见1.7节)。
#include
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
int sig2str(int signo, char *str);
int str2sig(const char *str, int *signop);
两个函数的返回值:若成功,返回0;若出错,返回−1 在编写交互式程序,其中需接收和打印信号名和信号编号时,这两
个函数是有用的。 sig2str函数将给定信号编号翻译成字符串,并将结果存放在str指向
的存储区。调用者必须保证该存储区足够大,可以保存最长字符串,包 括终止 null 字节。Solaris 在
str2sig 函数将给出的信号名翻译成信号编号。该信号编号存放在 signop指向的整型中。名字要么是不带“SIG”前缀的信号名,要么是表 示十进制信号编号的字符串(如“9”)。
注意,sig2str和str2sig与常用的函数做法不同,当它们失败时,并不 设置errno。