第十章:信号
101〓引言
信号是软件中断。很多比较重要的应用程序都需处理信号。信号提供了一种处理异
步事件的方法:终端用户键入中断键,则会通过信号机构停止一道程序.Unix的早期
版本,就已经有信号机构,但是这些系统,例如Version 7所提供的信号模型并不可
靠。信号可能被丢失,而且在执行临界区代码时,进程难于关闭所选择的信号.
4.3BSD和SVR3对信号模型都作了更改,增加了可靠信号机制。但是这两种更改之间并
不兼容。幸运的是POSIX.1对可靠信号例程进行了标准化,这正是本章所说明的.本章
先对信号机制进行综述,并说明每种信号的一般用法。然后分析早期实现的问题。在
分析存在的问题之后再说明解决这些问题的方法,这样有助于加深对改进机制的理解.
本章也包含了很多并非100%正确的实例,这样做的目的是为了对其不定之处进行讨论。
102〓信号的概念
首先,每个信号有一个名字。这些名字都以三个字符SIG开头。例如,SIGABRT是夭折
信号,当进程调用abort函数时产生这种信号。SIGALRM是闹钟信号,当由alarm函数设
置的时间已经超过后产生此信号。Version7有十五种不同的信号;SVR4和4.3BSD两者
都有31种不同的信号。
在头文件<signal.h>中,这些信号都被定义为正整数(信号编号)。没有一个信号其编
号为0.在10.9节中我们将会看到kill函数,对信号编号0有特殊的应用。POSIX.1将此
种信号编号值称为定信号。很多条件可以产生一个信号。
·当用户按某些终端键时,产生信号。在终端上按DELETE键通常产生中断信号(SIGINT).
这是停止一道已失去控制程序的方法。(第十一章中将说明此信号可被映照为终端上
的任一字符)。
·硬件异常产生信号:除数为0,无效的存储访问等等。这些条件通常是由硬件检测到的
并将其通知系统核。然后系统核为该条件发生时正在运行的进程产生适当的信号。
例如,对执行一个无效存储访问的进程产生一个SIGSEGV。
·进程用kill(2)函数可将信号发送给另一个进程或进程组。自然,有些限制:接收信号
进程和发送信号进程的属主必须相同,或发送信号进程的属主必须是超级用户。
·用户可用kill(1)命令将信号发送给其它进程。此程序是kill函数的界面。常用此命令
终止一个失控的后台进程。
·当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。例如SIGURG
(在网络连接上传来非规定波特率的数据)、SIGPIPE(在管道的读进程已终止后一个进
程写此管道),以及SIGALRM(进程所设置的闹钟时间已经超时)。
信号是异步事件的经典实例。产生信号的事件对进程而言是在随机时间出现的。进程
不能只是测试一个变量(例如errno)来判别是否发生了一个信号,代之以进程必须告诉系
统核"在此信号发生时,请执行下列操作"。可以要求系统在某个信号出现时按照下列三种
方式中的一种进行操作。
1.忽略此信号。大多数信号都可使用这种方式进行处理,但有两种信号却决不能被忽略。
它们是:SIGKILL和SIGSTOP。这两种信号不能被忽略的原因是:它们向超级用户提供一种
使进程终止(killing)或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(例
如非法存储访问或除以0),则进程的行为是末定义的。
2.捕捉信号。为了做到这一点要通知系统核在某种信号发生时,要调用一个用户函数。在
用户函数中,可执行用户希望对这种事件进行的处理。例如,若我们编写一个命令解释器,
当用户用键盘产生中断信号时,我们很可能希望返回到程序的主循环,终止系统正在为该
用户执行的命令。如果捕捉到SIGCHLD信号,则表示子进程已经终止,所以此信号的捕捉
函数可以调用Waitpid以取得该子进程的进程ID以及它的终止状态。又例如,如果进程创
建了临时文件,那么我们可能要为SIGTERM函数编写一个信号捕捉函数以清除临时文件
(kill命令传送的系统默认信号是终止信号。)
3.执行系统默认动作。在图10.1中示出了对每一种信号的系统默认动作。注意,对大多数
信号的系统默认动作是终止该进程。图10.1列出所有信号的名字,哪些系统支持此信号以
及对于信号的系统默认动作。在POSIX.1列。表示要求此种信号。job表示这是作业控制信
号(仅当支持作业控制时,才要求此种信号)。
在系统默认动作列,"终止w/core"表示在进程当前工作目录的Core文件中复制了该进程
的存储图象。(该文件名为core,从此可以看出这种功能很久之前就是Unix功能的一部分).
大多数Unix调试程序都使用core文件以检查进程在终止时的状态。在下列条件下不产生
core文件:(a)进程是设置-用户-ID的,而且当前用户并非是程序文件的属主,或者(b)进
程是设置一组-ID的,而且当前用户并非是该程序文件的组属主,或者(C)用户没有写当
前工作目录的许可权,或者(d)文件太大(回忆7.11节中的RLIMIT-CORE)。core文件的许
可权(假定该文件在此之前并不存在)通常是用户读/写,组读和其它读.core文件的产生
不是POSIX.1所属部分,而是很多Unix版本的实现特征。4.3+BSD产生名为core.prog的文
件,其中prog是被执行的程序名的前16个字符。它对core文件给予了某种标识,所以是
一种改进特证。在图10.1中的说明列,"硬件故障"对应于实现定义的硬件故障。这些名
字中有很多取自于Unix早先在PPP-11上的实现。请查看你所使用的系统的手册,以确切
地确定这些信号对应于哪些错误类型。
图10.1 Unix信号
下面比较详细地说明这些信号。
SIGABRT 调用abort函数时(1017节)产生此信号。该进程异常终止。
SIGALRM 调用alarm函数时设置的时间已经超过.详细情况见10.10节.看由setitimer(2)
函数设置的间隔时间已经过时,那么也产生此信号。
SIGBUS 这指示一个实现定义的硬件故障。
SIGCHLD 在一个进程终止或停止时,SIGCHLD信号被送给其父进程。按系统默认,将忽略
此信号。如果父进程希望了解其子进程的这种状态改变,则应捕捉此信号。信
号捕捉函数中通常要调用wait函数以取得子进程ID和其终止状态。系统V的早期
版本有一个名为SIGCLD(无H)的类似信号。这一信号具有非标准的语义,在SVR2
的手册页就警告在新的程序中尽量小要使用这种信号。应用程序应当使用标准的
SIGCHLD信号。在10.7节中讨论这两个信号。
SIGCONT 此作业控制信号送给需要其继续运行的处于停止状态的进程。如果接收到此信号
的进程处于停止状态,则系统默认动作是使该进程继续运行,否则默认动作是忽
略此信号。例如,vi编辑程序在捕捉到此信号后,重画终端屏幕。关于进一步的
情况见1020节。
SIGEMT 这指示一个实现定义的硬件故障。
SIGFPE 此信号表示一个算术运算异常,例如除以0,浮点溢出等。
SIGHUP 如果终端界面检测到一个连接断开,则将此信号送给与该终端相关的控制进程(对
话期首进程)。参见图9.11,此信号送给session结构中s-leader字段所指向的进
程。仅当终端的CLOCAL标志没有设置时,在上述条件下才产生此信号。(如果所连
接的终端是本地的,才设置该终端的CLOCAL标志。它告诉终端驱动程序忽略所有
调制解调器的状态行。在第十一章将说明如何设置此标志)注意,接到此信号的
对话期首进程可能在后台,作为一个例子见图97。这区别于通常由终端产生的
信号(中断、退出和挂起),这些信号总是传递给前台进程组。如果对话期前进程
终止,则也产生此信号。在这种情况,此信号送给前台进程组中的每一个进程。
通常用此信号通知精灵进程(第十三章)以再读它们的配置文件。为此选用SIGHUP
的理由是,因为一个精灵进程不会有一个控制终端,而且通常决不会接收到这种
信号。SIGKILL 此信号指示进程已执行一条非法硬件指令。4.3BSD由abort函数产
生此信号。SIGABRT现在用于这种情况。
SIGINFO 这是一种4.3+BSD信号,当用户按状态键(常常是Control-T)时,终端驱动程序产
生此信号并送至前台进程组中的每一个进程(见图98)。此信号通常造成在终
端上显示前台进程组中各进程的状态信息。
SIGINT 当用户按中断键(常常是DELETE或Control-C)时,终端驱动程序产生此信号
并送至前台进程组中的每一个进程(见图98)。当一个进程在运行时失控,特别
是它正在屏幕上产生大量不需要的输出时,常用此信号终止它。
SIGIO 此信号指示一个异步I/O事件。在12.6.2中将对此进行讨论。在图10.1中,对SIGIO
的系统默认动作是终止或忽略.不幸的是,这依赖于系统.在SVR4中,SIGIO与SIGPOLL
相同,其默认动作是终止此进程。在43+BSD中(此信号起源于4.2BSD),其默认动
作是忽略它。
SIGIOT 这指示一个实现定义的硬件故障。IOT这个名字来自于PPP-11对于输入/输出TRAP
(input/output TRAP)指令的缩写。系统V的早期版本,由abort函数产生此信号.
SIGABRT现用于这些情况。
SIGKILL 这是两个不能被捕捉或忽略信号中的一个。它向系统管理员提供了一种可以消灭
任一进程的可靠方法。
SIGPIPE 如果在读进程已终止时写管道,则产生信号SIGPIPE。在14.2节中将说明管道。
当套接口的一端已经终止时,一个进程写该插口也产生此信号。
SIGPOLL 这是一种SVR4信号,当在一个可轮询设备上发生一特定事件时产生此信号。在
12.5.2节中将说明poll函数和此信号.它与4.3+BSD的SIGIO和SIGURG信号相接近.
SIGPROF 将setitimer(2)函数设置的梗概统计 间隔时间已经超过时产生此信号。
SIGPWR 这是一种SVR4信号,它依赖于系统。它主要用于具有不间断电源(UPS)的系统上。
如果电源失效,则UPS就会起作用,而且通常软件会接到通知。在这种情况下,
系统依靠蓄电池电源继续运行,所以无须作任何处理。但是如果蓄电池也将不能
支持工作,则软件通常会再次接到通知,此时,它在15~30秒内使系统各部分
都停止运行。此时应当传递SIGPWR信号。在大多数系统中使接到蓄电池电压过低
的进程将信号SIGPWG发送给init进程,然后由init处理停机操作。很多系统V的
init实现在inittab文件中提供了两个记录项用于此种目的;powerfail以及
powerwait.目前已能获得低价格的UPS系统,它用RS-232串行连接能够很容易地将
蓄电池电压过低的条件通知系统,于是这种信号也就更加重要了。
SIGQUIT 当用户在终端上按退出键(常常是Control-\)时,产生此信号,并送至前台进程
组中的所有进程(见图9.8)。此信号不仅终止前台进程组(如SIGINT所做的那样),
它也产生一个core文件。
SIGSEGV 此信号指示进程进行了一次无效的存储访问。名字SEG表示"段违例"
("segmentation violation")。
SIGSTOP 这是一个作业控制信号,它停止一个进程。它类似于交互停止信号(SIGTSTP),
但是SIGSTOP不能被捕促或忽略。
SIGSYS 这指示一个无效的系统调用。由于某种未知原因,进程执行了一条系统调用指令,
但其指示系统调用类型的参数却是无效的。
SIGTERM 这是由kill(1)命令发送的系统默认终止信号。
SIGTRAP 这指示一个实现定义的硬件故障。
此信号名来自于PPP-11的TRAP指令。
SIGTSTP 这是交互停止信号,当用户在终端上按挂起键*(常常是Control-Z)时,终
端驱动程
序产生此信号。
SIGTTIN 当一个后台进程组进程试图读其控制终端时,终端驱动程序产生此信号。
(请参见9
8节中对此问题的讨论)。在下列例外情形下,不产生此信号,此时读操作出错返
回,errn
o设置为EIO:(a)读进程忽略或阻塞此信号,式(b)读进程所属的进程组是孤儿进程
组。
SIGTTOU 当一个后台进程组进程试图写其控制终端时产生此信号(请参见98节对
此问题的
讨论。)与上面所述的SIGTTIN信号不同,一个进程可以选择为允许后台进程写控制
终端。在
第十一章中将讨论如何更改此选择项。如果不允许后台进程写,则与SIGTTIN相似
也有两种
特殊情况:(a)写进程忽略或阻塞此信号,式(b)写进程所属进程组是孤儿进程组。
在这两种
情况下不产生此信号,写操作出错返回,errno设置为EIO。不论是否允许后台进程
写,某些
除写以外的下列终端操作也能产生此信号:tcsetatlr,tcsendbreak,tcdrain,tcf
lush,tcfl
ow以及tcsetpgrp。在第十一章将说明这些终端操作。
SIGURG 此信号通知进程已经发生一个紧急情况。在网络连接上,接到非规定波特
率的数据
时,此信号是可选择地产生的。
SIGUSR1 这是一个用户定义的信号,可用于应用程序。
SIGUSR2 这是一个用户定义的信号,可用于应用程序。
SIGVTALRM 当一个由setitimer(2)函数设置的虚拟间隔时间已经超过时产生此信号
。
SIGWINCH SVR4和43+BSD系统核保持与每个终端或伪终端相关联的*不幸的是语术
停止(st
op)有不同的意义。在讨论作业控制和信号时我们需提及停止(stopping)和继续作
业。但是
终端驱动程序一直用术语停止表示用Control-S和Control-Q字符停止和起动终输出
。因此,
终端驱动程序将产生交互停止信号和字符称之为挂起字符(suspend)而非停止字符
。
窗口的大小、一个进程可以用ioctl函数(见1112节)得到或设置窗口的大小。如
果一个进
程用ioctl的设置-窗口-大小命令更改了窗口大小,则系统核将SIGWINCH信号送至
在前台进
程组。
SIGXCPU SVR4和43+BSD支持资源限制的概念(见711节)。如果进程超过了其软
CPU时间限
制,则产生SIGXCPU信号。
SIGXFSZ 如果进程超过了其软文件长度限制(见711节),则SVR4和43+BSD产生
此信号。
103〓signal函数
Unix信号机制的最简单界面是signal函数
#include<signalh>
void (*signal(int signo,void(*func)(int)>>(int);
返回:以前的信号处理配置,出错时SIG-ERR
signal函数是由ANSIC定义的。因为ANSIC不涉及多进程、进程组、终端I/O等,
所以它对
信号的定义非常含糊,以致于对Unix系统而言几乎毫无用处。确实,ANSIC对信号
的说明只用了2页,而POSIX1的说明则用了15页。SVR4也提供signal函数,该
函数可提供老的SVR2不可靠信号语义(在104节中说明这些老的语义)。提供此函
数主要是为了向下兼容要求此老语义的应用程序,新应用程序不应使用它。
43+BSD也提供Signal函数,但是它是用sigaction函数实现的(在1014节中说明
sigaction函数),所以在43+BSD之下使用它提供新的可靠的信号语义。在本书中
用的signal函数都是程序1012中用sigaction实现的该函数。signo参数是图10
中的信号名。func的值是(a)常数SIG-IGN,或(b)常数SIG-DFL,或(c)当接到此信
号后要调用的函数的地址。如果指定SIG-IGN,则向系统核表示要忽略此信号。(要
记住有两个信号SIGKILL和SIGSTOP是不能忽略的。)如果指定SIG-DFL,则表示接到
此信号后的动作是系统默认动作(见图101中的最后1列)。当指定函数地址后,我
们称此为捕捉此信号。我们称此函数为信号处理程序或信号一捕捉函数。
signal函数的原型说明此函数要求两个参数,返回一个函数指针,而该指针所指向
的函数要
求一个整型参数,但无返回值(void(*)(int))。第一个参数signo是一个整型数。
第二个参
数是函数指针,它所指向的函数需要一个整型参数,去返回值。用一般语言来描述
也就是要
向信号处理程序借送一个整型参数,而它却无返回值。当调用signal设置信号处理
程序时,
第二个参数是指向该函数(也就是信号处理程序)的指针。signal的返回值则是指向
以前的信
号处理程序的指针。
很多系统以附加的依赖于实现的参数来调节信号处理程序。在1021节中将说明可
选择的SV
43+BSD也提供Signal函数,但是它是用sigaction函数实现的(在1014节中说明
sigaction函数),所以在43+BSD之下使用它提供新的可靠的信号语义。在本书中
用的signal函数都是程序1012中用sigaction实现的该函数。signo参数是图10
中的信号名。func的值是(a)常数SIG-IGN,或(b)常数SIG-DFL,或(c)当接到此信
号后要调用的函数的地址。如果指定SIG-IGN,则向系统核表示要忽略此信号。(要
记住有两个信号SIGKILL和SIGSTOP是不能忽略的。)如果指定SIG-DFL,则表示接到
此信号后的动作是系统默认动作(见图101中的最后1列)。当指定函数地址后,我
们称此为捕捉此信号。我们称此函数为信号处理程序或信号一捕捉函数。
signal函数的原型说明此函数要求两个参数,返回一个函数指针,而该指针所指向
的函数要
求一个整型参数,但无返回值(void(*)(int))。第一个参数signo是一个整型数。
第二个参
数是函数指针,它所指向的函数需要一个整型参数,去返回值。用一般语言来描述
也就是要
向信号处理程序借送一个整型参数,而它却无返回值。当调用signal设置信号处理
程序时,
第二个参数是指向该函数(也就是信号处理程序)的指针。signal的返回值则是指向
以前的信
号处理程序的指针。
很多系统以附加的依赖于实现的参数来调节信号处理程序。在1021节中将说明可
选择的SV
R4有43+BSD参数。
本节开头所示的signal函数原型太复杂了,如果使用下面的typedef〔plawget 19
92〕,则
可使其简单一些。
typedef void Sigfunc(int)
然后,可将signal函数原型写成:
Sigfunc *signal(int,Sigfunc *);
我们已将此typedef包括在ourhdrh文件中(附录B),并附本章中的函数一起使用
。
如果查看系统的头文件<signalh>,则多年都会找到下列形式的说明:
#degine SIG 迹茫模*常病紼RR
#define SIG 迹茫模*常病紼RR (void (*)(>>-1
#define SIG 迹茫模*常病紻FL (void (*)(>>>0
#define SIG 迹茫模*常病絀GN (Void (*)(>>1
这些常数可用于表示"指向函数的指针,该画数要一个整型参数,而且无返回值。
"signal
的第二个参数及其返回值就可用它们表示。这些常数所使用的三个值不一定要是-
1,0和1。
它们必须是三个值而不能是任一可说明函数的地址。大多数Unix系统使用上面所示
的值。
实例
程序101显示了一个简单的信号处理程序,它捕捉两个用户定义的信号并打印信
号编号。
在1010节中说明pause函数,它使调用进程睡眠。
程序101 捕捉SIGUSR1和SIGUSR2的简单处理程序
我们使该程序在后台运行,并且用kill(1)命令将信号送给它。注意,在Unix中,
消灭(kill
)这个术语是用词不当的。kill(1)命令和kill(2)函数只是将一个信号送给一个进
程或进程
组。该信号是否终止该进程则取决于该信号的类型,以及该进程是否安排了捕捉该
信号。
$ aout & 在后台启动进程
[1] 4720 作业控制shell打印作业号和进程ID
$ kill -USR1 4720 向该进程发送SIGUSR1
received SIGUSR1
$ kill -USR2 4720
received SIGUSR2 …………SIGUSR2
$ kill 4720
[1]+Terminated aout & ………SIGTERM
当向该进程发送SIGTERM信号后,该进程就终止,因为它可捕捉此信号,而对此信
号的系统
默认动作是终止。
程序起动
当exec一道程序时,所有信号的状态都是或系统默认或忽略。通常所有信号都被设
置为它们
的系统默认动作,除非调用exec的进程忽略该信号。非常特殊exec函数将原先设置
为要捕捉
的信号都更改为默认动作,其它信号的状态则不变。(一个进程原先要捕捉的信号
,与其exe
c一道新程序后,就自然地不能再捕捉了,因为信号一捕捉函数的地址很可能在所
执行的新
程序文件中已无意义。)
我们经常会碰到的一个具体例子是一个交互shell如何处理对后台进程的中断和退
出信号。
对于一个非作业控制shell。当我们在后台执行一个进程时,例如:
cc mainc &
shell自动地将后台进程中对中断和退出信号的处理方式设置为忽略。于是,当我
们按中断
字符时就不会影响到后台进程。如果没有这样处理,那么当我们按中断字符时,它
不但终止
前台进程,也终止所有后台进程。
很多捕捉这两个信号的交互程序具有下列形式的代码:
int sig 迹茫模*常病絠nt(),sig 迹茫模*常病絨uit();
if(signal(SIGINT,SIG 迹茫模*常病絀GN) !=SIG 迹茫模*常病絀GN)
signal(SIGINT,sig 迹茫模*常病絠nt);
if(signal(SIGQUIT,SIG 迹茫模*常病絀GN)!=SIG 迹茫模*常病絀GN)
signal(SIGQUIT,sig 迹茫模*常病絨uit);
这样处理后,仅当SIGINT和SIGQUIT原先并不忽略,进程才捕捉它们。
从这些signal调用中可以看到这种函数的限制:不改变信号的处理方式就不能确定
信号的当
前处理方式。我们将在本章的稍后部分说明使用sigaction可以确定一个信号的处
理方式,
而无需改变它。
进程创建
当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始
时复制了
父进程存储图象,所以信号捕捉函数的地址在子进程中是有意义的。
104〓不可靠的信号
在早期的Unix版本中(例如Version7),信号是不可靠的。不可靠在这里指的是,信
号可能会
被丢失〖CD2〗一个信号发生了,但进程却决不会知道这一点。那时,进程对信号
的控制能
力也很低,它能捕捉信号或忽略它,但有些很需析功能它却并不具备。例如,有时
用户希望
通知系统核阻塞-信号〖CD2〗不要忽略该信号,而是在其发生时记住它,然后在进
程作好了
准备时再通知它。这种阻塞信号的能力当时并不具备。
42BSD对信号机构进行了更改,提供了被称之为可靠信号的机制。然后,SVR3也
一个时间
窗口,在此段时间中,可能发生另一次中断信号。第二个信号会造成执行默认动作
,而对中
断信号则是终止该进程。这种类型的程序段在大多数情况会正常工作,使得我们认
为它们是
编写得正确的,而实际上却并不是如此。
这些早期版本的另一个问题是:在进程不希望某种信号发生时,它不能关闭该信号
。进程能
做
的就是忽略该信号。有时我们希望通知系统"阻止下列信号发生,如果它们确实产
生了,请
记住它们。"说明这种问题的一个经典实例是下列程序段,它捕捉一个信号,然后
设置一个
表示该信号已发生的标志:
int sig 迹茫模*常病絠nt 迹茫模*常病絝lag; /*当信号发生时设置非0*/
main()
{
int sig 迹茫模*常病絠nt(); /*我们信号处理程序*/
…
signal(SIGINT,sig 迹茫模*常病絠nt);/*设置处理程序 */
…
while(sig 迹茫模*常病絠nt 迹茫模*常病絝lag==0)
pause(); /* 睡眠,等待信号 */
…
}
sig 迹茫模*常病絠nt()
{
signal(SIGINT,sig 迹茫模*常病絠nt);/*为下一次信号重新设置处理程序*/
sig 迹茫模*常病絠nt 迹茫模*常病絝lag=1; /* 设置标志供main循环检查 */
其中,进程调用pause函数使其睡眠,直到捕捉到一个信号。当此信号被捕捉到后
,信号处
理程序将标志sig-int-flag设置为非0。在接获信号后系统将该进程唤醒,在信号
处理程
序返回之后,它检测到该标志为非0,然后执行它所需做的。但是这里也有一个时
间窗口,
这使操作可能错误。如果在测试sig-int-flag之后,调用paust之前发生信号,则
此进程可
能会一直睡眠(假定此信号不再次产生)。于是,这次发生的信号也就丢失了。这是
另一个例
子,某种代码并不正确,但是大多数时间,它能正常工作。查找并排除这种类型的
问题是很
困难的。
105〓中断的系统调用
早期Unix系统的一个特性是:如果在进程执行一个低速系统调用而阻塞期间捕捉到
一个信号
,则该系统调用就被中断不再继续执行。该系统调用出错返回,其errno设置为EI
NTR。这样
处理的理由是:因为一个信号发生了,进程捕捉到了它,这意味着已经发生了某种
事情,所
以是个好机会应当唤醒阻塞的系统调用。
在这里,我们必须区分系统调用的函数。当捕捉到某个信号时,被中断的是在系统
核内执行
的系统调用。
为了支持这种特性,将系统调用分成两类:"低速"系统调用和其它系统调用。低速
系统调
用是可能会使进程永远阻塞的一类系统调用,它们包括:
·在读某些类型的文件时,如果数据并不存在则可能会使调用者永远阻塞(管道、
终端设备
以及网络设备)。
·在写这些类型的文件时,如果不能立即接受这些数据,则也可能会使调用者永远
阻塞。
·打开文件,在某种条件发生之前也可能会使调用者阻塞(例如,打开终端设备,
主要等待
到一个所连接的调制解调器回答了电话)。
·pause(按照定义,它使调用进程睡眠直至捕捉到一个信号)和wait。
·某种ioctl操作。
·某些进程间通信函数(第十四章)。
值得注意的低速系统调用的例外是与磁盘I/O有关的系统调用。虽然读、写一个
磁盘文件
可能暂时阻塞调用者(在磁盘驱动程序将请求排入队列,然后在适当时间执行请求
期间),但
是除非发生硬件错,I/O操作总会很快返回,并使调用者不再处于阻塞状态。
可以用中断系统调用这种方法来处理的一种情况是:一个进程起动了读终端操作,
而使用该
终端设备的用户却离开该终端很长时间。在这种情况下进程可能处于阻塞状态几个
小时甚至
数天,除非系统停机,否则一直如此。
与被中断的系统调用相关的问题是必须用显式方法处理出错返回。典型的代码序列
(假定进
行一个读操作,它被中断,我们希望重新起动它)可能如下列样式:
again:
if((n=read(fd,buff,BUFFSIZE))<0){
if(errno==EINTR)
goto again; /*一个中断的系统调用 */
/* 处理其它出错 */
}
为了帮助应用程序使其不必处理被中断的系统调用,42BSD引进了某些被中断的
系统调用
的自动再起动。自动再起动的系统调用包括:ioctl、read、readv、write、writ
er、wait
和waitpid。正如前述,其中前五个函数只有对低速设备进行操作时才会被信号中
断。而wai
t和waitpid在捕捉到信号时总是被中断。某些应用程序并不希望这些函数被中断后
再起动,
于是这种自动再起动的处理方式也会带来问题,为此43BSD允许进程在每个信号
个别处理
的基础上不使用此功能。
POSIX11允许实现再起动系统调用,但这并不是要求的。系统V的默认工作方式是
不起动系
统调用。但是SVR4使用sigaction时(1014节),可以指定SA-RESTART选项以再起
动由该信
号中断的系统调用。
在43+BSD中,系统调用的再起动依赖于调用了那一个函数设置信号处理方式配置
。较老式
的43BSD兼容的sigvec函数使被该信号中断的系统调用自动再起动。但是,使用
较新的POS
IX1兼容的sigaction则可使它们再起动。但如同在SVR4中一样,在sigaction中
可以使
用SA-RESTART选择项,使系统核再起动由该信号中断的系统调用。
42BSD引进自动再起动功能的一个理由是:有时用户并不知道所使用的输入、输
出设备是
否是低速设备。如果我们编写的程序可以用交互方式运行,则它可能读、写终端低
速设备。
如果在程序中捕捉信号,而系统却可提供再起动功能,则对每次读、写系统调用就
要进行是
否出错返回的测试,如果是被中断的,则再进行读、写。
图102摘录列出了几种实现所提供的信号功能及它们的语义。
〖HT5"SS〗图102 几种信号实现所提供的功能〖HT5SS〗
应当了解,其他厂商提供的Unix系统可能会有不同于图102中所示的处理情况。
例如,Sun
OS 412中的sigaction其默认方式是再起动被中断的系统调用,这与SVR4和4
3+BSD都
不同。
在程序1012中提供了我们自己的signal函数版本,它试图重新起动被中断的系统
调用(
除SIGALRM信号外)。在程序1013中则提供了另一个函数signal-intr,它不进行
再起动。
在所有程序实例中,我们都有目的地显示了信号处理程序的返回(如果它返回的话
),这种返
回可能中断了一个系统调用。
在125节说明select和poll函数时还会涉及被中断的系统调用。
106〓可再入函数
进程捕捉到信号并继续执行时,它首先执行该信号处理程序中的指令。如果从信号
处理程序
返回(例如没有调用exit或longjmp),则在捕捉到信号时进程正在执行的正常指令
序列就继
续执行。(这类似于硬件中断发生时所做的。)但在信号处理程序中,不能判断捕捉
到信号时
进程执行到何处。如果进程正在执行malloc,在其堆中分配另外的存储空间,而此
时由于捕
捉到信号插入执行该信号处理程序,其中又调用malloc,这时会发生什么?又例如
若进程正
在执行getpwnam(62节)这种将其结果存放在静态存储单元中的函数,而插入执行
的信号处
理程序中又调用这样的函数,这时又会发生什么呢?在malloc例中子,可能会对进
程造成破
坏,因为malloc通常为它所分配的存储区保持一个连接表,而插入执行信号处理程
序时,进
程可能正在更改此连接表。在getpwnam的例子中,正常返回给调用者的信息可能由
返回至信
号处理程序的信息覆盖。
POSIX1说明了保证可再入的函数。图103列出了这些可再入函数。图中四个带
*号的函数
并没有按POSIX1说明为是可再入的,但SVR4 SVID 〔AT&T1980〕则将它们列为是
可再入
的。
图103 在信号处理程序中可以调用的可再入函数
没有列入图103中的大多数函数是不可再入的,其原因是(a)已知它们使用静态数
据结构,
式(b)它们调用malloc或Free,或(c)它们是标准I/O函数。标准I/O库的很多实现
都以不可
再入方式使用全局数据结构。
要了解在信号处理程序中即使调用列于图103中的函数,因为每个进程只有一个
errno变量
,所以我们可能修改了其原先的值。考虑一个信号处理程序,它恰好在main刚设置
errno之
后被调用。如果该信号处理程序调用read,则它可能更改errno的值,从而取代了
刚由main
设置的值。因此,作为一个通用的规则,当在信号处理程序中调用图103中列出
的函数时
,应当在其前保存,在后恢复errno。(要了解,常常被捕捉到信号是SIGCHLD,其
信号处理
程序通常要调用一种wait函数,而各种wait函数都能改变errno。
POSIX1没有包括图103中的longjmp和siglongjmp。(在1015节将说明siglon
gjmp函数
。)这是因为在主例程以非再入方式正在更新一数据结结构时可能产生信号。不是
从信号处
理程序返回而是调用siglongjmp,可能使该数据结构是部分更新的。如果应用程序
将要做更
新全局数据结构这样的事情,而同时规定要捕捉某些信号,而这些信号的处理程序
又会引起
执行siglongjmp,则在更新这种数据结构时要阻塞此种信号。
实例
在程序102中,信号处理程序my-alarm调用不可再入函数getpwnam,而my-alarm每
秒钟被调
用一次。1010节中将说明alarm函数。在程序102中用其每秒产生一次SIGAL
RM信号
。
运行此程序时,其结果具有附意性。通常,在信号处理程序第一次返回时,该程序
将由SIGS
EGV信号终止。检查core文件,从中可以看到main函数已调用getpwnam,而且当信
号处理程
序调用此同一函数时,某些内部指针示出了问题。偶然,此程序会运行若干秒,然
后因产生
SIGSEGV信号而终止。在捕捉到信号后,若main函数仍正确运行,其返回值却有时
错误,有
时正确。有时在信号处理程序中调用getpwnam会出错返回,其出错值为EBADF(无效
文件描述
符)。
从此实例中可以看出,若在信号处理程序中调用一个不可再入函数,则其结果是不
可予见的
。
〖HT5"SS〗程序102 在信号处理程序中调用不可再入函数〖HT5SS
〗
107〓SIGCLD语义
SIGCLD和SIGCHLD这两个信号经常易于混淆。SIGCLD是系统V的一个信号名,其语义
与名为SIGCHLD的BSD信号不同。POSIX1则标用BSD的SIGCHLD信号。
BSD SIGCHLD信号的语义与其它信号的语义相类似。子进程状态改变后产生此信号
,父进程需要调用一个wait类函数以检测发生了什么。
但是系统V因为历史沿袭,至今它处理SIGCLD信号的方式不同于其它信号。如果用
signal或sigset(设置信号配置的较老式的SRV3兼容性函数)设置信号配置,则SVR4
继续了这一具有问题色彩的传统(即兼容性限制)。对SIGCLD的较老处理方式是:
1如果进程特地指令对该信号的配置为SIG-IGN,则调用进程的子进程将不产生僵
死进程。
注意,这与其默认动作(SIG-DFL)忽略(见图101)不同。代之以,在子进程终止时
,将其状态丢弃。如果调用进程最后调用一个wait函数,那么它将阻塞到所有子进
程都终止,然后该wait会返回-1,其errno则设置为ECHILD。(此信号的默认配置是
忽略,但这不会造成上述语义。代之以我们必须特地指定其配置为SIG-IGN。)
POSIX1并说明在SIGCHLD被忽略时应产生的后果,所以这种行为是允许的。
43+BSD中,如SIGCHLD被忽略,则允许产生僵死子进程。如果要避免僵死子进程
,则必须
等待子进程。
在SVR4中,如果调用signal或sigset将SIGCHLD的配置设置为忽略,则不会产生僵
死子进程
。另外,使用SVR4版的sigaction,则可设置SA-NOCLDWAIT标志(图105)以避免子
进程僵死
。
2如果将SIGCLD的配置设置为捕捉,则系统核立即检查是否有子进程准备好被等
待,如果
是这样则调用SIGCLD处理程序。
第二项改变了为此信号编写处理程序的方法。
实例
在104节中曾说过进入信号处理程序后,首先要再次调用signal以再设置此信号
处理程序
。(在信号被复置为其默认值时,它可能被丢失,立即重新设置可以减少此窗口时
间。)程序
103显示了这一点。但此程序不能正常工作。如果在SVR2下编译并运行此程序,
则其输出
是不断重复"SIGCLD received n"。最后进程用完其栈空间并异常终止。
此程序的问题是:在信号处理程序的开始处调用signal,按照上述第二项,系统核
检查是否
有需要等待的子进程(因为我们正在处理一个SIGCLD,所以确实有这种子进程),所
以它产生
另一个对信号处理程序的调用。信号处理程序调用signal,整个过程再次重复。
为了解决这一问题,应当在调用wait取了子进程的终止状态之后再调用signal。此
时仅当其
它子进程终止,系统核再会再次产生此种信号。
如果为SIGCHLD建立了一个信号处理程序,又存在一个已终止的但尚未等待的进程
,则是否
会产生信号?POSIX1对此没有作说明。这样就允许前面所述的工作方式。但是,
因为POSIX
1在信号发生时并没有将信号配置复置为其默认值(假定我们正用POSIX1的sig
action函
数设置其配置),于是在SIGCHLD处理程序中也就不必再为该信号指定一个信号处理
程序。
务必了解你所用的系统中SIGCHLD信号的语义。也应了解在某些系统中#define SI
GCHLD为SI
GCLD或反之。更改这种信号的名字使你可以编译为另一个系统编写的程序,但是如
果该程序
使用该信号的另一种语义,则这样的程序也不能工作。
〖HT5"SS〗程序103 不能正常工作的系统V SIGCLD处理程序〖HT5S
S〗
108〓可靠信号术语和语义
我们需要定义一些在讨论信号时会用到的术语。首先,当造成信号的事件发生时,
为进程产
生一个信号(或向一个进程发送一个信号)。事件可以是硬件异常(例如除以0)、软
件条件(例
如,闹钟时间超过)、终端产生的信号或调用kill函数。在产生了信号时,系统核
通常在进
程表中设置某种形式的一个标志。当对信号做了这种动作时,我们说向一个进程递
送了一
个信号。在信号产生和递送之间的时间间隔内,我们称信号末决。
进程可以选用"信号送送阻塞"。如果为进程产生了一个选择为阻搴 信号,而且对
该信号
的动作是系统默认动作或捕捉该信号,则为该进程将此信号保持为末决状态,直到
该进程(a
)对此信号解除了阻塞,或者(b)将对此信号的动作更改为忽略。当传送一个原来被
阻塞的信
号给进程时,而不是在产生该信号时,系统核再决定对它的处理方式。于是进程在
信号传送
给它之前仍可改变对它的动作。进程调用sigpending函数(1013节)将指定的信号
设置为阻
塞的未决。
如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,这将如何呢?POSI
X1允许
系统递送该信号一次或多次。如果系统递送该信号多次,则我们先这些信号排了队
。但是大
多数Unix并不排队信号。代之以,Unix系统核只传送这种信号一次。
早期系统V版本的手册页称SIGCLD信号是用排队方式处理的,但实际并非如此。代
之以,系
统核按107节中所述方式产生此信号。AT&T〔1990e〕的sigaction(2)手册页
称SA-SIGI
NFO标志(图105)使信号可靠地排队,这也不正确。表面上此功能存在于系统核内
但在,SV
R4中并不起作用。
如果有多个信号要传送给一个进程,则将如何呢?POSIX1并没有规定这些信号传
送给进程
的顺序。但是POSIX1的原理阐述部分建议:与进程当前状态有关的信号,例如S
IGSEGV在
其它信号之前传送。
每个进程都有一个信号屏蔽字,它规定了当前要阻塞传送到该进程的信号集。对每
种可能的
信号在该屏蔽字中都有一位与之对应。对于某种信号,其对应位设置,则它当前是
被阻塞的
。一个进程可以调用sigprocmask(在1012节中说明)来检测和更改其当前信号屏
蔽字。
信号数可能会超过一个整型数所包含的二进制位数,为此POSIX1定义了一个新数
据类型,
sigset-t,它保持一个信号集。例如,信号屏蔽字就保存在这些信号集中的一个中
。1011
节中将说明对信号集进行操作的五个画数。
109〓kill和raise函数
kill函数将一个信号发送给一个进程或一个进程组。raise函数则允许一个进程向
自身发送
一个信号。
raise是由ANSI C而非POSIX1定义的。因为ANSI C并不涉及多进程,所以它不能
定义如kil
l这样要有一个进程ID作为其参数的函数。
#include<sys/typesh>
#include<signalh>
int kill(pid 迹茫模*常病絫 pid,int signo);
int raise(int signo);
两个函数返回:若成功为0,出错为-1
kill的pid参数有四种不同的情况。
pid>0 将信号发送给进程ID为pid的进程。
pid==0 将信号发送给其进程组ID关于发送进程的进程组ID,而是发送进程有许可
数问其发
送信号的所有进程。
这里用的术语"所有进程"不包括实现定义的系统进程集。对于大多数Unix系统,系
统进程
集包括:交换进程(pido),init(pid1)以及页精灵进程(pid2)。
pid<0 将信号发送给其进程组ID等于pid绝对值,而且发送进程有许可权向其发送
信号的所
有进程。如上所述一样,"所有进程"并不包括系统进程集中的进程。
pid==-1 POSIX1未定义此种情况。
SVR4和43+BSD用此广播信号。在广播信号时,并不把信号发送给上述系统进程集
。43+B
SD也不将广播信号发送给发送进程自身。若调用者是超级用户,则将信号发送给所
有进程。
如果调用者不是超级用户,则将信号发送给其实际用户ID或保存的位置-用户-ID等
于调用者
的实际或有效用户ID的所有进程。广播信号只能用于管理方面(例如一个超级用户
进程将该
系统停止运行)。
上面曾提及,一个进程将一个信号发送给其它进程需要许可权。超级用户可将信号
发送给另
一个进程。对于其它,其基本规则是发送者的实际或有效用户ID必须等于接收者的
实际或有
效用户ID。如果实现支持-POSIX-SAVED-IDS(如SVR4所做的那样),则用保存的设置
-用户-ID
代替有效用户ID。
在对许可权进行测试时也有一个特例:如果被发送的信号是SIGCONT,则进程可将
它发送给
属于同一对话期的任一其它进程。
POSIX1将信号编号0定义为空信号。如果signo参数是0,则kill仍执行正常的错
误检查,
但不发送信号。这常被用来确定一个特定进程是否仍旧存在。如果向一个并不存在
的进程发
送空信号,则kill返回-1,errno则被设置为ESRCH。但是,应当了解,Unix系统在
经过一定
时间后会重新使用进程ID,所以一个现存的具有所给定进程ID的进程并不一定就是
你所想要
的进程。
如果kill调用为调用进程产生信号,而且此信号是不被阻塞的,那么在kill返回之
前,或者
signo,或者某个其它未决的,非阻塞信号被传送号该进程。
1010〓alarm和pause函数
使用alarm函数可以设置一个时间值(闹钟时间),在将来的某个时刻该时间值会被
超过。当
所设置的时间值被超过后,产生SIGALRM信号。如果不忽略或不捕捉此信号,则其
默认动作
是终止该进程。
#include<unistdh>
unsigned int alarm(unsigned int seconds);
Returns:0 or number of 返回:0或以前设置的闹钟时间的余留秒数
其中,参数seconds的值是秒数,经过了所指定的seconds秒后会产生信号SIGALRM
。要了解
的是,经过了指定秒后,信号是由系统产生的,由于进程调度的延迟,进程得到控
制能够处
理该信号还需一段时间。
早期的Unix版本曾警告,这种信号可能比予定值提前1秒发送。POSIX1则不允许
这样做。
每个进程只能有一个闹钟时间。如果在调用alarm时,以前已为该进程设置过闹钟
时间,而
且它还没有超时,则该闹钟时间的余留值作为本次alarm函数调用的值返回。以前
登记的闹
钟时间则被新值代换。
如果有以前登记的尚未超过的闹钟时间,而且seconds值是0,则取消以前的闹钟时
间。前闹
钟时间的余留值仍作为函数的返回值。
虽然SIGALRM的默认动作是终止进程,但是大多数使用闹钟的进程捕捉此信号。如
果此时进
程要终止,则在终止之前它可以执行所需的清除操作。
pause函数使调用进程挂起直至捕捉到一个信号。
#include<unistdh>
int pause(void);
返回:-1 errno设置为EINTR
只有执行了一个信号处理程序并从其返回时,pause再返回。在这种情况下,paus
e返回-1
,errno设置为EINTR。
实例
使用alarm和pause,进程可使自己睡眠一段指定的时间。程序104中的sleep函数
提供这种
功能。
程序104 sleep1的简化但并不完整的实现
sleep1函数看起来与将在1019节中说明的sleep函数类似,但这种简化的实现有
下列问题
。
1如果调用者已设置了闹钟,则它被sleepl函数中的第一次alarm调用擦去。
可用下列方法更正这一点:检查第一次调用alarm的返回值,如其小于本次调用al
arm的参数
值,则只应等到该前次放置的闹钟时间超时。如果前次设置的闹钟时间的超时时刻
后于本次
设置值,则在sleepl函数返回之前,要再设置闹钟时间,使其在予定时间再发生超
时。
2该程序中修改了对SIGALRM的配置。如果编写了一个函数供其它函数调用,则在
该函数被
调用时先要保存原配置,在本函数返回前再恢复原配置。
更改这一点的方法是:保存signal函数的返回值,在返回前恢复设置原配置。
3不调用alarm和pause之间有一个竟态条件。在一个很繁忙的系统中,可能alar
m在调用pa
use之前超时,并调用了信号处理程序。如果发生了这种情况,则在进程调用paus
e后,如果
没有捕捉到其它信号它就永远被挂起。
早期的sleep实现与程序104类似,但更正了问题1和2。有两种方法可以更正问题
3。第一
种方法是使用setjmp,下面立即说明这种方法。另一种方法是使用sigprocmask和
sigsusp
end,在1019节中将说明这种方法。
实例
SVR2中的Sleep实现使用了setjmp和longjmp(见710节)以避免问题3中所说明的竟
态条件
。此函数的一个简化版本,称为sleep2,示于程序105中(为了缩短这一实际的长
度,在程
序中没有处理上面所说的问题1和2。)
〖HT5"SS〗程序105 Sleep的另一个不完善的实现〖HT5SS〗
在此函数中,程序104具有的竟态条件已被避免。即使pause从未执行,在发生S
IGALRM时
,sleep2函数也返回。
但是,sleep2函数中却有另一个难于察觉的问题,它涉及到与其它信号的互相作用
。如果SI
GALRM中断了某个其它信号处理程序,则调用longjmp就会提早终止该信号处理程序
。程序10
5显示了这种情况。在SININT处理程序中的for循环语句其执行时间在作者所用的
系统上会超过5秒钟,也就是大于sleep2的参数值,这正是我们所想要的。整型变量
j说明为volatile,这样就阻止了优化编译程序除去循环语句。执行程序106得到:
$ aout
^? 键入中断字符
sig-int starting
sleep2 returned:0
〖HT5"SS〗程序106 在一个捕捉其它信号的程序中调用sleep2〖HT5
SS〗
从中可见sleep2函数所引起的longjmp使另一个信号处理程序sig-int提早终止。如
果将SVR2的Sleep函数与其它信号处理程序一起使用,你就可能碰到这种情况。见
练习10?。sleep1和sleep2函数这两个实例的目的是告诉我们在涉及到信号时需要
有精细而周到的考虑。下面几节将说明解决这些问题的方法,使我们能够可靠地,
在不影响其它代码段的情况下处理信号。
实例
除了用来实现sleep函数外,alarm还常用于对可能阻塞的操作设置一个时间上限值
。例如,程序中有一个读低速设备的会阻塞的操作(见105),我们希望它超过一
定时间量后就一定
终止。程序107实现了这一点,它从标准输入读一行,然后将其写到标准输出上
。
〖HT5"SS〗程序107 带时间限制调用read〖HT5SS〗
这种代码序列在很多Unix应用程序中都能见到,但是这种程序有两个问题:
1程序107有程序104中的同样问题:在第一个alarm调用和read之间有一个竞
态条件。
如果系统核在这两个函数调用之间使进程不能占用处理机运行,而其时间长度又超
过闹钟时
间,则read可能永远阻塞。这种类型的大多数操作使用较长的闹钟时间,例如1分
钟或更长
一点,使这种问题不会发生,但无论如何这是一个竞态条件。
2如果系统调用是自动再起动的,则当从SIGALRM信号处理程序返回时,read并不
被终止。
在这种情形下,设置时间限制不会起作用。
在这里我们确实需要终止慢速系统调用。但是,POSIX1并未提供一种可移植的方
法实现这
一点。
实例
让我们用longjmp再实现前面的实例。使用这种方法我们就无需担心一个慢速的系
统调用是
否被中断。
程序108 使用longjmp,带时间限制调用read
不管系统是否重新起动系统调用,该程序都会如所予期的那样工作。但是要理解,
该程序仍
旧有与程序105一样的与其它信号处理程序相互作用的问题。
如果要对I/O操作设置时间限制,则如上所示可以使用longjmp,当然也要理解它
可能有与
其
它信号处理程序相互作用的问题。另一种选择是使用select或poll函数,在125
1和12
52将对它们进行说明。
1011〓信号集
我们需要有一个能表示多个信号〖CD2〗信号集的数据类型。将在sigprocmask(下
一节中说
明)这样的函数中使用这种数据类型,以告诉系统核不允许发生核信号集中的信号
。如同前
面已提过的,信号种类数可能超过一个整型量所包含的二进位数,所以一般而言,
不能用一
个
整型量中的一个二进位代表一种信号。POSIX1定义数据类型sigset-t以包含一个
信号集,
也定义了下列五个处理信号集的函数。
#include <signalh>
int sigemptyset(sigset 迹茫模*常病絫 *set);
int sigfillset(sigset 迹茫模*常病絫 *set);
int sigaddset(sigset 迹茫模*常病絫 *set,int signo);
int sigdelset(sigset 迹茫模*常病絫 *set,int signo);
4个函数都返回:若成功为0,出错为-1
int sigismember(const sigset 迹茫模*常病絫 *set,int signo);
返回:若真为1,若假为0
函数sigemptyset初始化由set指向的信号集,使排除其中所有信号。函数sigfill
set初始化
由set指向的信号集,使包括其中所有信号。所有应用程序在使用信号集前,要对
该信号集
调用sigemptyset或sigfillset一次。这是因为C编译程序将不赋初值的外部和静态
度量都初
始化为0,而这是否与给定系统上信号集的实现相对应并不清楚。
一旦我们已经初始化了一个信号集,以后就可在该信号集中增、删特定的信号。函
数sigadd
set将一个信号添加到一个现存集中,sigdelset则从一个信号集中删除一个信号。
对所有以
信号集作为一个参数的函数,都向其传送信号集地址。
实现
如果系统所实现的信号数少于一个整型量所包含的两进位数,则可用一个二进制位
代表一个
信号的方法实现信号集。例如,大多数43+BSD实现中有31种信号,和32位字长整
型。sige
mptyset和sigfillset这两个函数可以在<signalh>头文件中实现为宏:
#define sigemptyset(ptr) (*(ptr)=0)
#define sigfillset(ptr) (*(ptr)=~(sigset 迹茫模*常病絫)0,0)
注意,除了设置对应信号集中各信号的位外,sigfillset必须返回0,所以我们使
用逗号算
符,它将逗号算符后的值作为表达式的值返回。
使用这种实现的sigaddset打开一位,sigdelset则关闭一位。sigismember测试一
指定位。
因为没有信号编号值为0,所以从信号编号中减1以得到要处理的二进制位的位编号
数。程序
109实现这些功能。
程序109 sigaddset、sigdelset和sigismember的实现
我们也可将这三个函数在<signalh>中实现为各一行的宏,但是POSIX1要求检
查信号编
号参数的有效性,如果无效则设置errno。在宏中实现这一点比函数要难。
1012〓sigprocmask函数
在108节中曾说明一个进程的信号屏蔽字规定了当前阻塞而不能传送给该进程的
信号集。
调用函数sigprocmask可以检测或更改(或两者)进程的信号屏蔽字。
#include<signalh>
int sigprocmask(int how,const sigset 迹茫模*常病絫 *set,sigset 迹茫模?nbsp;
2〗t *
oset);
返回:若成功为0,出错为-1 Returns:0 if OK,-1 on error
首先,oset是非定指针,进程的当前信号屏蔽字通过oset返回。其次,若set是一
个非定指
针,则参数how指示如何修改当前信号屏蔽字。图104说明了how可选用的值。SI
G-BLOCK是
或操作,而SIG-SETMASK则是赋值操作。
〖HT5"SS〗图104 用sigprocmask更改当前信号屏蔽字的方法〖HT5〗
如果set是个定指针,则不改变该进程的信号屏蔽字,how的值也无意义。
如果在调用sigprocmask后有任何未决的,不再阻塞的信号,则在sigprocmask返回
的至少将
其中之一传送给该进程。
实例
程序1010是一个函数,它打印调用进程的信号屏蔽字所阻塞的信号的名称。从程
序1014
和1015中调用此函数。为了节省空间,没有对图101中列出的每一种信号测试
该屏蔽字
。(见练习109)
程序1010 为进程打印信号 帘为?nbsp;
1013〓sigpending函数
sigpending返回对于调用进程被阻塞不能传送和当前未决的信号集。该信号集通过
set参数
返回。
#include<signalh>
int sigpending(sigset 迹茫模*常病絫 *set);
返回:若成功为0,出错为-1
实例
程序1011使用了很多前面说明过的信号功能。进程阻塞了SIGQUIT信号,保存了
当前信
号屏蔽字(以便以后恢复),然后睡眠5秒钟。在此期间所产生的退出信号都被阻塞
,不传送
至该进程,直到该信号不再被阻塞。在5秒睡眠结束后,检查是否有信号未决,然
后将SIGQU
IT设置为不再阻塞。
注意,在设置SIGQUIT为阻塞时,我们保存了老的屏蔽字。为了解除对该信号的阻
塞,我们
用老的屏蔽字重新设置了进程信号屏蔽字(SIG-SETMAS)。可以使用的另一种方法是
用SIG-U
NBLOCK使以前阻塞的信号不再阻塞。但是,应当了解如果我们编写了一个可能由其
他人使用
的函数,而且我们需要在我们的函数中阻塞一个信号,则不能用SIG-UNBLOCK解除
对此信号
的阻塞,这是因为此函数的调用者在调用本函数之前可能也阻塞了此信号。在这种
情况下我
们必须使用SIG-SETMASK将信号屏蔽字恢复为原先值。在1018节的system函数部
分有这
样的一个例子。
在睡眠期间如果产生了退出信号,那么程序运行到这一点则该信号是未决的,但是
不再受阻
塞,所以在sigprocmask返回之前,它就被传送到本进程。从程序的输出中我们可
以看到这
一点:在SIGQUIT处理程序(sig-quit)中的printf语句先执行,然后再执行sigpro
cmask之后
的printf语句。
〖HT5"〗程序1011 信号设置和sigprocmask的实例〖HT5〗
然后该进程再睡眠5秒钟。如果在此期间再产生退出信号,那么它就会使该进程终
止,因为
在上次捕捉到该信号时,已将其处理方式设置为默认动作。此时如果键入终端退出
字符Cont
rol-\,则输出"QUIT(coredump)"信息,表示进程因接到SIGQUIT而终止,但是在
core文
件中保存了与进程有关的信息(该信息是由shell发现其子进程异常终止时打印的。
)
$ aout
^\ 产生信号一次(在5秒之内)
SIGQUIT pending 从sleep返回后
caught SIGQUIT 在信号处理程序中
SIGQUIT unblocked 从sigprocmask返回后
^\Quit(coredump) 再次产生信号
$ aout
^\^\^\^\^\^\^\^\^\^\ 产生信号10次(在5秒之内)
SIGQUIT pending
caught SIGQUIT 只产生信号一次
SIGQUIT unblocked
^\Quit(coredump) 再产生信号
注意,在第二次运行该程序时,在进程睡眠期间我们使SIGQUIT信号产生了10次,
但是解除
了对该信号的阻塞后,只向进程传送一次SIGQUIT。从中可以看出在此系统上没有
将信号进
行排队。
1014〓sigaction函数
sigaction函数的功能是检查或修改(或两者)与指定信号相关联的处理动作。此函
数取代了U
nix早期版本使用的signal函数。在本市末尾我们用sigaction函数实现了signal。
#include<signalh>
int sigaction(int signo,const struct sigaction *act,
struct sigaction *oact);
返回:若成功为0,出错为-1
其中,参数signo是要检测或修改具体动作的信号的编号数。若act指针非定,则要
修改其动
作。如果oact指针非定,则系统返回该信号的原先动作。此函数使用下列结构:
struct sigaction {
void (*sa 迹茫模*常病絟andler) (); /* 信号处理程序的地址,或SIG-IGN,
或SIG-DFL
,
sigset 迹茫模*常病絫 sa 迹茫模*常病絤ask; /*添加的要阻塞的信号
int sa 迹茫模*常病絝lags; /*信号选项,见图105
};
当更改信号动作时,如果sa-handler指向一个信号捕捉函数(不是常数SIG-IGN或S
IG-DFL)
,则sa-mask字段说明了一个信号集,在调用信号捕捉函数之前,该信号集要加到
进程的信
号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原先值。这
样,在调
用执行信号处理程序时就能阻塞某些信号。在信号处理程序被调用时,系统建立的
新信号屏
蔽字会自动包括正被传送的信号。因此就保证了在处理一个给定的信号时,如果这
种信号再
次发生,那么它会被阻塞到对前一个信号的处理结束为止。回忆108节,若同一
种信号多
次发生,通常并不将它们排队,所以如果在某种信号被阻塞时,它发生了五次,那
么对这种
信号解除阻塞后,其信号处理函数通常只会被调用一次。
一旦对一个给定的信号设置了一个动作,那么在用sigaction改变它之前,该设置
就一直有
效。这与早期的不可靠信号机制不同,而符合了POSIX1在这方面的要求。
act结构的sa-flags字段包含了对信号进行处理的各个选择项。图105详细列出了
这些选择
项的意义。
图105 信号处理的选择项标志(sa-flags)
实例〖CD2〗signal函数
现在让我们用sigaction实现signal函数。43+BSD也是这样做的(POSIX1的原理
阐述中分
也说明这里POSIX所希望的)。SVR4则提供老的不可靠信号语义的signal函数。除非
为了向后
兼容面使用老的语义,在SVR4之下,你也应使用下面的signal实现,或者直接调用
sigactio
n(在SVR4下,可以在调用sigaction时指定SA-RESETHAND和SA-NODEFER选项以实现
老的语义
的signal函数)。
在本书中所有调用signal的实例都是调用程序1012中所实现的该函数。
〖HT5"〗程序1012 用sigaction所实现的signal函数〖HT5〗
注意,我们必须用sigemptyset函数初始化act结构的成员。不能保证
actsa-mask=0;
会做同样的事情。
我们对除SIGALRM以外的所有信号都企图设置SA-RESTART标志,于是被这些信号中
断的系统
调用都能再起动。不希望再起动由SIGALRM信号中断的系统调用的原因是:我们希
望对I/
O操作可以设置时间限制。(请回忆与程序107有关的讨论。)
某些系统定义了SA-INTERRUPT标志。这些系统的默认方式是重新起动被中断的系统
调用,
而指定此标志则使系统调用被中断后不再重起动。
实例〖CD2〗signal 迹茫模*常病絠ntr函数
程序1013是signal函数的另一种版本,它阻止被中断的系统调用的再起动。
程序1013 signal-intr函数
如果系统定义了SA-INTERRUPT,则在sa-flags中增加该标志,这样也就阻止了被中
断的系统
调用的再起动。
1015〓sigsetjmp和siglongjmp函数
在710节中说明了用于非局部转移的setjmp和longjmp函数。在信号处理程序中经
常调用lo
ngjmp函数以返回到程序的主循环中,而不是从该处理程序返回。确实,ANSIC标准
说明一个
信号处理程序可以或者返回,或者调用abort,exit或longjmp。在程序105和10
8中已经
有了这种情况。
调用longjmp有一个问题。当捕捉到一个信号时,进入信号捕捉函数,此时当前信
号被自动
地加到进程的信号屏蔽字中。这阻止了后来产生的这种信号中断此信号处理程序。
如果用lo
ngjmp跳出此信号处理程序,则对此进程的信号屏蔽字会发生什么呢?
在43+BSD下,setjmp和longjmp保存和恢复信号屏蔽字。但是,SVR4并不做这种
操作。4
3+BSD提供函数-setjmp和-longjmp,它们也不保存和恢复信号屏蔽字。
为了允许两种形式并存,POSIX1并没有说明setjmp和longjmp对信号屏蔽字的作
用。作为
替代,POSIX1定义了两个新函数sigsetjmp和siglongjmp。在信号处理程序中作
非局部转
移时应当使用这两个函数。
#include <setjmph>
int sigsetjmp(sigjmp 迹茫模*常病絙uf env,int savemask);
void siglongjmp bufenv int val;
返回:若直接调用为0,若从siglongjmp调用返回则为非0。
这两个函数和setjmp,longjmp之间的唯一区别是sigsetjmp增加了一个参数。如果
savemask
非0,则sigsetjmp在env中保存进程的当前信号屏蔽字。
在调用siylongjmp时,如果带非0 savemask的sigsetjmp调用已经保存了env,则si
glongjmp
从其中恢复保存的信号屏蔽字。
实例
程序1014显示了在信号处理程序被调用时,系统所设置的信号屏蔽字如何自动地
包括刚被
捕捉到的信号。它也例示了如何使用sigsetjmp和siglongjmp函数。
程序1014 信号屏蔽、sigsetjmp和siglongjmp的实例
此程序例示了另一种技术,只要在信号处理程序中调用siglongjmp就应使用这种技
术。在调
用sigsetjmp之后将变量canjump设置为非0。在信号处理程序中检测此变量,仅当
它为非0值
时才调用siglongjmp。这提供了一种保护机制,使得若在jmpbuf(跳转缓存)尚未由
sigsetjm
p初始化时,调用信号处理程序,则不执行其处理动作就返回。(在本程序中,sig
longjmp之
后程序很快就结束,但是在较大的程序中,在siglongjmp之后,信号处理程序可能
仍旧被设
置)。在一般的C代码中(不是信号处理程序),对于longjmp并不需要这种保护措施
。但是,
因为信号可能在任何时候发生,所以在信号处理程序中,需要这种保获措施。
在程序中使用了数据类型sig-atomic-c,它是ANSI C定义的在写时不会被中断的变
量类型。
它意味着这种变量在具有虚存的系统上不会跨越页的边界,可以用一条机器指令对
其进行存
取。对于这种类型的变量总是带ANSI C的类型修饰符vlatile,其原因是:该变量
将由二个
不同的控制线-main函数和异步执行的信号处理程序存取。
图106显示了此程序的执行时间顺序。可将图106分成三部分:左面部分(对应
于main),
中间部分(sig-usr1)和右面部分(sig-alrm)。在进程执行左面部分时,信号屏蔽字
是0(没有
信号是阻塞的)。而执行中间部分时,其信号屏蔽字是SIGUSR1。执行右面部分时,
信号屏蔽
字是SIGUSR1|SIGALRM。
〖HT5"〗图106 处理两个信号的实例程序的时间顺序〖HT5〗
执行程序1014得到下面的输出:
$ aout & 在后台启动进程
starting main:
[1] 531 作业控制shell打印其进程ID
$ kill -USR1 531 向该进程发送SIGUSR1
starting sig 迹茫模*常病絬sr1:SIGUSR1
$ in sig 迹茫模*常病絘lrm:SIGUSR1 SIGALRM
finishing sig 迹茫模*常病絬sr1:SIGUSR1
ending main:
[1]+Done aout & 键入回车
这与我们所期望的相同:当调用一个信号处理程序时,被捕捉到的信号加到了进程
的当前信
号屏蔽字。当从信号处理程序返回时,原来的屏蔽字被恢复。另外,siglongjmp恢
复了由si
gsetjmp所保存的信号屏蔽字。
如果将程序1014中的sigsetjmp和siglongjmp分别代换成-setjmp和-longjmp,则
在43+B
SD下运行此程序,最后一行输出变成:
ending main:SIGUSR1
这忌味着在调用-setjmp之后执行main函数时,其SIGUSR1是阻塞的。这多半不是我
们所希
望的。
1016〓sigsuspend函数
上面已经说明,更改进程的信号屏蔽字可以阻塞或解除阻塞所选择的信号。使用这
种技术可
以保获不希望由信号中断的代码临界区。如果希望对一个信号解除阻塞,然后pau
ser以等待以前被阻塞的信号发生,则将又如何呢?假定信号是SIGINT,实现这一点
的不正确方法是:
sigset 迹茫模*常病絫 newmask,oldmask;
sigemptyset(&newmask);
sigaddset(&newmask,SIGINT);
/* 阻塞SIGINT,保存当前信号屏蔽 */
if(sigprocmask(SIG 迹茫模*常病紹LOCK,&newmask,&oldmask)<0)
err 迹茫模*常病絪ys("SIG 迹茫模*常病紹LOCK error");
/* 代码临界区 */
/* 恢复信号屏蔽,它不阻塞SIGINT
if(sigprocmask(SIG 迹茫模*常病絊ETMASK,&oldmask,NULL)<0)
err 迹茫模*常病絪ys("SIG 迹茫模*常病絊ETMASK error");
pause();/* 等待信号发生 */
/* 继续处理 */
如果在解除对SIGINT的阻塞和pause之间发生了SIGINT信号,则此信号就被丢失。
这是早期的不可靠信号机制的另一个问题。
为了纠正此问题,需要在一个原子操作中实现恢复信号屏字,然后使进程睡眠这种
功能是由sigsuspend函数所提供的。
#include<signalh>
int sigsuspend(const sigset 迹茫模*常病絫 *sigmask);
返回:-1 errno设置为EINTR
进程的信号屏蔽字设置为由sigmask指向的值。在捕捉到一个信号或发生了一个会
终止该进
程的信号之前,该进程也被挂起。如果捕捉到一个信号而且从该信号处理程序返回
,则sigs
uspend返回,并且该进程的信号屏蔽字设置为调用sigsuspend之前的值。
注意,此函数没有成功返回值。如果它返回到调用者,则总是返回-1,并且errno
设置为EIN
TR(表示一个被中断的系统调用)。
实例
程序1015显示了保护临界区,使其不被指定的信号中断的正确方法。
程序1015 保护临界区不被信号中断
注意,当sigsuspend返回时,它将信号屏蔽字设置为调用它之前的值。在本例中,
SIGINT信
号将被阻塞。因此将信号屏蔽复置为早先保存的值(oldmask)。
运行程序1015得到下面的输出:
$ aout
in critical region:SIGINT
^? 键入我们的中断字符
in sig 迹茫模*常病絠nt:SIGINT
after return from sigsuspend:SIGINT
从中可见,在sigsuspend返回时,它将信号屏蔽字恢复为调用它之前的值。
实例
sigsuspend的另一种应用是等待一个信号处理程序设置一个全局变量。在程序10
16中,捕
捉中断信号和退出信号两者,但是希望只有在捕捉到退出信号时再继续执行main程
序。此程
序的样本输出是:
$ aout
^? 键入我们的中断学符
interrupt
^? 再次键入我们的中断字符
interrupt
^? 再一次
interrupt
^/$ 用退出符终止
考虑到ANSI C的非POSIX系统,以及POSIX系统两者之间的可移植性,在一个信号处
理程序中
我们唯一应当做的是赋一个值给类型为sig-atomic-t的变量。POSIX1规定得更多
一些,它
说明了在一个信号处理程序中可以安全地调用的函数表(图103),但是如果这样
来编写代
码,则它可能不会正确地在非POSIX系统上运行。
〖HT5"〗程序1016 用sigsuspend等待一个全局变量被设置〖HT5〗
实例
可以用信号实现父子进程之间的同步,这是信号应用的另一个实例。程序1017实
现了88
节中提到的五个例程:TELL-WAIT、TELL-PARENT、TELL-CHILD、WAIT-PARENT和WA
IT-CHILD
。
其中使用了两个用户定义的信号:SIGUSR1由父进程发送给子进程;SIGUSR2由子进
程发送给
父进程。在程序143中,我们说明了用管道的这五个函数的另一种实现。
程序1017 父子进程可用来实现同步的例程
如果在等待信号发生时希望去睡眠,则Sigsuspend函数可以满足此种要求(正如在
前面两个
例子中所示),但是如果在等待信号期间希望调用其它系统函数则均如何呢?不幸的
是对此问
题没有可靠的解决方法。
在程序1713中我们遇到了这种情况。我们捕捉SIGINT和SIGALRM这两种信号,在
信号发生
时,这两个信号处理程序都设置一个全局变量。用signal-intr函数设置这两个信
号处理程
序,使得它们中断任一被阻塞的慢速系统调用。当我们阻塞在select函数调用(见
1251
节),等待慢速设备的输入时很可能发生这两种信号。(因为常设置闹钟以阻止永远
等待输入
,所以这对SIGALRM是特别真实的。)我们能尽力做到的是:
if (intr 迹茫模*常病絝lag) /*我们的SIGINT处理程序设置标志
handle 迹茫模*常病絠ntr();
if(alrm 迹茫模*常病絝lag) /* */…… SIGALRM……
handle 迹茫模*常病絘lrm();
/* 在此处发生的信号被丢失
while (select(…)<0){
if(errno==EINTR){
if(alrm 迹茫模*常病絝lag)
handle 迹茫模*常病絘lrm();
else if (intr 迹茫模*常病絝lag)
handle 迹茫模*常病絠ntr();
}else
/* 某个其它错 */
}
在调用select之前测试各全局附标志,如果select返回一个中断的系统调用错,则
再次进行
测
试。如果在前两个if语句和后过的select调用之间捕捉到两个信中的任一个,则问
题就发生
了。正如代码中的注释所指出的,在此处发生的信号丢失了。调用了相应的信号处
理程序,
它们设置了相应的全局变量,但是select决不会返回(除非某些数据已准备好可读
)。
我们想要能够做的是下列步骤序列:
1阻塞SIGINT和SIGALRM
2测试两个全局变量以判别是否发生了一个信号,如果已发生则处理此条件。
3调用select(或任何其它系统调用,例如read)并解除对这两个信号的阻塞,这
两个操作
要作为一个原子操作。
sigsuspend函数仅当第3步是一个pause操作时才帮助我们。
1017〓abort函数
前面已提及abort函数的功能是使程序异常终止。
#include<stdlibh>
void abort(void);
此函数不返回
此函数将SIGABRT信号发送给调用进程。一个进程不应忽略此信号。
ANSIC要求若捕捉到此信号而且相应信号处理程序返回,abort仍不会返回到其调用
者。如果
捕捉到信号,则信号处理程序不能返回的唯一方法是它调用exit、exit、longjmp
或siglong
jmp。(1015节讨论了在longjmp和siglongjmp之间的区别)。POSIX1也说明abo
rt复盖了
进程对此信号的阻塞和忽略。
让进程捕捉SIGABRT的意图是:在进程终止之前由其执行所需的清除操作。如果进
程并不在
信号处理程序中终止自己,POSIX1说明当信号处理程序返回时,abort终止该进
程。
ANSI C对此函数的规格说明将这一问题留由实现决定,而不管输出流是否刷新以及
不管临时
文件(513节)是否删除。POSIX1的要求则进了一步,它要求如果abort调用终止
进程,
则它应该有对所有打开的标准I/O流调用fclose的效果。但是如果abort调用并
不终止进
程,则它对打开流也不应有影响。正如我们将在后面所看到的,这种要求是很难实
现的。
系统V的早期版本中,abort函数产生SIGIOT信号。更进一步,一个进程忽略此信号
,或者捕
捉它并从信号处理程序返回都是可以的,在返回情况下,abort返回到它的调用者
。
43BSD产生SIGILL信号。在此之前,该函数解除对此信号的阻塞,将其配置恢复
为SIG-DFL
(终止并构造core文件)。这阻止一个进程忽略或捕捉此信号。
SVR4在产生此信号之前关闭所有I/O流。在另一方面,43+BSD则可做此操作。对
于保护性
的程序设计,如果希望刷新标准I/O流,则在调用abort之前要做这种操作。在er
r-dump函
数中实现了这一点(附录B)。
因为大多数Unix tmpfile(临时文件)的实现在创建该文件之后立即调用Unlink,所
以ANSI C
关于临时文件的警告通常与我们无关。
实例
程序1018按POSIX1的说明实现了abort函数。对处理打开的标准I/O流的要求
是难于实
现
的。首先我们查看如果执行默认动作,并且如果我们刷新所有标准I/O流。这并
不等价于
对所有打开的流调用fclose(因为只刷新它们,并不关闭它们),但是当进程终止时
,系统会
关闭所有打开文件。如果进程捕捉此信号并返回,则我们刷新所有的流。(如果进
程捕捉此
信号,并且不返回,则我们也不会触及标准I/O流。)我们没有处理的唯一条件
是如果进
程捕捉此信号,然后调用-exit。在这种情况下,任何未刷新的在存储器中的标准
I/O缓存
都被丢弃。我们假定捕捉此信号,并特地调用-exit的调用者并不想要刷新缓存。
回忆109节,如果调用kill使为调用者产生信号,并且如果该信号是不被阻塞的
(在程序10
18中保证做出了这一点),则在kill返回前该信号就被传送给了该进程。这样我
们就可确
知如果对kill的调用返回了,则该进程一定已捕捉到该信号,并且也从该信号处理
程序返回
。
程序1018 abort的POSIX1实现
1018〓system函数
在812节已经有了一个system函数的实现。但是该版本并不做任何信号处理。PO
SIX2要
求system忽略SIGINT和SIGQUIT,阻塞SIGCHLD。在给出一个正确地处理这些信号的
一个版本
之前,先说明为什么要考虑信号处理。
实例
程序1019使用812节中的system版本,用其调用ed(1)编辑程序。(ed很久以来
就是Unix
的组成部分。在这里使用它的原因是:它是一个交互式的捕捉中断和退出信号的程
序。若从
shell调用ed,并按中断字符,则它捕捉中断信号并打印问号符。它也将对退出符
的处理方
式设置为忽略。)
程序1019〓用syetem调用ed编辑程序
程序1019捕捉SIGINT和SIGCHLD。若调用它则可得:
$ aout
a 将正文添加至编辑器缓存
Here is one line of text
and another
行首的点停止添加方式
1,$p 打印第1行至最后1行,以便观察缓存中的内容
Here is one line of text
and another 将缓存写至一文件
w tempfoo 编辑器称写了37个字节
37 离开编辑器
q
caught SIGCHLD
当编辑程序终止时,对父进程(aou十进程)产生SIGCHLD信号。父进程捕捉它,执
行其处理
程序sig-chid,然后从其返回。但是若父进程正捕捉SIGCHLD信号,因为它已创建
自己的子
进位,所以它应当这样做,使其了解何时它的子进程已终止。在system函数执行时
,父进程
中该信号的传送应当阻塞。确实,这就是POSIX2所说明的。否则,当system创建
的子进程
结束时,system的调用者可能错误地认为,它自己的一个子进程结束了。
如果我们再次执行该程序,在这次运行时将一个中断信号传送给编辑程序,则可得
:
$ aout
a 将正文添加至编辑器缓存
hello,world
行首的点停止添加方式
1,$p 打印第1街至最后1行
hello,world
w etmpfoo 将缓存写至一文件
13 编辑器称写了13个字节
^? 键入中断符
? 编辑器捕捉信号,打印问号
caught SIGINT 父进程执行同一操作
q 离开编辑器
caught SIGCHLD
回忆96节可知,按中断字符使中断信号传送给前台进程组中的所有进程。图10
7显示了
编辑程序正在进行时的进程安排。
图107 程序1019运行时的前、后台进程组
在这一实例中,SIGINT被送给三个前台进程。(shell进程忽略此信号)从输出中可
见,Qou
t进程和ed进程捕捉该信号。但是,当我们用system运行另一道程序(例如ed)时,
不应使父
、子进程两者都捕捉终端一产生的两个信号:中断和退出。这两个信号只应送给正
在运行的
程序:子进程。因为由system执行的命令可能是交互作用命令(为本例中的ed),以
及因为sy
stem的调用者在程序执行时放弃了控制,等待该执行程序的结束,所以system的调
用者就不
应接收这两个终端一产生的信号。这就是为什么POSIX2规定system的调用者应当
忽略这两
个信号的原因。
实例
程序1020是system函数的另一个实现,它进行了所要求的信号处理。
程序1020是ystem函数的POSIX2实现
很多较早的文献中使用下列程序段忽略中断和退出信号:
if((pid=fork())<0)
err 迹茫模*常病絪ys("fork error");
else if (pid==0){ /* 子进程 */
execl(…);
迹茫模*常病絜xit(127);
}
/* parent */
old 迹茫模*常病絠ntr=signal(SIGINT,SIG 迹茫模*常病絀GN);
old 迹茫模*常病絨uit=signal(SIGQUIT,SIG 迹茫模*常病絀GN);
waitpid(pid,&status,0)
signal(SIGINT,old 迹茫模*常病絠ntr);
signal(SIGQUIT,old 迹茫模*常病絨uit);
这段代码的问题是:在fork之后不能保证父进程还是子进程先运行。如果子进程先
运行,父
进程在一段时间后再运行,那么在父进程将中断信号的配置改变为忽略之前,就可
能产生这
种信号。由于这种原因在程序1020中在fork之前就改变对该信号的配置。
注意,子进程在调用execl之前要先恢复这两个信号的配置。这就允许在调用者配
置的基础
上,execl可将它们的配置更改为默认值。
system的返回值
system的返回值是shell的终止状态,它不总是执行命令字符串进程的终止状态。
在程序8
13中有一些例子,其结果正是我们所期望的;如果执行一条如date那样的简单命令
,则其终
止状态是0。执行shell命令exit44,则得终止状态44。在信号方面又如何呢?
让我们运行程序814,并向正执行的命令发送一些信号。
$ tsys "sleep 30"
^?normal termination,exit status=130 wetype our interrupt key键入中断
$ tsys "sleep 30"
^\sh:946 Quit 退出
normal termination,exit status=131
当用中断信号终止sleep时,pr-exit函数(程序83)认为它正常终止。当用退出键
消sleep
进程时,发生同样的事情。终止状态130,131又是怎样得到的呢?原来Bourne she
ll有一个
在其文档中没有清楚地说明的特性,其终止状态是128加上它所执行的命令由一个
信号终止
时的该信号编号数。用交互方式使用shell可以看到这一点。
$ sh 确保运行Bourne shell
$ sh -c "sleep 30"
^? 按中断键
$ echo $? 打印最后一条命令的终止状态
130
$ sh -c "sleep 30"
^\sh:962 Quit-core dumped 按退出键
$ echo $?
131
$ exit 离开Bourne shell
在所使用的系统中,SIGINT的值为2,SIGQUIT的值为3,于是给出shell终止状态1
30,131
。
再做几个类似的例子,这一次将一个信号直接送给shell,然后观察system返回什
么。
$ tsys "sleep 30" ^ 这一次在后台启动它
[1] 980
$ ps 查看进程ID
PID TT STAT TIME COMMAND
980 p3 S 0:00 tsys sleep 30
981 p3 S 0:00 sh -c sleep 30
982 p3 S 0:00 sleep 30
985 p3 R 0:00 ps
$ kill -KILL 981
kill shell
abnormal termination,signal number=9
[1]+Done tsys "sleep 30" &
从中可见仅当shell本身异常终止时,system的返回值再报告一个异常终止。
在编写使用system函数的程序时,一定要正确地解释返回值。如果直接调用fork,
exec和wa
it,则终止状态与调用system是不同的。
1019〓sleep函数
在本市的很多例子中都已使用了sheep函数,在程序104和105中有两个sleep的
很完善的
实现。
# include<unistdh>
unsigned int sleep(unsigned int seconds);
返回:0或未睡的秒数
此函数使调用进程被挂起直到:
1已经过了seconds所指定的墙上时钟时间,或者
2该进程捕捉到一个信号并从信号处理程序返回。
如同alarm信号一样,由于某些系统活动,实际返回时间比所要求的会迟一些。
在第一种情形,返回值是0。当由于捕捉到某个信号,sleep提早返回时(第二种情
形),返回
值是末睡是的秒数(所要求的时间减实际睡眠时间)。
sleep可以用alarm函数(1010节)实现,但这并不是一定要求的。但是如果使用a
larm,则
这两个函数之间可以有交互作用。POSIX1标准对这些交互作用并未作任何说明。
例如,若
先调用alarm(10),过了3秒后又调用sleep(5),则如何呢?sleep将在5秒后返回(假
定在这
段时间内没有捕捉到另一个信号),但是否在2秒后又产生另一个SIGALRM信号呢?这
种细节依
赖于实现。
SVR4用alarm实现sleep。sleep(3)手册页中说明以前按排的闹钟仍被正常处理。例
如,在前
面的例子中,在sleep返回之前,它安排在2秒后再次到达闹钟时间。在这种情况下
,sleep
返回0。(很明显,sleep必须保存SIGALRM信号处理程序的地址,在返回前重新设置
它)。另
外,如果先做一次alarm(6),3秒钟之后又做一次sleep(5),则在3秒后sleep返回,
而不是5
秒钟。而sleep的返回值则是未睡定的时间2秒。
43+BSD则使用另一种技术:由setitimer(2)提供间隔计时器。该计时器独立于a
larm函数
,但在以前设置的间隔计时器和sleep之间仍能有相互作用。另外,即使闹钟计时
器(alarm)
和间隔计时器(setitimer)是分开的,但是不幸它们使用同一SIGALRM信号。因为s
leep暂时
将该信号的处理程序改变为它自己的函数,所以在alarm和sleep之间仍可能有不所
希望的相
互作用。
如果混合调用sleep和其它与时间有关的函数,它们之间有相互作用,则你应当清
楚地了解
你所使用的系统是如何实现sleep的。
以前贝克莱类的sleep实现不提供任何有用的返回信息。这在43+BSD中已经解决
。
实现
程序1021是一个POSIX1 sleep函数的实现。此函数是程序104的修改版,它
可靠地处
理信号,避免了早期实现中的竞态条件,但是仍未处理与以前设置的闹钟的相互作
用。(正
如前面提到的POSIX1并未对这些交互作用进行定义)。
程序1021 sleep的可靠实现
与程序104相比为了可靠的实现sleep,程序1021的代码比较长。程序中没有使
用任何形
式的非局部转移(如程序105为了避免在alarm和pause之间的竞态条件所做的那样
),所以
对处理SIGALRM信号期间可能执行的其它信号处理程序没有影响。
1020〓作业控制信号
在图101中有六个POSIX1认为是与作业控制有关的信号。
SIGCHLD 要进程已停止或终止。
SIGCONT 如果进程已停止,则使其继续运行。
SIGSTOP 停止信号(不能被捕促或忽略)。
SIGTSTP交互停止信号。
SIGTTIN 一个后台进程组的成员读控制终端。
SIGTTOU 一个后台进程组的成员写控制终端。
虽然仅当系统支持作业控制时,POSIX1才要求它支持SIGCHLD,但是几乎所有Un
ix版本,都支持这种信号。我们已经说明了在子进程终止时这种信号是如何产生的。
大多数应用程序并不处理这些信号〖CD2〗交互式shell通常做处理这些信号的所有
工作。当我们按挂起字符(通常是Control-Z)时,SIGTSTP被送至后台进程组的所有
进程。当我们通知shell在前台或后台恢复一个作业时,该shell向该作业中的所有
进程发送SIGCONT信号。相类似,如果向一个进程传送了SIGTTIN或SIGTTOU信号,
则按系统默认,此进程就停止,作业控制shell了解到这一点后就通知我们。
一个例外是管理终端的进程〖CD2〗例如,vi(1)编辑程序。当用户要挂起它时,它
需要能了解到这一点,这样它就能将终端状态恢复到vi起动时的情况。另外,当
在前台恢复它时,它需
要将终端状态设置回所希望的状态,它需要重画终端屏幕。我们可以在下面的例子
中观察到
vi这样的程序是如何处理这种情况的。
在作业控制信号间有某种相互作用。当对一个进程产生四种停止信号(SIGTSTP,S
IGSTOP,S
IGTTIN或SIGTTOU)中的任何一种时,对该进程的任一末决的SIGCONT信号就被丢弃
。相类似
,当对一个进程产生SIGCONT信号时,对该同一进程的任一末决的停止信号被丢弃
。
注意,SIGCONT的默认动作是继续一个进程,如果该进程是停止的,否则忽略此信
号。通常
,我们对该信号无需做任何事情。当对一个停止的进程产生一个SIGCONT信号时,
该进程就
继续,即使该信号是被阻塞或忽略也是如此。
实例
程序1022例示了当一道程序处理作业控制时所使用的通常的代码序列。这道程序
只是将其
标准输入复制到其标准输出,但是在信号处理程序中以注释形式给出了管理屏幕的
程序所执
行的典型操作。当程序1022起动时,仅当SIGTSTP信号的配置是SIG-DFL,它再安
排捕捉该
信号。其理由是:当此程序由不支持作业控制的shell(例如/bin/sh)所起动时,
此信号的
配
置应当设置为SIG-IGN。实际上,shell并不显式地忽略此信号,而是init将这三个
作业控制
信号SIGTSTP、SIGTTIN和SIGTTOU设置为SIG-IGN。然后,这种配置由所有登录she
ll继承。
只有作业控制shell才应将这三个信号重新设置为SIG-DFL。
当我们键入挂起字符时,进程接到SIGTSTP信号,然后该信号处理被调用。在此点
上,我们
应当进行与终端有关的处理:将光标移到左下角,恢复终端工作方式,等等。在将
SIGTSTP
重新设置为默认值(停止该进程),并且解除了对此信号的阻塞之后,进程向自己发
送同一信
号SIGTSTP。
因为现在正处理SIGTSTP信号,而在捕捉到该信号期间系统自动地阻塞它,所以我
们应当解
除
对此信号的阻塞。仅当某个进程(通常是正响应一个交互式fg命令的作业控制shel
l)向该进
程发送一个SIGCONT信号时,该进程再继续。我们不捕捉SIGCONT信号。该信号的默
认配置是
继续停止的进程,当此发生时,此程序如同从kill函数返回一样继续运行。当此程
序继续运
行时,将SIGTSTP信号再设置为捕捉,并且做我们所希望做的终端处理。(例如可以
重画屏幕
。)
在第十八章中将介绍处理特定的作业控制挂起字符的另一种方法,其中并不使用信
号,而是
由程序自身识别该特定字符。
程序1022 如何处理SIGTSTP
1021〓其它特征
本节介绍某些依赖于实现的信号的其它特征。
信号名字
某些系统提供数组
extern char *sys-siglist 病常华?nbsp;
数组下标是信号编号,数组中的元素是指向一个信号字符串名字的指针。
这些系统通常也提供函数psignal。
字符串msg(通常是程序名)输出到标准出错,后面跟着一个冒号和一个空格,再跟
着对该信
号的说明,最后是一个新行符。
SVR4和43+BSD都提供sys-siglist和psignal函数
SVR4信号处理程序的附加参数
当调用sigaction对一个信号设置配置时,可以指定sa-flags值SA-SIGINFO(图10
5)。这使
两个附加参数传给信号处理程序。整型的信号编号总是作为第一个参数传送。第二
个参数或
者是一个定指针,或者是一个指向siginfo结构的指针。(第三个参数提供在一个进
程内不同
控制线程的有关信息,我们在此不对它进行讨论)。
struct siginfo {
int si 迹茫模*常病絪igno; /* 信号编号 */
int si 迹茫模*常病絜rrno; /* 若非0,则为<errnoh>中的errno值
int si 迹茫模*常病絚ode; /* 附加的info(取决于信号)
pid 迹茫模*常病絫 si 迹茫模*常病絧id; /* 发送进程ID */
uid 迹茫模*常病絫 si 迹茫模*常病絬nid; /* 发送进程实际用户ID */
/* 其它字段 */
};
对于由硬件产生的信号,例如SIGFPE,si-code值给出附加的信息;FPE-INTDIV表
示整数除
以0,FPE-FLTDIV表示浮点数除以0等等。如若si-code小于或等于0,则表示信号是
由调用kill(2)的用户进程产生的。在此情况下,si-pid和si-uid给出了发透此信号的
进程的有关信息。依赖于正被捕捉的信号,还有一些信息可用,见SVR4 siginfo(5)手
册页。
43+BSD信号处理程序的附加参数
43+BSD总是用三个参数调用信号处理程序:
handler(int signo,int code,struct sigcontext *scp);
参数signo是信号编号,code给出某些信号的附加信息。例如,对于SIGFPE的code
值FPE-INT
DIV-TRAP表示整数除以0。第三个参数SCP是与硬件有关的。
1022〓摘要
信号用于很多比较复杂的应用程序中。对进行信号处理的原因和方式有较好理解对
高级Unix程序设计是极其重要的。本章对Unix信号进行详细而且比较深入的介绍。
开始时先说明以前的信号实施的问题以及它们又是如何显现出来的。然后介绍POSIX1
的可靠信号概念以及所有相关的函数。在此基础上接着提供了abort、system和sleep
函数的POSIX1实现。最后以观察分析作业控制信号结束。