Unix之Unix之信号(十)

本文主体内容来自《UNIX环境高级编程第三版》。

本章将更详细地说明进程组以及POSIX.1引入的会话的概念。还将介绍登录shell(登录时所调用的)和所有从登录shell启动的进程之间的关系。


信号概念

信号是软件中断。信号提供了一种处理异步事件的方法。

首先,每个信号都有一个名字。这些名字都以3个字符 SIG 开头。

在头文件 <signal.h> 中,信号名都被定义为正整数常量(信号编号)。实际上,实现将各信号定义在另一个头文件中,但是该头文件又包括在 <signal.h> 中。

不存在编号为 0 的信号。

  • 当用户按某些终端键时,引发终端产生的信号。在终端上按 Delete 键(或者很多系统中的Ctrl+C键)通常产生中断信号( SIGINT)。这是停止一个已失去控制的程序的方法。

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

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

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

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

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

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

(2)捕捉信号。为了做到这一点,要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。注意,不能捕捉 SIGKILLSIGSTOP 信号。

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

下图列出了所有信号的名字,说明了哪些系统支持此信号以及对于这些信号的系统默认动作。

在系统默认动作列,“终止+core”表示在进程当前工作目录的core文件中复制了该进程的内存映像。大多数UNIX系统调试程序都使用core文件检查进程终止时的状态。

img

在下列条件下不产生core文件:(a)进程是设置用户ID的,而且当前用户并非程序文件的所有者;(b)进程是设置组ID的,而且当前用户并非该程序文件的组所有者;(c)用户没有写当前工作目录的权限;(d)文件已存在,而且用户对该文件没有写权限;(e)文件太大(RLIMIT_CORE 限制)。

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

SIGABRT 调用abort函数时产生此信号。进程异常终止。

SIGALRM 当用alarm函数设置的定时器超时时,产生此信号。若由 setitimer 函数设置的间隔时间已经超时时,也产生此信号。

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

SIGCHLD 在一个进程终止或停止时,SIGCHLD 信号被送给其父进程。按系统默认,将忽略此信号。如果父进程希望被告知其子进程的这种状态改变,则应捕捉此信号。信号捕捉函数中通常要调用一种wait函数以取得子进程ID和其终止状态。

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

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

SIGHUP 如果终端接口检测到一个连接断开,则将此信号送给与该终端相关的控制进程(会话首进程)。

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

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

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

SIGIO 此信号指示一个异步I/O事件。

SIGIOT 这指示一个实现定义的硬件故障。 FreeBSD 8.0、Linux 3.2.0、Mac OS X 10.6.8和Solaris 10将 SIGIOT 定义为与 SIGABRT 具相同值。

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

SIGPIPE 如果在管道的读进程已终止时写管道,则产生此信号。当类型为 SOCK_STREAM 的套接字已不再连接时,进程写该套接字也产生此信号。

SIGPOLL 这个信号在SUSv4中已被标记为弃用,将来的标准可能会将此信号移除。当在一个可轮询设备上发生一个特定事件时产生此信号。在Linux和Solaris中,SIGPOLL定义为与 SIGIO 具有相同值。

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

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

SIGSEGV 指示进程进行了一次无效的内存引用(通常说明程序有错,比如访问了一个未经初始化的指针)。名字SEGV代表“段违例”(segmentation violation)。

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

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

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

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

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

SIGTTOU 当一个后台进程组进程试图写其控制终端时,终端驱动程序产生此信号。与上面所述的SIGTTIN信号不同,一个进程可以选择允许后台进程写控制终端。

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

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

SIGUSR2 这是另一个用户定义的信号。

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

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

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


函数signal

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

1
2
3
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
// 返回值:若成功,返回以前的信号处理配置;若出错,返回SIG_ERR

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

4.4BSD 也提供 signal 函数,但它是按照 sigaction 函数定义的,所以在 4.4BSD 之下使用它提供新的可靠信号语义。目前大多数系统遵循这种策略。

因为signal的语义与实现有关,所以最好使用 sigaction 函数代替signal函数。

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

signal 函数原型说明此函数要求两个参数,返回一个函数指针,而该指针所指向的函数无返回值(void)。第一个参数signo 是一个整型数,第二个参数是函数指针,它所指向的函数需要一个整型参数,无返回值。signal 的返回值是一个函数地址,该函数有一个整型参数(即最后的(int))。

使用下面的typedef,则可使其简单一些。

typedef void Sigfunc(int);

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

Sigfunc *signal(int, Sigfunc *);


中断的系统调用

早期UNIX系统的一个特性是:如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno设置为EINTR。这样处理是因为一个信号发生了,进程捕捉到它,这意味着已经发生了某种事情,所以是个好机会应当唤醒阻塞的系统调用。

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

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

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

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

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

  • 某些ioctl操作;

  • 某些进程间通信函数。

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

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

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


可重入函数

可重入函数是指在多线程或多任务环境中安全调用的函数。这意味着如果一个函数在执行过程中被中断(例如,由操作系统调度器切换到另一个线程或任务),并且在中断后再次被调用,那么该函数的执行状态不会受到影响,能够正确地返回结果。具体来说,可重入函数具有以下几个特征:

  1. 无状态性:可重入函数不依赖于全局或静态变量,或者如果使用它们,则在函数内部对它们的访问是安全的。例如,函数不应修改任何共享数据或状态,除非使用适当的同步机制(如锁)。

  2. 局部数据:可重入函数应使用局部变量,这样每个调用都有自己的变量副本,互不干扰。

  3. 无副作用:可重入函数在执行时不应产生影响外部状态的副作用,例如不应修改全局变量、文件等。

  4. 可并发执行:可重入函数可以被多个线程或任务并发调用,而不会导致数据竞争或不一致的状态。

