APUE第10章 信号

第10章 信号

APUE第10章 信号

我们可以捕捉信号进行处理,或者忽略信号
捕捉信号SIGTSTP

运行时像进程kill -TSTP pid或者输入ctrl + z即可捕捉到

或者忽略指定信号(SIGKILL 和SIGSTOP不能忽略)

10.5 中断的系统调用

早期UNIX系统的一个特性是:如果进程在执行一个低速系统调用 而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该 系统调用返回出错,其errno设置为EINTR。这样处理是因为一个信号发 生了,进程捕捉到它,这意味着已经发生了某种事情,所以是个好机会 应当唤醒阻塞的系统调用。
在这里,我们必须区分系统调用和函数。当捕捉到某个信号时,被 中断的是内核中执行的系统调用。
为了支持这种特性,将系统调用分成两类:低速系统调用和其他系 统调用。低速系统调用是可能会使进程永远阻塞的一类系统调用,包 括:

果某些类型文件(如读管道、终端设备和网络设备)的数据不 存在,则读操作可能会使调用者永远阻塞;
果这些数据不能被相同的类型文件立即接受,则写操作可能会 使调用者永远阻塞;
某种条件发生之前打开某些类型文件,可能会发生阻塞(例如 要打开一个终端设备,需要先等待与之连接的调制解调器应答);
ause函数(按照定义,它使调用进程休眠直至捕捉到一个信号) 和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风格的语义。

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

为了帮助应用程序使其不必处理被中断的系统调用,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的例子 中,返回给正常调用者的信息可能会被返回给信号处理程序的信息覆 盖。

4064394-0f460e1b05180f85.png image.png

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会返回,并将其errno设置为ECHILD。(此 信号的默认配置是忽略,但这不会使上述语义起作用。必须将其配置明 确指定为SIG_IGN才可以。)

我们需要先定义一些在讨论信号时会用到的术语。首先,当造成信 号的事件发生时,为进程产生一个信号(或向一个进程发送一个信 号)。事件可以是硬件异常(如除以 0)、软件条件(如alarm 定时器超 时)、终端产生的信号或调用kill 函数。当一个信号产生时,内核通常 在进程表中以某种形式设置一个标志。
当对信号采取了这种动作时,我们说向进程递送了一个信号。在信 号产生(generation)和递送(delivery)之间的时间间隔内,称信号是未 决的(pending)。
进程可以选用“阻塞信号递送”。如果为进程产生了一个阻塞的信 号,而且对该信号的动作是系统默认动作或捕捉该信号,则为该进程将 此信号保持为未决状态,直到该进程对此信号解除了阻塞,或者将对此 信号的动作更改为忽略。内核在递送一个原来被阻塞的信号给进程时 (而不是在产生该信号时),才决定对它的处理方式。于是进程在信号 递送给它之前仍可改变对该信号的动作。进程调用sigpending函数(见 10.13节)来判定哪些信号是设置为阻塞并处于未决状态的。
如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,那 么将如何呢OSIX.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函数)的函数。

调用raise(signo);
等价于调用两个函数返回值:若成功,返回0;若出错,返回

kill(getpid(), signo);
kill的pid参数有以下4种不同的情况。

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

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

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;若出错,返回
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>头文件中实 现为宏:

注意,除了设置信号集中各位为1外,sigfillset必须返回0,所以使用
C语言的逗号算符,它将逗号算符后的值作为表达式的值返回。 使用这种实现,sigaddset 开启一位(将该位设置为 1),sigdelset 则
关闭一位(将该位设置为0);sigismember测试一个指定的位。因为没 有信号编号为0,所以从信号编号中减1以得到要处理位的位编号数。图 10-12给出了这些函数的实现。

10.12 函数sigprocmask

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

返回值:若成功,返回0;若出错,返回首先,若oset是非空指针,那么进程的当前信号屏蔽字通过oset返回。
其次,若set是一个非空指针,则参数how指示如何修改当前信号屏
蔽字。图10-13说明了how可选的值。SIG_BLOCK是或操作,而 SIG_SETMASK则是赋值操作。注意,不能阻塞SIGKILL和SIGSTOP信 号。

来源:MachinePlay

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2020年1月18日
下一篇 2020年1月18日

相关推荐