image-20250307194208027


可靠信号术语和语义

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

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

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

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

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

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

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


函数kill和raise

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

1
2
3
4
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
// 两个函数返回值:若成功,返回0;若出错,返回−1

调用 raise(signo); 等价于调用 kill(getpid(), signo);

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

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

pid == 0 将该信号发送给与发送进程属于同一进程组的所有进程(这些进程的进程组 ID等于发送进程的进程组 ID),而且发送进程具有权限向这些进程发送信号。

pid < 0 将该信号发送给其进程组ID等于 pid 绝对值,而且发送进程具有权限向其发送信号的所有进程。

pid == −1 函数将向系统中的所有进程发送信号,除非调用进程没有足够的权限。

如果被发送的信号是 SIGCONT,则进程可将它发送给属于同一会话的任一其他进程。


函数alarm和pause

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

1
2
3
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// 返回值:0或以前设置的闹钟时间的余留秒数

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

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

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

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

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

1
2
3
#include <unistd.h>
int pause(void);
// 返回值:−1,errno设置为EINTR

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


信号集

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

1
2
3
4
5
6
7
8
9
#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指向的信号集,使其包括所有信号。所有应用程序在使用信号集前,要对该信号集调用 sigemptysetsigfillset 一次。这是因为C编译程序将不赋初值的外部变量和静态变量都初始化为0,而这是否与给定系统上信号集的实现相对应却并不清楚。

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


函数sigprocmask 和 sigpending

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

1
2
3
#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 则是赋值操作。注意,不能阻塞 SIGKILLSIGSTOP 信号。

image-20250307102416999

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

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

sigprocmask 是仅为单线程进程定义的。处理多线程进程中信号的屏蔽使用另一个函数。

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

1
2
3
#include <signal.h>
int sigpending(sigset_t *set);
// 返回值:若成功,返回0;若出错,返回−1

函数sigaction

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

1
2
3
#include <signal.h>
int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);
// 返回值:若成功,返回0;若出错,返回−1

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

1
2
3
4
5
6
7
8
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*/
/* alternate handler */
void  (*sa_sigaction)(int, siginfo_t *, void *);
};

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

一旦对给定的信号设置了一个动作,那么在调用 sigaction 显式地改变它之前,该设置就一直有效。

act 结构的 sa_flags 字段指定对信号进行处理的各个选项。下图详细列出了这些选项的意义。

image-20250307114817729

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_signosi_code 成员。另外,符合XSI的实现至少应包含下列字段:

1
2
3
4
5
6
7
8
9
10
11
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 联合包含下列字段:

1
2
int sival_int;
void *sival_ptr;

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


函数sigsetjmp和siglongjmp

调用 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并没有指定 setjmplongjmp 对信号屏蔽字的作用,而是定义了两个新函数 sigsetjmpsiglongjmp。在信号处理程序中进行非局部转移时应当使用这两个函数。

1
2
3
4
#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
// 返回值:若直接调用,返回0;若从siglongjmp调用返回,则返回非0
void siglongjmp(sigjmp_buf env, int val);

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


函数sigsuspend

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

1
2
3
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
// 返回值:−1,并将errno设置为EINTR

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

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


函数abort

abort 函数的功能是使程序异常终止。

1
2
3
#include <stdlib.h>
void abort(void);
// 此函数不返回值

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

ISO C要求若捕捉到此信号而且相应信号处理程序返回, abort 仍不会返回到其调用者。如果捕捉到此信号,则信号处理程序不能返回的唯一方法是它调用exit_exit_Exitlongjmpsiglongjmp。POSIX.1也说明abort并不理会进程对此信号的阻塞和忽略。

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


函数sleep、nanosleep和clock_nanosleep

1
2
3
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
// 返回值:0或未休眠完的秒数

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

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

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

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

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

FreeBSD 8.0、Linux 3.2.0、Mac OS X 10.6.8和Solaris 10用 nanosleep 函数实现sleep,使sleep具体实现与信号和闹钟定时器相互独立。

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

1
2
3
#include <time.h>
int nanosleep(const struct timespec *reqtp, struct timespec *remtp);
// 返回值:若休眠到要求的时间,返回0;若出错,返回−1

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

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

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

1
2
3
#include <time.h>
int clock_nanosleep(clockid_t clock_id, int flags, const struct timespec *reqtp, struct timespec *remtp);
// 返回值:若休眠到要求的时间,返回0;若出错,返回错误码

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

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


函数sigqueue

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

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

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

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

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

1
2
3
#include <signal.h>
int sigqueue(pid_t pid, int signo, const union sigval value);
// 返回值:若成功,返回0;若出错,返回−1

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

信号不能被无限排队。到达相应的 SIGQUEUE_MAX 限制以后,sigqueue 就会失败,将 errno 设为 EAGAIN

下图总结了排队信号在不同的实现中的行为上的差异。

image-20250307175719033


作业控制信号

POSIX.1认为有以下6个信号与作业控制有关。

SIGCHLD 子进程已停止或终止。

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

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

SIGTSTP 交互式停止信号。

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

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

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

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

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

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


信号名和编号

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

extern char *sys_siglist[];

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

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

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

1
2
#include <signal.h>
void psignal(int signo, const char *msg);

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

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

1
2
#include <signal.h>
void psiginfo(const siginfo_t *info, const char *msg);

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

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

1
2
3
#include <string.h>
char *strsignal(int signo);
// 返回值:指向描述该信号的字符串的指针

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