UNIX环境高级编程 学习笔记 第十章 信号

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

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

每个信号都有一个名字,它们由SIG开头。如SIGABRT是夭折信号;SIGALRM是闹钟信号,由alarm函数设置的定时器超时后产生。

头文件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的信号。kill函数对信号编号0有特殊应用。POSIX.1将其称为空信号。

产生信号的一些情况:
1.终端上按Delete或Ctrl+C通常产生中断信号SIGINT。这是停止一个已失去控制程序的方法。
2.硬件异常,如除0、无效的内存引用等。通常由硬件检测到,再通知内核,内核再向异常发生时正在运行的进程产生适当的信号,如,执行一个无效内存引用的进程产生SIGSEGV信号。
3.进程可调用kill将任意信号发送给另一个进程或进程组,但接收信号进程和发送信号进程的所有者必须相同,或信号发送者有root特权。
4.用户可用kill命令将信号发送给其他进程。此命令是kill函数的接口。常用于终止一个失控的后台进程。
5.当检测到某种软件条件已经发生,并应将其通知某进程时。如SUGURG(网络连接上传来带外的数据)、SIGPIPE(管道的读进程已终止后,一个进程写此管道)、SIGALRM(进程设置的定时器超时)。

信号是异步事件的经典案例,产生信号的事件对进程而言是随机出现的。

信号出现时,可以告诉内核按以下三种方式之一处理:
1.忽略信号。只有两种信号不能忽略,即SIGKILL和SIGSTOP,因为它们是为了向内核和root提供使进程终止或停止的可靠方法。如忽略某些硬件异常产生的信号,则进程运行行为未定义。
2.捕捉信号。要通知内核在某信号发生时,调用一个用户函数,在其中处理它。如,捕捉到SIGCHLD信号,表示一个子进程已经终止,此信号的捕捉函数可以调用waitpid以取得子进程的ID和它的终止状态;又例如,进程创建了临时文件,可能就要为SIGTERM(终止信号,也是kill命令传送的默认信号)信号编写一个信号捕捉函数以清除临时文件。
3.执行系统默认操作,每个信号执行的默认操作见下图。
在这里插入图片描述
图中SUS列中的·表示信号是基本POSIX.1规范部分,XSI表示信号定义在XSI扩展部分。终止+core表示在进程当前工作目录中将该进程的内存映像复制到名为core的文件中。大多数UNIX系统调试程序使用core文件检查进程终止时的状态。

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

不同系统中core文件的名字可能不同,FreeBSD 8.0中,其名为cmdname.core(cmdname是接收到信号的进程的命令名);Mac OS X 10.6.8中,其名为core.pid(pid是接收到信号的进程的id);这两个系统允许经sysctl命令配置core文件名。Linux 3.2.0中,可通过/proc/sys/kernel/core_pattern文件配置core文件名。

大多数实现在进程的工作目录中产生core文件,但Mac OS X将所有core文件都放置在/cores目录。

不产生core文件的情况:
1.进程是设置用户ID的,且当前用户并非程序文件的所有者。
2.进程是设置组ID的,且当前用户不是该程序文件的组所有者。
3.用户没有写当前进程工作目录的权限。
4.core文件已存在,但用户没有对该文件的写权限。
5.core文件太大(RLIMIT_CORE限制)。

core文件的权限(假定该文件在要产生之前不存在)通常是用户读、写,但Mac OS X将其设置为只用户读。

信号说明:
1.SIGABRT。调用abort时产生此信号,进程异常终止。
2.SIGALRM。当alarm函数设置的定时器超时时,或由setitimer函数设置的间隔时间已经超时时,产生此信号。
3.SIGBUS。指示一个实现定义的硬件故障。实现常常在出现某种内存故障时产生它。
4.SIGCANCEL。Solaris线程库内部使用的信号。不适用于一般应用。
5.SIGCHLD。进程终止或停止时,该信号被送给其父进程。系统默认忽略此信号。父进程可捕捉此信号,并可在信号捕捉函数中调用wait取得子进程ID和终止状态。System V早期版本有一个名为SIGCLD的类似信号,它具有其他语义,SVR2的手册页警告新程序中最好不要使用SIGCLD,但令人奇怪的是在SVR3和SVR4版的手册页中,该警告消失了。应用应当使用标准的SIGCHLD信号,但为了向后兼容,很多系统定义了与SIGCHLD等同的SIGCLD,如果有使用SIGCLD信号的软件,需查询系统手册,了解其具体的语义。
6.SIGCONT。此信号发送给需要继续运行,但现在处于停止状态的进程。如果进程收到该信号时处于停止状态,则系统默认操作是使该进程继续运行,否则,忽略此信号。
7.SIGEMT。指示一个实现定义的硬件故障。EMT名字来自于PDP-11的仿真器陷入指令(又称自陷指令或访管指令,用于实现在用户态下运行的进程调用操作系统内核程序,即当运行的用户进程或系统实用进程欲请求操作系统内核为其服务时,可以安排执行一条陷入指令引起一次特殊异常)。并非所有平台都支持此信号。例如,Linux只对SPARC、MIPS和PA_RISC等系统结构支持SIGEMT。
8.SIGFPE。表示算术运算异常,如除0、浮点溢出等。
9.SIGFREEZE。仅由Solaris定义,用于通知进程在冻结系统状态之前需要采取特定动作。如系统休眠或挂起时可能需要做这种处理。
10.SIGHUP。如果终端接口检测到一个连接断开,则将此信号送给与该终端相关的控制进程(会话首进程)。此信号被送给sesssion结构中s_leader字段所指向的进程。只有在终端的CLOCAL标志没有设置时,才会在上述条件下产生此信号。如果连接的终端是本地的,则设置该终端的CLOCAL标志,它告诉终端驱动程序忽略所有调制解调器的状态行。接收到此信号的会话首进程可能在后台,这区别于终端正常产生的中断、退出、挂起信号,它们总是传给前台进程组。如果会话首进程终止,也产生此信号,此时该信号传给前台进程组中的每一个进程。通常用此信号通知守护进程再次读取它们的配置文件,使用此信号通知的原因是守护进程不会有控制终端,通常决不会收到这种信号。
11.SIGILL。表示进程已执行一条非法硬件指令。4.3BSD的abort函数产生此信号,现在该函数产生SIGABRT信号。
12.SIGINFO。一种BSD信号,用户按状态键(一般是Ctrl+T)时,终端驱动程序产生此信号并发送至前台进程组中的每个进程。此信号一般在终端上显示前台进程组中各进程的状态信息。Alpha平台将SIGINFO定义为与SIGPWR具有相同值,Linux不支持SIGINFO信号。
13.SIGINT。用户按中断键(Delete或Ctrl+C)时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程。可用于终止进程。
14.SIGIO。指示一个异步IO事件。上图中对其默认动作的说明是终止或忽略,这依赖于系统。System V中,SIGIO和SIGPOLL相同,默认终止此进程;BSD中,默认忽略此信号;Linux 3.2.0和Solaris 10将SIGIO定义为与SIGPOLL具有相同值,默认终止此进程;FreeBSD 8.0和Mac OS X 10.6.8中,默认忽略此信号。
15.SIGIOT。指示一个实现定义的硬件故障。IOT名字来自于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具有相同值。
16.SIGJVM1。Solaris上为Java虚拟机预留的信号。
17.SIGJVM2。Solaris上为Java虚拟机预留的信号。
18.SIGKILL。不能被捕捉。系统管理员用它可杀死任意进程。
19.SIGLOST。运行在Solaris NFSv4客户端系统中的进程在恢复阶段不能重新获得锁,此时将由此信号通知该进程。
20.SIGLWP。由Solaris线程库内部使用。在FreeBSD中,SIGLWP是SIGTHR的别名。
21.SIGPIPE。在管道的读进程终止时写管道会产生此信号。当类型为SOCK_STREAM的套接字不再连接时,进程写该套接字也产生此信号。
22.SIGPOLL。在SUSv4中已被弃用,将来的标准可能会移除它。在一个可轮询设备上发生特定事件时产生此信号。它起源于SVR3,与BSD的SIGIO和SIGURG接近。Linux和Solaris中,SIGPOLL被定义为与SIGIO具有相同值。
23.SIGPROF。在SUSv4中已被弃用,将来的标准可能会移除它。当setitimer函数设置的梗概统计间隔定时器超时时产生它。
24.SIGPWR。依赖于系统。主要用于具有不间断电源(UPS)的系统,如电源失效,则UPS起作用,软件通常也会接收到通知。此时系统依靠蓄电池继续运行,无须做任何处理。但蓄电池将不能支持工作时,软件通常会再次接收到通知,此时系统必须使其各部分都停止运行,此时应发送SIGPWR信号,大多数系统中的接到蓄电池电压过低信息的进程将信号SIGPWR发送给init进程,然后由init处理停机操作。Solaris 10和有些Linux版本在inittab文件中有两个记录项用于此种目的:powerfail和powerwait(powerokwait)。上图中,SIGPWR的默认动作为终止或忽略,它依赖于系统,Linux对此的默认动作是终止相关进程,而Solaris默认忽略。
25.SIGQUIT。用户在终端按退出键(Ctrl+\)时,终端驱动程序产生它并发送给前台进程组中所有进程。此信号不仅终止前台进程组(SIGINT这样做),还产生一个core文件。
26.SIGSEGV。指示进程进行了一次无效的内存引用(通常说明程序有错,如访问了未初始化的指针)。SEGV表示段违例(segmentation violation)。
27.SIGSTKFLT。仅由Linux定义,出现在Linux早期版本,企图用于数学协处理器的栈故障。该信号并非由内核产生,但保留以向后兼容。
28.SIGSTOP。作业控制信号,终止一个进程。它类似交互停止信号SIGTSTP,但不能被捕捉和忽略。
29.SIGSYS。指示一个无效的系统调用。发生在由于某种特殊原因,进程执行了一条机器指令,内核认为这是一条系统调用,但该指令指示的系统调用类型的参数是无效的时,这可能发生在用户编写了一个使用新系统调用的程序,但所用系统太旧不支持此系统调用的情况下。
30.SIGTERM。由kill命令发送的系统默认终止信号。该信号可由应用捕获,以使程序在退出前有机会做好清理工作。
31.SIGTHAW。仅由Solaris定义,在被挂起的系统恢复时,通知相关进程以采取特定操作。
32.SIGTHR。FreeBSD线程库预留的信号,其值与SIGLWP相同。
33.SIGTRAP。指示一个实现定义的硬件故障。名字TRAP来自PDP-11的TRAP指令。执行断点指令时,实现常用此信号将控制转移到调试程序。
34.SIGTSTP。交互停止信号。当用户在终端上按挂起键(Ctrl+Z)时,终端驱动程序产生此信号,发送到前台进程组中的所有进程。停止有不同的含义,可以是停止和继续作业,而终端驱动程序使用术语“停止”一词表示Ctrl+S这一字符的作用,即表示终止终端输出,为启动该终端的输出,使用Ctrl+Q字符。因此终端驱动程序称该信号为挂起字符,而非停止字符。(Ctrl+任意字母键都有一个ASCII码字符对应)
35.SIGTTIN。当一个后台进程组进程尝试读其控制终端时,终端驱动进程产生此信号。但当读进程忽略或阻塞此信号、读进程所属的进程组是孤儿进程组时不产生此信号,并且读操作返回出错,errno设置为EIO。
36.SIGTTOU。当一个后台进程组进程尝试写其控制终端时,终端驱动进程产生此信号。与SIGTTIN不同,一个进程可以选择允许后台进程写控制终端(此时不产生信号)。而如果不允许后台进程写时,当写进程忽略或阻塞此信号、写进程所属的进程组是孤儿进程组时不产生此信号,后一种情况下写操作返回出错,errno设为EIO。不论是否允许后台进程写,写操作以外的下列函数也产生此信号,如tcsetattr、tcsendbreak、tcdrain、tcflush、tcflow、tcsetpgrp。
37.SIGURG。通知进程已经发生一个紧急情况。在网络连接上接到带外数据时,可产生此信号。SIGURG信号的默认动作是忽略。
38.SIGUSR1。用户定义的信号,可用于应用程序。
39.SIGUSR2。用户定义的信号,可用于应用程序。
40.SIGVTALRM。当setitimer函数设置的虚拟间隔时间超时时,产生它。
41.SIGWAITING。由Solaris线程库内部使用。
42.SIGWINCH。内核维持与每个终端或伪终端相关联窗口的大小,进程可用ioctl函数得到或设置窗口大小,如果ioctl函数更改了窗口大小,则内核将SIGWINCH信号发送至前台进程组。
43.SIGXCPU。SUS的XSI扩展支持资源限制的概念,如果进程超过了其软CPU时间限制,则产生此信号。上图中显示该信号的默认动作为终止或终止+core,这依赖于操作系统。Linux 3.2.0和Solaris 10的默认动作是终止并创建core文件。FreeBSD 8.0和Mac OS X 10.6.8默认终止但不产生core。SUS只要求终止进程,但core文件是否生成取决于具体实现。
44.SIGXFSZ。如果进程超过了其软文件长度限制,则产生此信号。上图中显示该信号的默认动作为终止或终止+core,这依赖于操作系统。Linux 3.2.0和Solaris 10的默认动作是终止并创建core文件。FreeBSD 8.0和Mac OS X 10.6.8默认终止但不产生core。SUS只要求终止进程,但core文件是否生成取决于具体实现。
45.SIGXRES。仅由Solaris定义,可选择使用此信号通知进程超过了预配置的资源值。

UNIX信号机制最简单的接口:
在这里插入图片描述
signal函数原型说明此函数要求两个参数,返回一个函数指针,且该指针指向的函数无返回值(void)、有一个int型参数。signal函数的第一个参数signo是一个整型数,第二个参数是一个函数指针,该函数指针指向的函数无返回值、有一个int型参数。

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

UNIX System V派生的实现支持signal函数,但它提供的是旧的、不可靠的语义,提供此函数是为了向后兼容,新程序不应使用它。

4.4BSD也提供signal函数,但它是按sigaction函数定义的,因此在该系统中使用它可提供新的可靠的语义。大多系统遵循这种策略,但Solaris 10沿用System V signal函数的定义。

signal的语义与具体实现有关,最好使用sigaction函数代替它。

signo参数是信号名,func参数的值可以是:
1.常量SIG_IGN。向内核表示忽略此信号(SIGKILL、SIGSTOP不能被忽略)。
2.常量SIG_DFL。接到此信号的动作是系统默认动作。
3.当接到此信号后要调用的函数的地址。信号发生时,调用该函数(捕捉该信号)。此函数称之为信号处理程序或信号捕捉函数。

很多系统用附加的依赖于实现的参数来调用信号处理程序。

signal函数的原型太复杂,可使用typedef简化:

typedef void Sigfunc(int);

之后signal函数的原型变为:

Sigfunc *signal(int, Sigfunc *);

signal.h中可能会有以下声明:

#define SIG_ERR (void (*)())-1
#define SIG_DFL (void (*)())0
#define SIG_IGN (void (*)())1

以上常量可用于表示指向函数的指针,该函数要求一个整型参数,而且无返回值。signal函数的第二个参数及其返回值都可用它们表示。这些常量用的3个值不一定是-1、0、1,但它们不能是任一可声明函数的地址。大多UNIX实现使用以上值。

以上宏定义不是#define SIG_ERR (void (*)(int))-1的原因为,在C语言中,可以这样:

void fun();    // 由于C中没有函数重载,因此这样就可以确定唯一函数
 
int main() {
    fun(12, 34);
}
 
void fun(int i, int j) { }

使用信号的程序:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

static void sig_usr(int);

int main() {
    if (signal(SIGUSR1, sig_usr) == SIG_ERR) {
        printf("can not catch SIGUSR1\n");
	    exit(1);
    }

    if (signal(SIGUSR2, sig_usr) == SIG_ERR) {
        printf("can not catch SIGUSR2\n");
	    exit(2);
    }

    for ( ; ; ) {
        pause();    // 使调用进程(或线程)进入休眠状态,直到进程被信号终止或调用信号捕获函数
    }
}

static void sig_usr(int signo) {
    if (signo == SIGUSR1) {
        printf("received SIGUSR1\n");
    } else if (signo == SIGUSR2) {
        printf("received SIGUSR2\n");
    } else {
        printf("received signal %d\n", signo);
    }
}

运行它:
在这里插入图片描述
以上程序不捕捉SIGTERM信号,对该信号的默认动作是终止。

执行一个程序时,所有信号的状态都是系统默认,除非该程序的父进程忽略了某信号,而exec时继承对信号的忽略。exec函数将原来要捕捉的信号都改为默认动作,其他信号的状态不变。这是由于exec把原进程的代码段等信息都替换掉了,只留下了小部分如进程ID的信息,因此,信号处理程序的入口地址也找不到了,但忽略信号并不需要入口地址,因此,忽略信号的处理方式被保留。atexit函数登记的退出函数也会失效,原理同上。

对于一个非作业控制的交互式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函数返回值是以前的信号配置。这两个调用也暴露了这种函数的限制,即不改变信号的处理方式就不能知道信号当前的处理方式。

进程fork产生子进程时,子进程继承父进程的信号处理方式,原因是子进程会复制父进程的内存映像,所以信号捕捉函数的地址在子进程中是有意义的。

早期UNIX版本(如V7)中信号是可能丢失的,进程对信号的控制能力也差,不能阻塞一个信号,阻塞信号含义是不忽略该信号,在其发生时记住它,然后在进程做好准备时通知它。

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

早期版本中,进程每次接收到信号对其处理时,会将该信号动作重置为默认值。因此早期系统的编程书籍有这样一个案例处理中断信号:

int sig_int();    // signal handling function
/* ... */
signal(SIGINT, sig_int);    // establish handler
/* ... */
sig_int() {
    signal(SIGINT, sig_int);    // reestablish handler for next time
    /* ... */
}

早期C不支持ISO C的void数据类型,因此信号处理程序返回值声明为int。

以上代码大多情况下会正常工作,但当信号发生后到信号处理程序调用signal函数之间有一个时间窗口,这段时间可能会发生另一次中断信号,而第二次中断信号会执行默认动作,即终止该进程。

早期版本中,当不想某信号出现时,不能关闭该信号,进程能做的就只是忽略该信号。

有时候我们想阻止某信号发生,如果它们发生了,则记录信号发生过:

int sig_int();    // signal handling funciton
int sig_int_flag;    // set nonzero when signal occurs,记录信号的发生

main() {
    signal(SIGINT, sig_int);
    while (sig_int_flag == 0) {
        pause();    // goto sleep, waiting for signal
    }
}

sig_int() {
    signal(SIGINT, sig_int);
    sig_int_flag = 1;
}

以上程序也有一个时间窗口,如果在测试sig_int_flag后,调用pause前发生信号,此进程调用pause时可能将永久休眠(信号不再发生时),于是,这次发生的信号就丢了。这种代码大部分时间能工作,但会出错,且这种错误排除很困难。

早期UNIX的一个特性是:如果进程在“慢速”系统调用中被阻塞时捕获到信号,则该系统调用被中断不再执行,该系统调用返回出错,并将errno设为EINTR。这是基于以下假设的:既然一个信号发生了,并且进程捕捉到了它,那么很有可能发生了什么事情,应该唤醒被阻塞的系统调用。这里要区分函数和系统调用,当捕捉到某个信号时,被中断的是内核中执行的系统调用。

为支持这种特性,系统调用被分为低速系统调用和其他系统调用。低速系统调用是可能导致进程永远阻塞的一类系统调用,如:
1.如果某些类型文件(如读管道、终端设备、网络设备)的数据不存在,则读操作可能使调用者永远阻塞。
2.如果数据不能被1中相同类型文件立即接受,则写操作可能使调用者永远阻塞。
3.某条件发生前打开某类型设备可能会发生阻塞,如打开一个终端设备,需要先等待与之相连的调制解调器应答。
4.pause函数(它使调用进程休眠直至捕捉到一个信号)和wait函数。
5.某些ioctl操作。
6.某些进程间通信函数。

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

一种可以用中断系统调用来处理的情况:一个进程启动了读操作,但使用该终端设备的用户却离开终端很久。

对于中断的read和write系统调用,POSIX.1的语义在该标准的2001版有所改变,此前,如何处理read和write已完成的数据,允许实现具体选择。例如,如果read系统调用已接收并传送了部分数据至应用缓冲区,此时被中断,OS可以认为该系统调用失败,并将error设为EINTR;也可认为该系统调用成功返回,返回值是已接收的数据。write函数也是如此。历史上,System V派生的实现将其视为失败,而BSD派生的实现则处理为部分成功返回。2001版POSIX.1标准采用BSD风格语义。

处理(此处是重启处理)被中断的系统调用read(System V风格处理方式):

again:
    if ((n = read(fd, buf, BUFFSIZE)) < 0) {
        if (errno == EINTR) {
            goto again;    
        }
    }

为使应用程序不必处理被中断的系统调用,4.2BSD引进了对一些被中断的系统调用的自动重启动,这些自动重启动的系统调用有:ioctl、read、readv、write、writev、wait、waitpid,前5个函数只有对低速设备进行操作时才会被信号中断,而wait和waitpid函数在等待期间捕捉到信号时总是被中断。而有些应用不希望这些函数被中断后重启动,为此4.3BSD允许基于每个信号禁用此功能。

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

历史上,使用signal函数建立信号处理程序时,如何处理被中断的系统调用,各实现做法互不相同。System V默认不重启系统调用;BSD启动被信号中断的系统调用;FreeBSD 8.0、Linux 3.2.0、Mac OS X 10.6.8中,当信号处理程序是用signal函数设置的,被中断的系统调用才会重启;Solaris 10默认是出错返回,并将errno设为EINTR。自己定义signal函数可避免这些差异。

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

在这里插入图片描述
进程捕捉到信号时,会中断正在执行的指令序列,从信号处理程序返回时(假如没有调用exit或longjmp),则继续执行指令序列。但在信号处理时,不能判断捕捉信号时进程在执行什么,如果进程在执行malloc,而在信号处理函数中又调用malloc,可能对进程造成破坏,因为malloc函数通常为它所分配的存储区维护一个链表,而此时插入一个信号处理函数,进程可能正在更改此链表(改了一部分,信号处理程序又在还未修改完成的基础上修改);或进程在调用getpwnam(用来逐一搜索参数name指定的用户名,找到后将该用户的数据以passwd结构返回)这种将其结果放在静态存储单元中的函数,在信号处理程序中又调用了,则返回给正常调用者的信息可能会被返回给信号处理程序的信息覆盖。

SUS说明了在信号处理中保证调用安全的函数,这些函数是可重入的,并被称为是异步信号安全的。这些函数除了可重入(reentrant)之外,还能阻塞任何会引起不一致的信号。以下是异步信号安全的函数:
在这里插入图片描述
其他函数大部分是不可重入的,可能因为:
1.它们使用静态数据结构。
2.它们调用malloc或free。
3.它们是标准IO函数,标准IO库很多实现以不可重入方式使用全局数据结构。

信号处理程序中可能也会调用printf函数,但这不保证产生预期的结果,信号处理程序可能中断主进程中的printf函数调用。

即使信号处理程序调用的是上图中的可重入函数,但由于每个进程只有一个errno,所以信号处理程序可能会修改其原先值。因此当在信号处理程序中调用以上函数时,应在调用前保存errno的值,调用后恢复errno。如SIGCHLD常被捕捉,其信号处理程序通常需要调用wait函数,而wait函数能改变errno。

上图中不包括longjmp和siglongjmp函数,这是因为在主例程以非可重入方式更新一个数据结构时可能产生信号,如果信号处理时不是从信号处理程序返回而是调用siglongjmp,那么该数据结构可能是部分更新的。因此,如果程序要更新全局数据结构,而同时要捕捉某些信号,而这些信号的处理程序中会调用siglongjmp,则更新数据结构时要阻塞此类信号。

调用alarm每秒产生一次SIGALRM信号,同时调用非可重入函数getpwnam:

#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>

static void my_alarm(int signo) {
    struct passwd *rootptr;

    printf("in signal handler\n");
    if ((rootptr = getpwnam("root")) == NULL) {
        printf("getpwnam(root) error\n");
	    exit(1);
    }

    alarm(1);    // 在头文件unistd.h
}

int main() {
    struct passwd *ptr;    // passwd在头文件pwd.h

    signal(SIGALRM, my_alarm);
    alarm(1);

    for( ; ; ) {
        if ((ptr = getpwnam("sar")) == NULL) {    // getpwnam在头文件pwd.h
	   		printf("getpwnam error\n");
	    	exit(1);
		}

		if (strcmp(ptr->pw_name, "sar") != 0) {
		    printf("return value corrupted! pw_name = %s\n", ptr->pw_name);
		}
    }
}

该程序运行结果有随机性,通常程序会被SIGSEGV信号终止,检查core文件,可以看到main已调用getpwnam,但当getpwnam函数调用free时,信号处理程序中断了它的运行,并调用getpwnam,进而再次调用free,从而损坏了malloc和free函数维护的数据结构。

SIGCLD和SIGCHLD区别:
1.SIGCLD是System V的信号,其语义与BSD的SIGCHLD不同。
2.POSIX.1采用的是BSD的SIGCHLD信号。

子进程状态改变后向父进程发送SIGCHLD信号。

基于SVR4的系统继承了用signal或sigset函数(早期设置信号的配置、与SVR3兼容的函数)设置信号的配置,尽管这是有兼容性限制的。对SIGCLD早期处理方式:
1.如果进程将该信号的配置设置为SIG_IGN,则调用进程的子进程将不产生僵死进程。子进程在终止时,将其状态丢弃。如果此时调用进程调用wait,那么它将阻塞到所有子进程都终止,并返回-1,并将errno置为ECHILD(含义为No child processes)。但此信号的默认动作是忽略,而非SIG_IGN。POSIX.1未说明SIGCHLD被忽略时的后果,因此此行为是允许的。SUS的XSI选项可以要求对SIGCHLD支持SIGCLD的行为。如果SIGCHLD被忽略,4.4BSD总是产生僵死进程,要避免僵死进程,则必须等待子进程。SVR4中,如果调用signal或sigset将SIGCHLD配置设置为忽略,则不会产生僵死进程。sigaction函数可设置SA_NOCLDWAIT标志,以避免进程僵死。
2.如果捕捉SIGCLD,则内核立即检查是否有子进程准备好被等待,如果有,调用SIGCLD信号处理程序。

使用SIGCLD:

#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

static void sig_cld(int);

int main() {
    pid_t pid;

    if (signal(SIGCLD, sig_cld) == SIG_ERR) {
        perror("signal error\n");
    }

    if ((pid = fork()) < 0) {
        perror("fork error\n");
    } else if (pid == 0) {
        sleep(2);
	    _exit(0);
    }

    pause();
    exit(0);
}

static void sig_cld(int signo) {
    pid_t pid;
    int status;

    printf("SIGCLD received\n");

    if (signal(SIGCLD, sig_cld) == SIG_ERR) {
        perror("signal error\n");
    }

    if ((pid = wait(&status)) < 0) {
        perror("wait error\n");
    }

    printf("pid = %d\n", pid);
}

执行它:
在这里插入图片描述
但该程序在某些传统System V平台上不能正常工作,程序将一行行不断输出“SIGCLD received”,最后进程用完其栈空间并异常终止。这是因为进入信号处理程序后,首先调用signal函数重新设置此信号处理程序,但设置时还没有调用wait接收子进程信息,又因为System V中刚设置SIGCLD就立即在内核中寻找等待被接收的子进程,从而又触发了信号处理程序,其中又会重新设置信号处理程序,从而无限循环。

基于BSD的系统通常不支持早期System V的SIGCLD语义,因此FreeBSD 8.0和Mac OS X 10.6.8没有出现以上无限循环的问题。Linux 3.2.0虽然将SIGCLD和SIGCHLD定义为相同值,但在设置信号处理程序且已有进程准备好被接收时,并不立即调用信号处理程序。Solaris 10应该会出现此问题,但内核中增加了避免此问题出现的代码。但有些系统(AIX)依然有此问题。

为解决此问题,要在wait函数取到子进程终止状态后再调用signal。

POSIX.1标准的sigaction函数在信号发生时不将信号重置为默认值。

在Linux 3.2.0和Solaris 10上,SIGCLD等同于SIGCHLD。

术语:
1.当造成信号的事件发生时,为进程产生一个信号(或向进程发送一个信号)。当一个信号产生时,内核通常在进程表中以某种形式设置一个标志。
2.当信号动作被执行时,我们说向进程递送了一个信号。
3.信号产生和递送之间,称其为未决的。

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

在进程解除对某信号的阻塞前,信号如果发生了多次,则POSIX.1允许递送该信号一次或多次。如递送了多次,则称这些信号进行了排队。除非支持POSIX.1实时扩展,否则大多UNIX不对信号排队,从而只能递送该信号一次。

多个信号递送给同一个进程时,递送顺序POSIX.1没有规定,但它建议先递送与进程当前状态有关的信号(如SIGSEGV)。

每个进程都有一个信号屏蔽字,它规定了要阻塞递送到该进程的信号集。屏蔽字中每一位对应一种信号,置位时即屏蔽。进程可调用sigprocmask来检测和更改当前信号屏蔽字。

POSIX.1定义了新类型sigset_t,它保证可以容纳一个信号集。

kill函数将信号发送给进程或进程组,raise函数允许进程向自身发送信号:
在这里插入图片描述
向自身发送信号:

raise(signo);
kill(getpid(), signo);

函数kill的pid参数:
1.pid>0,将信号发送给该进程ID。
2.pid=0,将信号发送给调用进程所在进程组的所有进程(但不包括实现定义的系统进程集(包括内核进程和pid为1的init进程))。发送进程要有向这些进程发送信号的权限。
3.pid<0,将信号发送给值为参数pid的绝对值的进程组中所有进程(发送进程要有向这些进程发送信号的权限(不包括情况与2中相同))。
4.pid=-1,将信号发送给所有有权限向它们发送信号的进程(不包括的进程如上)。

root有权将信号发送给任意进程。非root用户发送权限的基本规则是发送者的实际用户ID或有效用户ID等于接收者的实际用户ID或有效用户ID。如果实现支持POSIX.1中的_POSIX_SAVED_IDS,则检查接受者的保存的设置用户ID而非有效用户ID。一个特例是可将SIGCONT信号发送给属于同一会话的任一其他进程。

POSIX.1定义信号编号0为空信号,signo为0时,kill函数仍执行正常的错误检查,但不发送信号,这常用来检测一个特定进程是否存在。向不存在的进程发送空信号时,kill函数返回-1,errno置为ESRCH。但进程ID可重用,当前具有特定ID的进程不一定是我们想要的进程。

函数kill测试进程是否存在的操作不是原子操作,可能kill函数返回检测进程是否存在的结果时,被测试的进程已被终止。

使用函数kill向调用进程产生信号时,该信号不被阻塞,在kill函数返回前,参数signo或其他某个未决的非阻塞信号被传送到该进程。

alarm函数可设一个定时器(闹钟时间),将来该定时器会超时并产生SIGALRM信号,该信号默认终止调用alarm的进程:
在这里插入图片描述
经过seconds参数指定的秒数后,由内核产生该信号,由于进程调度的延迟,进程得到控制并处理该信号还需一个时间间隔。

早期UNIX可能比预定值提前1s发送信号,POSIX.1更准。

进程只能有一个闹钟时间,如果二次调用alarm,且第一次调用还没有超时,则会返回该闹钟时间的余留值,而以前注册的时间被新值代替,如果此新值为0,则取消闹钟时间。

要想捕捉SIGALRM信号,需要在调用alarm前安装该信号的处理程序。

SIGALRM会终止进程,因此大多程序都会捕捉它,从而可以执行清理操作后再终止。

pause函数使调用进程挂起,直到捕捉到一个信号:
在这里插入图片描述
只有捕捉到信号并执行了信号处理程序从其返回时,pause才返回,此时,pause返回-1,并将errno置为EINTR。

使用alarm和pause函数可使进程休眠一段时间:

#include <signal.h>
#include <unistd.h>
#include <stdio.h>

static void sig_alrm(int signo) { }

unsigned int sleep1(unsigned int seconds) {
    if (signal(SIGALRM, sig_alrm) == SIG_ERR) {
        return seconds;
    }

    alarm(seconds);
    pause();
    return alarm(0);
}

int main() {
    printf("before sleep1\n");
    sleep1(3);
    printf("done\n");
}

以上实现有三个问题:
1.如果sleep1函数被调用前,调用者已设置了闹钟,则会被sleep1函数中的第一次alarm调用擦除。改正方法是:检查alarm的返回值,如果小于seconds参数值,则sleep1应等待已有闹钟超时,否则,应重置闹钟为seconds,并在sleep1结束时重置此闹钟,使其在之前闹钟的设定时间再次超时。
2.修改了对SIGALRM的配置。可以在函数被调用时先保存原配置,在函数结束时再恢复原配置。
3.alarm和pause函数间有竞争条件,在一个繁忙系统中,可能alarm函数在pause被调用前就超时了。可以调用setjmp在信号处理程序中跳回或用sigprocmask和sigsuspend函数改正。

SVR2中使用setjmp函数解决上述第三个问题:

#include <setjmp.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>

static jmp_buf env_alrm;

static void sig_alrm(int signo) {
    longjmp(env_alrm, 1);
}

unsigned int sleep2(unsigned int seconds) {
    if (signal(SIGALRM, sig_alrm) == SIG_ERR) {
        return seconds;
    }

    if (setjmp(env_alrm) == 0) {
        alarm(seconds);
	    pause();
    }

    return alarm(0);    // turn off timer, return unslept time
}

int main() {
    printf("before sleep2\n");
    sleep2(3);
    printf("done\n");
}

这样,即使SIGALRM信号在pause函数执行前递送给进程,也不会卡在pause函数处。

但sleep2中涉及了与其它信号的交互,如果SIGALRM中断了某个其他信号处理程序,则调用longjmp会提前终止该信号处理程序:

#include <stdlib.h>
#include <setjmp.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>

static jmp_buf env_alrm;

unsigned int sleep2(unsigned int);
static void sig_int(int);

int main() {
    unsigned int unslept;

    if (signal(SIGINT, sig_int) == SIG_ERR) {
        printf("signal(SIGINT) error\n");
	    exit(1);
    }
    unslept = sleep2(5);
    printf("sleep2 returned: %u\n", unslept);
    exit(0);
}

static void sig_alrm(int signo) {
    longjmp(env_alrm, 1);
}

unsigned int sleep2(unsigned int seconds) {
    if (signal(SIGALRM, sig_alrm) == SIG_ERR) {
        return seconds;
    }

    if (setjmp(env_alrm) == 0) {
        alarm(seconds);
		pause();
    }

    return alarm(0);    // turn off timer, return unslept time
}

static void sig_int(int signo) {
    int i, j;
    volatile int k;

    printf("\nsig_int starting\n");
    // 循环运行时间长于5s
    for (i = 0; i < 300000; ++i) {
        for (j = 0; j < 4000; ++j) {
		    k += i * j;
		}
    }

    printf("sig_int finished\n");
}

执行它:
在这里插入图片描述
在休眠的5秒内按下中断字符,SIGINT的中断处理程序没有运行完就会被SIGALRM的中断处理程序中断。如果将SVR2的sleep函数与其它信号处理程序一起使用,就可能发生这种情况。

alarm函数还常用于对可能阻塞的操作设置时间限制,如程序中有一个读低速设备的操作,它可能被阻塞,我们希望超过一定时间后停止该操作:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

static void sig_alrm(int signo) {
    // just return
}

static unsigned MAXLINE = 50;

int main() {
    int n;
    char line[MAXLINE];

    if (signal(SIGALRM, sig_alrm) == SIG_ERR) {
        printf("signal(SIGALRM) error\n");
	    exit(1);
    }

    alarm(10);

    if ((n = read(STDIN_FILENO, line, MAXLINE)) < 0) {
        printf("read error\n");
	    exit(1);
    }

    alarm(0);

    write(STDOUT_FILENO, line, n);
    
    exit(0);
}

以上代码中有两个问题:
1.alarm的第一次调用和其后的read调用有一个竞争条件,当内核在两个函数的调用之间使进程阻塞,且阻塞时间超过闹钟时间(上例中是10s),那么read函数可能被永远阻塞(如果STDIN_FILENO永远没人输入)。可将闹钟时间设置长一些,如几分钟,使这种问题不会发生。
2.如果系统调用是自启动的,当从SIGALRM的信号处理程序返回时,read函数还会重新启动,时间限制就失效了。

使用longjmp函数再实现前面的实例,这种方法无需担心由于自启动系统调用而使时间限制失效的问题:

#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

static void sig_alrm(int);

static jmp_buf env_alrm;
static unsigned MAXLINE = 50;

int main() {
    int n;
    char line[MAXLINE];

    if (signal(SIGALRM, sig_alrm) == SIG_ERR) {
        printf("signal(SIGALRM) error\n");
	    exit(1);
    }

    if (setjmp(env_alrm) != 0) {
        printf("read timeout\n");
	    exit(2);    // 输入超时直接退出进程
    }

    alarm(10);

    if ((n = read(STDIN_FILENO, line, MAXLINE)) < 0) {
        printf("read error\n");
	    exit(3);
    }

    alarm(0);

    exit(0);
}

static void sig_alrm(int signo) {
    longjmp(env_alrm, 1);
}

以上代码不会因自启动系统调用而失效。但它仍有与其它信号处理程序交互的问题。另一种限制IO操作时间的选择是使用select和poll函数。

POSIX.1使用sigset_t类型包含一个信号集,以下函数可处理信号集:
在这里插入图片描述
sigemptyset函数清空参数set中所有信号。sigfillset函数向参数set中添加所有信号。所有程序在使用信号集前要先调用一次sigemptyset或sigfillset初始化该信号集。

sigaddset函数将一个信号添加到已有信号集中,sigdelset函数从信号集中删除一个信号。

总是以信号集的地址作为上述函数的参数。

以上函数可实现为宏:

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

由于以上函数在运行成功时返回0,因此sigfillset函数使用C语言的逗号运算符,它将逗号算符后的值作为表达式的值返回。

函数sigismember函数测试一个指定的位。

以上函数的一种实现:

#include <errno.h>
#include <signal.h>

// <signal.h> usually defines NSIG to include signal number 0
#define SIGBAD(signo) ((signo) <= 0 || (signo) >= NSIG)

int sigaddset(sigset_t *set, int signo) {
    if (SIGBAD(signo)) {
        errno = EINVAL;
	    return -1;
    }
    *set |= 1 << (signo - 1);    // turn bit on
    return 0;
}

int sigdelset(sigset_t *set, int signo) {
    if (SIGBAD(signo)) {
        errno = EINVAL;
	    return -1;
    }
    *set &= ~(1 << (signo - 1));    // turn bit off
    return 0;
}

int sigismember(const sigset_t *set, int signo) {
    if (SIGBAD(signo)) {
        errno = EINVAL;
	    return -1;
    }
    return ((*set & (1 << (signo - 1))) != 0);
}

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

sigprocmask函数可同时检测和更改信号的屏蔽字:
在这里插入图片描述
若参数oset是一个非空指针,那么当前信号的屏蔽字通过oset参数返回。

若参数set是一个非空指针,则参数how指示如何修改当前信号屏蔽字,参数how的可选值:
在这里插入图片描述
但不能阻塞SIGKILL和SIGSTOP。

如果参数set是空指针,则不改变进程信号屏蔽字,参数how此时无意义。

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

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

打印调用进程的信号屏蔽字中的信号名:

#include <errno.h>
#include <stdio.h>
#include <signal.h>

void pr_mask(const char *str) {
    sigset_t sigset;
    int errno_save;

    errno_save = errno;

    if (sigprocmask(0, NULL, &sigset) < 0) {
        printf("sigprocmask error\n");
	    return;
    } else {
        printf("%s", str);
		if (sigismember(&sigset, SIGINT)) 
		    printf(" SIGINT");
		if (sigismember(&sigset, SIGQUIT))
			printf(" SIGQUIT");
		// remaining signals can go here  
		printf("\n");
		
    }
    errno = errno_save;
}

sigpending函数返回在送往进程的时候被阻塞挂起的信号集合:
在这里插入图片描述
使用这些函数:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

static void sig_quit(int);

int main() {
    sigset_t newmask, oldmask, pendmask;

    if (signal(SIGQUIT, sig_quit) == SIG_ERR) {
        printf("can not catch SIGQUIT\n");
	    exit(1);
    }

    sigemptyset(&newmask);
    sigaddset(&newmask, SIGQUIT);    
    if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {    // block SIGQUIT
        printf("SIG_BLOCK error\n");
	    exit(2);
    }

    sleep(5);    // SIGQUIT here will remain pending

    if (sigpending(&pendmask) < 0) {
        printf("sigpending error\n");
	    exit(3);
    }
    if (sigismember(&pendmask, SIGQUIT)) {
        printf("\nSIGQUIT pending\n");
    }

    if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {    // restore signal mask
        printf("SIG_SETMASK error\n");
	    exit(4);
    }
    printf("SIGQUIT unblocked\n");

    sleep(5);    // SIGQUIT here will terminate with core file
    exit(0);
}

static void sig_quit(int signo) {
    printf("caught SIGQUIT\n");
    if (signal(SIGQUIT, SIG_DFL) == SIG_ERR) {
        printf("can not reset SIGQUIT\n");
	    exit(5);
    }
}

执行它:
在这里插入图片描述
上例中我们用函数sigprocmask的SIG_SETMASK标志重新设置回旧的信号屏蔽字,也可以用SIG_UNBLOCK使该信号不再被阻塞,但如果该函数调用者在调用该函数前也阻塞了该信号,那么就必须使用SIG_SETMASK,因为这时再用SIG_UNBLOCK会解除对此信号的阻塞。

被阻塞的未决的信号因调用sigprocmask而不再受阻塞,在sigprocmask调用返回前,它就被递送到了调用进程,反应在上图中就是先处理信号处理程序中的printf函数,再处理sigprocmask函数之后的printf函数。

从程序执行结果可见,在进程休眠期间向其传送了10次SIGQUIT信号,但只调用了一次信号处理程序,因此可见此系统上没有将信号进行排队。

sigaction函数替代了UNIX早期的signal函数:
在这里插入图片描述
signo参数指出要处理的信号。若参数act指针非空,则修改其动作,若参数oact指针非空,则系统经由oact指针返回该信号的上一个动作,这两个参数前的restrict是C语言中的一种类型限定符,用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。sigaction结构体定义如下:
在这里插入图片描述
更改信号动作时,如果sa_handler字段包含一个信号捕捉函数的地址(而非常量SIG_IGN或SIG_DFL),则sa_mask字段说明了一个信号集,调用该信号捕捉函数之前,这一信号集要加到进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原先值,这样可以在调用信号处理程序时就能阻塞某些信号。在信号处理程序被调用时,操作系统建立的新信号屏蔽字包括正在被递送的信号,这保证了在处理一个给定信号时,如果它再次发生,那么它会被阻塞到对前一个信号的处理结束为止。若同一种信号多次发生,通常不将它们加入队列,如果在某种信号被阻塞时,它发生了多次,那么对这种信号解除阻塞后,其信号处理函数通常只会被调用一次。

一旦对给定的信号设置了一个动作,在sigaction函数显式地改变它之前都不会变,这符合POSIX.1要求。

sigaction结构的sa_flags字段:
在这里插入图片描述
sa_sigaction字段是一个替代的信号处理程序,在sigaction结构的sa_flags字段中使用了SA_SIGINFO标志时,使用该信号处理程序。该字段和sa_handler字段的实现可能共用同一存储区,因此应用一次只能使用这两个字段其中一个。

通常这样调用信号处理程序:

void handler(int signo);

但如果设置了SA_SIGINFO标志,那么这样调用信号处理程序:

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

siginfo_t结构包含了信号产生原因的有关信息:
在这里插入图片描述
符合POSIX.1的siginfo_t实现至少包含si_signo、si_code字段,符合XSI的siginfo_t实现至少包含上图中所有字段。

上图中的sigval联合类型包含下列字段:
在这里插入图片描述
下图中定义了各种信号的si_code值(基于信号的),它们是由SUS定义的,具体实现还能定义附加的代码值:
在这里插入图片描述
若信号是SIGCHLD,则设置si_pid、si_status、si_uid字段;若信号是SIGBUS、SIGILL、SIGFPE、SIGSEGV,则si_addr字段包含造成故障的根源地址,但并不一定准确。si_errno字段包含错误编号,它对应造成信号产生的条件,由实现定义。

信号处理程序的context参数类型是无类型指针,它可被强制转换成ucontext_t结构类型,它至少包含以下字段:
在这里插入图片描述
其中的stack_t结构类型描述了当前上下文使用的栈,其中至少包括以下成员:
在这里插入图片描述
当支持信号的实时扩展时,用SA_SIGINFO建立的信号处理程序将使信号可靠地排队。应用可以通过使用sigqueue函数随同信号一起传递一些信息。

用sigaction函数实现signal函数,很多平台都这么做:

#include <signal.h>

typedef void Sigfunc(int);

Sigfunc *signal(int signo, Sigfunc *func) {
    struct sigaction act, oact;
    act.sa_handler = func;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
        act.sa_flags |= SA_INTERRUPT;    // 将信号中断的系统调用设为不自动重启
#endif
    } else {
        act.sa_flags |= SA_RESTART;
    }
    if (sigaction(signo, &act, &oact) < 0) {
        return SIG_ERR;
    }
    return oact.sa_handler;
}

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

对于SIGALRM信号,设置SA_INTERRUPT,使被中断的系统调用不再重新启动,这是由于我们希望对IO操作设置时间限制。

sigaction函数默认不重新启动被中断的系统调用,除非说明了SA_RESTART标志。

当我们在信号处理程序中用longjmp函数返回时,由于进入该信号的处理程序时会将该信号加入到屏蔽字中,那么在返回时是否会恢复信号的屏蔽字由具体实现决定。此时可用以下函数代替setjmp和longjmp函数指定对屏蔽字的操作:
在这里插入图片描述
如果sigsetjmp函数的savemask非0,则在env中保存当前屏蔽字,从而在调用siglongjmp时恢复保存的信号屏蔽字。

演示在信号处理程序被调用时,系统设置的信号屏蔽字和sigsetjmp和siglongjmp函数的使用:

#include <setjmp.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>

static void sig_usr1(int);
static void sig_alrm(int);
static void pr_mask(const char *str);

static sigjmp_buf jmpbuf;
static volatile sig_atomic_t canjmp;

int main() {
    if (signal(SIGUSR1, sig_usr1) == SIG_ERR) {
        printf("signal(SIGUSR1) error\n");
	    exit(1);
    }
    if (signal(SIGALRM, sig_alrm) == SIG_ERR) {
        printf("signal(SIGALRM) error\n");
	    exit(2);
    }

    pr_mask("starting main: ");

    if (sigsetjmp(jmpbuf, 1)) {
        pr_mask("end main: ");
        exit(0);
    }
    canjmp = 1;

    for ( ; ; ) {
        pause();
    }
}

static void sig_usr1(int signo) {
    time_t starttime;

    if (canjmp == 0) {
        return;
    }

    pr_mask("starting sig_usr1: ");

    alarm(3);
    starttime = time(NULL);    // 以秒为单位获取系统时间
    for ( ; ; ) {
        if (time(NULL) > starttime + 5) {
	        break;
	    }
    }

    pr_mask("finishing sig_usr1: ");

    canjmp = 0;
    siglongjmp(jmpbuf, 1);
}

static void sig_alrm(int signo) {
    pr_mask("in sig_alrm: ");
}

static void pr_mask(const char *str) {
    sigset_t sigset;
    int errno_save;

    errno_save = errno;

    if (sigprocmask(0, NULL, &sigset) < 0) {
        printf("sigprocmask error\n");
	    return;
    }
    
    printf("%s", str);
	if (sigismember(&sigset, SIGINT)) {
	    printf(" SIGINT");
    }
    if (sigismember(&sigset, SIGUSR1)) {
        printf(" SIGUSR1");
    } 
    if (sigismember(&sigset, SIGALRM)) {
        printf(" SIGALRM");
    } 
    // remaining signals can go here
	printf("\n");

    errno = errno_save;
}

以上程序仅在调用sigsetjmp函数之后才将变量canjmp设置为非0值,之后在信号处理函数中检测此变量,仅当它是非0值时才能调用siglongjmp,这提供了一种保护机制,使得jmp_buf被sigsetjmp函数初始化之前不会调用信号处理程序。对于没有信号处理的程序,此时的longjmp函数不必使用这种保护措施。

canjmp对象的类型为sig_atomic_t,这是ISO C标准定义的变量类型,在写这种类型变量时不会被中断,这意味着在具有虚拟存储器的系统上,这种变量不会跨越页边界,可用一条机器指令对其进行访问。这种类型变量总是会被volatile修饰,因为它将由main函数和异步执行的信号处理程序访问。

可将上述程序的执行分为三部分:
在这里插入图片描述
执行左边main函数部分时,信号屏蔽字为0;中间部分的信号屏蔽字是SIGUSR1;右边部分的信号屏蔽字是SIGALRM和SIGUSR1。

执行上图程序:
在这里插入图片描述
如果在Linux中,使用setjmp和longjmp函数,或在FreeBSD中,使用_setjmp和_longjmp函数,则最后一行的输出将变成:
在这里插入图片描述
这意味着正常退出中断处理程序时进程的屏蔽字会恢复,而longjmp函数跳回来时进程的屏蔽字不会恢复。

通过更改进程的信号屏蔽字可以阻塞某个信号集中的信号,以保护不希望由信号中断的代码临界区。如果在临界区之后解除阻塞的信号,并调用pause等待被阻塞的信号发生:

sigset_t newmask, oldmask;
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);

if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
    err_sys("SIG_BLOCK error");
}
/* 代码临界区,其中阻塞了SIGINT信号 */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {    // 此处结束代码临界区
    err_sys("SIG_SETMASK error");
}
// 时间窗口
pause();    // 等待信号发生

以上程序有一个问题,信号可能在pause函数运行前递送到进程,使得pause函数不会见到该信号。为纠正此问题,需要将解除信号阻塞和使进程休眠成为一个原子操作,以下函数提供此操作:
在这里插入图片描述
进程的信号屏蔽字被设置为sigmask参数指向的值。该函数阻塞进程,直到捕捉到一个信号并从该信号处理程序返回,之后恢复该进程的屏蔽字到调用sigsuspend函数之前,然后该函数返回。

保护代码临界区,使其不被特定信号中断:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

static void sig_int(int);
static void pr_mask(const char *);

int main() {
    sigset_t newmask, oldmask, waitmask;
    
    pr_mask("program start: ");

    if (signal(SIGINT, sig_int) == SIG_ERR) {
        printf("signal(SIGINT) error\n");
	    exit(1);
    }
    sigemptyset(&waitmask);
    sigaddset(&waitmask, SIGUSR1);
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGINT);

    if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {    // block SIGINT
        printf("SIG_BOLCK error\n");
	    exit(1);
    }

    /*
     * Critical region of code.
     */
    
    pr_mask("in critical region");

    if (sigsuspend(&waitmask) != -1) {    // 屏蔽SIGUSR1并休眠,在休眠结束时将信号屏蔽字恢复到调用sigsuspend函数前
     									  // 此时信号屏蔽字被设置为waitmask表示的信号集,SIGINT不在此集合内
        printf("sigsuspend error\n");
	    exit(1);
    }

    pr_mask("after return from sigsuspend: ");

    if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {
        printf("SIG_SETMASK error\n");
	    exit(1);
    }

    pr_mask("program exit: \n");

    exit(0);
}

static void sig_int(int signo) {
    pr_mask("\nin sig_int: ");
}

static void pr_mask(const char *str) {
    sigset_t sigset;
    int errno_save;

    errno_save = errno;

    if (sigprocmask(0, NULL, &sigset) < 0) {
        printf("sigprocmask error\n");
	    return;
    }
    
    printf("%s", str);
	if (sigismember(&sigset, SIGINT)) {
	    printf(" SIGINT");
    }
    if (sigismember(&sigset, SIGUSR1)) {
    	printf(" SIGUSR1");
    } 
    if (sigismember(&sigset, SIGALRM)) {
        printf(" SIGALRM");
    } 
    // remaining signals can go here
	printf("\n");

    errno = errno_save;
}

运行它:
在这里插入图片描述
我们可以看到,sigsuspend函数在被调用时,已经将SIGUSR1信号添加到了屏蔽字中,因此在SIGINT的信号处理进程中屏蔽字有两个(还有一个是由于调用SIGINT的信号处理程序而将SIGINT加入到屏蔽字),在调用sigsuspend之前被屏蔽的信号SIGINT也能被递送并调用它的进程处理程序,说明sigsuspend函数将信号屏蔽字设为参数表示的信号集而非将参数表示的信号集加入到信号屏蔽字。

使用sigsuspend函数的另一个例子:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

volatile sig_atomic_t quitflag;

static void sig_int(int signo) {
    if (signo == SIGINT) {
        printf("\ninterrupt\n");
    } else if (signo == SIGQUIT) {
        quitflag = 1;
    }
}

int main() {
    sigset_t newmask, oldmask, zeromask;

    if (signal(SIGINT, sig_int) == SIG_ERR) {
        printf("signal(SIGINT) error\n");
	    exit(1);
    }
    if (signal(SIGQUIT, sig_int) == SIG_ERR) {
        printf("signal(SIGQUIT) error\n");
	    exit(1);
    }

    sigemptyset(&zeromask);
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGQUIT);

    if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {    // block SIGQUIT
        printf("SIG_BLOCK error\n");
	    exit(1);
    }

    while (quitflag == 0) {
        sigsuspend(&zeromask);    // 休眠,并清空信号屏蔽字
    }
    quitflag = 0;

    if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {
        printf("SIG_SETMASK error\n");
        exit(1);
    }

    exit(0);
}

执行它:
在这里插入图片描述
可用信号实现父子进程间的同步,以下是用于父子进程通信的五个例程的实现:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

static volatile sig_atomic_t sigflag;
static sigset_t newmask, oldmask, zeromask;

static void sig_usr(int signo) {
    sigflag = 1;
}

void TELL_WAIT() {    // prepare for the flowing functions
    if (signal(SIGUSR1, sig_usr) == SIG_ERR) {
        printf("signal(SIGUSR1) error\n");
	    exit(1);
    }

    if (signal(SIGUSR2, sig_usr) == SIG_ERR) {
        printf("signal(SIGUSR2) error\n");
	    exit(1);
    }

    sigemptyset(&zeromask);
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGUSR1);
    sigaddset(&newmask, SIGUSR2);

    if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
        printf("SIG_BLOCK error\n");
		exit(1);
    }
}

void TELL_PARENT(pid_t pid) {
    kill(pid, SIGUSR2);
}

void WAIT_PARENT() {
    while (sigflag == 0) {
        sigsuspend(&zeromask);
    }
    sigflag = 0;

    if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {
        printf("SIG_SETMASK error\n");
	    exit(1);
    }
}

void TELL_CHILD(pid_t pid) {
    kill(pid, SIGUSR1);
}

void WAIT_CHILD() {
    while (sigflag == 0) {
        sigsuspend(&zeromask);
    }
    sigflag = 0;

    if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {
        printf("SIG_SETMASK error\n");
        exit(1);
    }
}

以上程序先用TELL_WAIT函数把SIGUSR1和SIGUSR2阻塞,避免还没调用WAIT_XX函数时就进入信号处理程序并将sigflag设为1,这样会使WAIT_XX函数失效。

在等待信号发生期间,调用sigsuspend使进程休眠,但如果想在等待信号发生时再调用其他函数,在单线程环境下无法解决,多线程环境下可以安排一个线程处理信号。

如果不用多线程,我们只能像下面这样尽可能好地实现等待信号发生时调用其他函数。SIGINT和SIGALRM的信号处理程序中分别将全局变量intr_flag和alrm_flag置为1。设置SIGINT和SIGALRM的信号处理程序时使这两个信号中断被阻塞的慢速系统调用:

if (intr_flag) {    // 如果发生了信号,则处理信号
    handle_intr();
}
if (alrm_flag) {
    handle_alrm();
}
// 出现在此处的信号被丢失
while (read( ... ) < 0) {
    if (errno == EINTR) {
        if (alrm_flag) { 
            handle_alrm();
        } else if (intr_flag) {
            handle_intr();
        }
    } else {
        // 其他错误处理
    }
}

如果read函数返回一个中断的系统调用错误,则再次测试是否发生了SIGINT和SIGALRM信号。问题在于前两个if和read函数中间发生信号时,信号处理程序会设置相应的全局变量,但read函数不会出错返回,无法测试是否发生了信号,该信号会丢失。

实际上,我们希望以下步骤依次发生:
1.阻塞SIGINT和SIGALRM。
2.测试两个全局变量,如信号已经发生,则处理它。
3.解除两个信号的阻塞、调用read,这两个操作应该是原子操作。只有当这一步中要调用的函数是pause时,sigsuspend函数才符合条件。

abort函数使程序异常终止:
在这里插入图片描述
函数将SIGABRT发送给调用进程。ISO C规定,调用abort将向主机环境递送一个未成功终止的通知,其方法是调用raise(SIGABRT)。

ISO C要求如果捕捉到此信号,则从该信号处理程序返回时仍不会返回到其调用者。

POSIX.1规定abort不理会进程对SIGABRT信号的阻塞和忽略。

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

ISO C规定abort函数的以下问题由实现决定:

  1. 是否冲洗输出流。
  2. 是否删除临时文件。

POSIX.1要求更进一步,调用abort终止进程时,它对所有打开标准IO流的效果与进程终止前对每个流调用fclose相同。

System V早期版本中,abort函数产生SIGIOT信号,进程可忽略它或捕捉它并从信号处理函数返回,返回时,返回到调用进程。

大多UNIX系统临时文件的实现在创建该文件之后立即调用unlink(减少inode的引用计数,为0时删除文件)。

按POSIX.1实现abort函数:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void abort() {
    sigset_t mask;
    struct sigaction action;

    sigaction(SIGABRT, NULL, &action);
    if (action.sa_handler == SIG_IGN) {
        action.sa_handler = SIG_DFL;
		sigaction(SIGABRT, &action, NULL);
    }
    if (action.sa_handler == SIG_DFL) {
        fflush(NULL);    // flush all open stdio streams
    }

    sigfillset(&mask);
    sigdelset(&mask, SIGABRT);
    sigprocmask(SIG_SETMASK, &mask, NULL);    // mask has only SIGABRT turned off
    kill(getpid(), SIGABRT);

    // if we are here, process caught SIGABRT and returned,当SIGABRT有信号处理函数时会到此处
    fflush(NULL);
    action.sa_handler = SIG_DFL;
    sigaction(SIGABRT, &action, NULL);
    sigprocmask(SIG_SETMASK, &mask, NULL);
    kill(getpid(), SIGABRT);    // try again
    exit(1);    // should never be executed
}

以上函数首先看SIGABRT是否执行默认动作,如不是且没有SIGABRT的信号处理程序,则将其修改为执行默认动作,若是,则冲洗所有标准IO流,这不等价于对打开流调用fclose,因为只冲洗而没有关闭它们,进程终止时,系统会关闭所有打开的文件,如果进程捕捉SIGABRT信号并从其信号处理程序返回,此时因为可能进程产生了更多的输出,所以再一次冲洗所有流。如果想不冲洗,直接在SIGABRT的信号处理程序中执行_exit或_Exit,此时,所有未冲洗的内存中标准IO缓存都被丢弃。

调用kill为调用者产生信号时,如果该信号是不被阻塞的,则在kill返回前该信号(或某个未决的、未阻塞的信号)被传送给该进程。我们阻塞除SIGABRT外的所有信号,这样就可知,如果kill返回了,则该进程一定已捕捉到该信号,并从该信号的处理程序返回。

POSIX.1要求system函数忽略SIGINT和SIGQUIT,阻塞SIGCHLD。

以下程序实现了不带信号处理的system函数,程序中使用的ed编辑器是捕获中断和退出信号的交互式程序,若从shell调用ed,并键入中断字符,则它捕捉中断信号并打印问号,ed程序对退出信号SIGQUIT的处理方式设置为忽略:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>

static void sig_int(int signo) {
    printf("caught SIGINT\n");
}

static void sig_chld(int signo) {
    printf("caught SIGCHLD\n");
}

int system(const char *cmdstring) {
    pid_t pid;
    int status;

    if (cmdstring == NULL) {
        return(1);
    }

    if ((pid = fork()) < 0) {
        status = -1;
    } else if (pid == 0) {
        execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
	    _exit(127);
    } else {
        while (waitpid(pid, &status, 0) < 0) {
		    if (errno != EINTR) {    // EINTR以外的错误
		        status = -1;
				break;
		    }
		}
    }

    return status;
}

int main() {
    if (signal(SIGINT, sig_int) == SIG_ERR) {
        printf("signal(SIGINT) error\n");
	    exit(1);
    }
    if (signal(SIGCHLD, sig_chld) == SIG_ERR) {
        printf("singal(SIGCHLD) error\n");
	    exit(1);
    }
    if (system("bin/ed") < 0) {
        printf("system() error\n");
	    exit(1);
    }
    exit(0);
}

执行它:
在这里插入图片描述
在这里插入图片描述
由上图,编辑器终止时,系统向父进程发送SIGCHLD信号,父进程捕捉它,执行了信号处理程序sig_chld。但POSIX.1说明,执行system函数时,应阻塞对父进程递送SIGCHLD信号,否则当system创建的子进程结束时,父进程会误以为自己的一个子进程结束了,调用者于是可能在SIGCHLD的信号处理程序中调用wait获取子进程的终止状态,这就阻止了system函数获得子进程的终止状态,并将其作为返回值。

如果运行上例程序时,传一个中断信号给编辑器:
在这里插入图片描述
输入中断字符使中断信号传送给前台进程组中的所有进程,运行以上程序时:
在这里插入图片描述
因此,信号被传给了3个前台进程,而shell进程忽略此信号。但不应将中断和退出信号同时发送给system函数产生的父子进程,只应发送给正在运行的子进程。

如果父进程捕获并处理SIGCHLD,如果父进程阻塞在调用wait时子进程终止,则父进程会先被唤醒回收子进程状态,之后再切换回用户态,此时再执行信号处理程序,验证以下过程:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <errno.h>

void sig_chld(int signo) {
    printf("caught sigchld\n");
    if (wait(NULL) <= 0) {
        if (errno == ECHILD) {
	        printf("no child\n");
	    }
    }
}

int main() {
    if (signal(SIGCHLD, sig_chld) == SIG_ERR) {
        printf("signal error\n");
    }   

    pid_t pid;
    if ((pid = fork()) < 0) {
        printf("fork errori\n");
    } else if (pid > 0) {
        if (wait(NULL) != pid) {
            if (errno == EINTR) {
                printf("eintr\n");
            }   
        }   
        printf("after wait\n");
        exit(0);
    }   
    sleep(5);
    printf("in child\n");
    exit(0);
}

运行以上程序:
在这里插入图片描述
上图显示在子进程结束后,wait函数并没有被中断,说明先执行的wait函数获取到了子进程的终止状态,之后进入了SIGCHLD的信号处理程序,在其中发现父进程已经没有僵尸子进程了,因此信号处理程序是在wait函数之后发生的,之后才输出了after wait,说明信号处理程序返回后开始执行wait函数之后的输出语句。

进行了信号处理的system函数(符合POSIX.1的实现):

#include <sys/wait.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>

int system(const char *cmdstring) {
    pid_t pid;
    int status;
    struct sigaction ignore, saveintr, savequit;
    sigset_t chldmask, savemask;

    if (cmdstring == NULL) {
        return 1;
    }

    ignore.sa_handler = SIG_IGN;    // ignore SIGINT and SIGQUIT
    sigemptyset(&ignore.sa_mask);
    ignore.sa_flags = 0;
    if (sigaction(SIGINT, &ignore, &saveintr) < 0) {
        return -1;
    }
    if (sigaction(SIGQUIT, &ignore, &savequit) < 0) {
        return -1;
    }
    sigemptyset(&chldmask);
    sigaddset(&chldmask, SIGCHLD);
    if (sigprocmask(SIG_BLOCK, &chldmask, &savemask) < 0) {
        return -1;
    }

    if ((pid = fork()) < 0) {
        status = -1;
    } else if (pid == 0) {
        sigaction(SIGINT, &saveintr, NULL);    // restore previous signal actions & reset signal mask
		sigaction(SIGQUIT, &saveintr, NULL);
	        
		execl("/bin/bash", "sh", "-c", cmdstring, (char *)0);
		_exit(127);
    } else {
        while (waitpid(pid, &status, 0) < 0) {
		    if (errno != EINTR) {
		        status = -1;    // error other than EINTR from waitpid()
				break;
		    }
		}
    }
    
    if (sigaction(SIGINT, &saveintr, NULL) < 0) {
        return -1;
    }
    if (sigaction(SIGQUIT, &savequit, NULL) < 0) {
        return -1;
    }
    if (sigprocmask(SIG_SETMASK, &savemask, NULL) < 0) {
        return -1;
    }

    return status;
}

如果使用ed的那个程序使用此system函数,则:
1.键入中断字符或退出字符时,不向调用system的进程发送。
2.ed终止时,不向调用system的进程发送SIGCHLD信号,而是阻塞SIGCHLD。POSIX.1说明,在SIGCHLD未决期间,若wait或waitpid返回了子进程的状态,则SIGCHLD信号不应递送给父进程,除非还有另一个没有被wait函数获取终止状态的子进程。FreeBSD 8.0、Mac OS X 10.6.8、Solaris 10都实现了它,但Linux 3.2.0没有实现这种语义,以下是在Linux上的测试:

#include <signal.h>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
using namespace std;

int main() {
    pid_t pid;
    sigset_t blockChld, saved, blocked;
    sigemptyset(&blockChld);
    sigaddset(&blockChld, SIGCHLD);
    sigprocmask(SIG_BLOCK, &blockChld, &saved);

    if ((pid = fork()) < 0) {
        printf("fork error\n");
		exit(1);
    } else if (pid == 0) {
        sleep(1);    // 避免竞争条件,在系统不太繁忙时保证父进程先调用waitpid
        _Exit(127);
    } else {
        if (waitpid(pid, NULL, 0) < 0) {
		    printf("waitpid error\n");
		    exit(1);
		}

        sigemptyset(&blocked);
		if (sigpending(&blocked) == -1) {
		    printf("sigpending error\n");
		    exit(1);
		}

		if (sigismember(&blocked, SIGCHLD)) {
		    printf("after wait, before unmasked, SIGCHLD is still pending\n");
		} else {
		    printf("after wait, before unmasked, SIGCHLD is no longer pending\n");
		}

    	sigprocmask(SIG_SETMASK, &saved, NULL);

		exit(0);
    }
}

以上程序输出:
在这里插入图片描述

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

if ((pid = fork()) < 0) {
    err_sys("fork error");
} else if (pid == 0) {
    execl( ... );
    _exit(127);
} 

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后不能确定父进程还是子进程先运行,可能子进程运行一段时间后,父进程将信号改为忽略前,产生该信号。解决方法是调用fork前就改变信号配置,但在调用execl前,在子进程中恢复这两个信号的处理,否则exec函数会继承旧的信号屏蔽字。

system函数的返回值为shell的终止状态,但shell的终止状态并不总是执行命令字符串的子进程的终止状态。当信号中断shell命令时:
在这里插入图片描述
如上图,终止状态130、131被认为是正常终止(pr_exit函数可验证),以上终止状态来源于Bourne shell的一个特性,即终止状态是128+信号编号。

只有当把信号送给shell时,system函数返回值才报告一个异常终止:
在这里插入图片描述
其他shell处理终端产生的信号时表现出来的行为各不相同,在bash和dash中,输入中断或退出符会导致返回带有对应信号编号的表示异常终止的退出状态。如果我们发现调用sleep并直接向其发送一个信号时,信号只发送给单个进程,而非整个前台进程组时,那么这个shell行为像Bourne shell,会返回128+信号编号作为终止状态。

如果不使用system函数而是直接使用fork、exec、wait函数,则终止状态与使用system函数不同。

sleep函数:
在这里插入图片描述
此函数使进程被挂起,直到:
1.过了参数seconds指定的墙上时钟时间,返回0。
2.调用进程捕捉到一个信号并从信号处理程序返回,返回剩余秒数。

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

sleep函数可用alarm函数实现,如果是这样实现的,那么可能两个函数间互相影响。很多系统使用nanosleep函数实现sleep函数,使其与闹钟定时器相互独立。

以下是一个POSIX.1的sleep函数实现,它可靠地处理信号,避免了早期实现中的竞争条件,但仍未处理与以前设置的闹钟的相互作用(POSIX.1未显式对这些交互进行定义):

#include <signal.h>
#include <stdio.h>

static void sig_alrm(int signo) {
    // nothing to do, just returning wakes up sigsuspend()
}

unsigned int sleep(unsigned int seconds) {
    struct sigaction newact, oldact;
    sigset_t newmask, oldmask, suspmask;
    unsigned int unslept;

    newact.sa_handler = sig_alrm;
    sigemptyset(&newact.sa_mask);
    newact.sa_flags = 0;
    sigaction(SIGALRM, &newact, &oldact);    // set SIGALRM's handler, save previous information

    sigemptyset(&newmask);
    sigaddset(&newmask, SIGALRM);
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);    // block SIGALRM and save current signal mask

    alarm(seconds);    // start the timer
    suspmask = oldmask;

    sigdelset(&suspmask, SIGALRM);    // make sure SIGALRM isn't blocked

    sigsuspend(&suspmask);    // wait for any signal to be caught

    unslept = alarm(0);

    sigaction(SIGALRM, &oldact, NULL);    // reset previous action
    
    sigprocmask(SIG_SETMASK, &oldmask, NULL);    // reset signal mask, which unblocks SIGALRM
    return unslept;
}

以上程序中没有在SIGALRM的信号处理程序中使用非局部转移以避免alarm函数和pause函数之间的竞争条件,因此在处理SIGALRM信号期间,对可能执行的其他信号处理程序没有影响(即不会因为执行SIGALRM的信号处理函数的跳回而中断其他信号处理函数的运行)。

nanosleep函数提供纳秒级精度:
在这里插入图片描述
参数reqtp用秒和纳秒指定了需要休眠的时间长度,如果某个信号中断了休眠间隔,remtp参数指向的timespec结构会被设置为未休眠完的时间长度,也可将第二个参数设为NULL,这样表示不关心剩余时间。

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

随着多个系统时钟的引入,需要使用相对于特定时钟的延迟时间来挂起调用线程,以下函数提供此功能:
在这里插入图片描述
参数clock_id指定了计算延迟时间基于的时钟。参数flags控制延迟是相对的(0,希望睡眠的时间长度)还是绝对的(TIMER_ABSTIME,希望睡眠到某个特定时间)。参数reqtp和remtp与nanosleep函数中的相同,但使用绝对时间时,参数remtp未使用。时钟到达指定的绝对时间值之前,其他的clock_nanosleep调用可以复用reqtp参数的值。

除了出错返回,调用以下两函数效果相同:

clock_nanosleep(CLOCK_REALTIME, 0, reqtp, remtp);
nanosleep(reqtp, remtp);

相对休眠的问题是有些应用对休眠长度有精度要求,相对休眠时间会导致实际休眠时间比要求的长。如某应用按固定时间间隔执行任务,就必须获取当前时间,计算下次执行任务的时间,然后调用nanosleep,在获取当前时间和调用nanosleep之间,处理器调度可能会导致相对休眠时间超过实际需要的时间间隔。

大部分UNIX系统不对信号排队,在POSIX.1实时扩展中,有些系统开始增加对信号排队的支持。

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

使用信号排队必须要:
1.使用sigaction函数安装信号处理程序时指定SA_SIGINFO标志,否则,信号是否进入队列取决于具体实现。
2.在sigaction结构的sa_sigaction成员中提供信号处理程序。实现可能允许用户使用sa_handler字段,但这样不能获取sigqueue函数发送的额外信息。
3.使用sigqueue函数发送信号:
在这里插入图片描述
它把信号发送给单个进程,可用value参数向信号处理程序传递整数或指针值。

信号不能无限排队,SIGQUEUE_MAX限制最大值,到达最大值后,调用sigqueue会失败,并将errno设为EAGAIN。

随着实时信号增强,引入了用于应用的独立信号集,这些信号编号位于SIGTMIN~SIGTMAX之间,包含两端。它们的默认行为是终止进程。

在这里插入图片描述
POSIX.1认为以下信号与作业控制有关:
在这里插入图片描述
交互式shell中输入挂起字符(Ctrl+Z)时,SIGTSTP被送到前台进程组的所有进程;通知shell在前台或后台恢复运行一个作业时,shell向该作业中所有进程发送SIGCONT信号;如果向一个进程递送了SIGTTIN或SIGTTOU信号,则默认会停止此进程。除了SIGCHLD外,一般应用并不处理这些信号,但一个例外是vi程序,用户挂起它时,它需要了解这一点,并把终端状态恢复到vi启动时的情况;前台恢复它时,它需要将终端状态设置回它所希望的状态,并需要重新绘制屏幕。

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

如果进程是停止的,则SIGCONT默认继续该进程(即使该信号是被阻塞或忽略的也是如此),否则忽略此信号。

处理SIGTSTP:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUFFSIZE 1024

static void sig_tstp(int signo) {
    sigset_t mask;

    // move cursor to lower left corner, reset tty mode

    sigemptyset(&mask);
    sigaddset(&mask, SIGTSTP);
    sigprocmask(SIG_UNBLOCK, &mask, NULL);    // unblock SIGTSTP, since it's blocked while we're handling it

    signal(SIGTSTP, SIG_DFL);    // reset disposition to default

    kill(getpid(), SIGTSTP);    // send SIGTSTP to ourself

    // wo won't return from kill until we're continued

    signal(SIGTSTP, sig_tstp);    // reestablish signal handler

    // reset tty mode, redraw screen
}

int main() {
    int n;
    char buf[BUFFSIZE];

    if (signal(SIGTSTP, SIG_IGN) == SIG_DFL) {    
        signal(SIGTSTP, sig_tstp);
    }

    while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0) {
	    if (write(STDOUT_FILENO, buf, n) != n) {
		    printf("write error\n");
		    exit(1);
		}
    }

    if (n < 0) {
        printf("read error\n");
		exit(1);
    }

    exit(0);
}

以上程序中,仅当SIGTSTP信号的默认配置是SIG_DFL时,才捕捉该信号,理由是,当此程序由不支持作业控制的shell启动时,信号的默认设置为SIG_IGN。shell并不显式地忽略此信号,而是由init进程将这三个信号(SIGTSTP、SIGTTIN、SIGTTOU)设为SIG_IGN,然后这种配置由所有登录shell继承。

某些系统(不可移植)提供信号编号和信号名之间的映射:

extern char *sys_siglist[];    // 下标是信号编号,值是指向信号名字符串的指针

可用psignal函数可移植地打印与信号编号对应的信号名:
在这里插入图片描述
程序执行结果为:字符串msg(常是程序名)输出到标准错误文件 ,后随冒号和空格,再后面是对信号的说明,最后是换行符。如果msg为NULL,只有信号说明部分输出到标准错误文件。

如果用sigaction函数设置信号处理程序时使用到了siginfo_t结构,可以使用psiginfo函数打印信号信息:
在这里插入图片描述
它输出除信号编号外的更多信息。工作方式与psignal函数类似。不同平台输出的额外信息有所不同。

返回信号的字符描述的函数:
在这里插入图片描述
Solaris提供信号编号和信号名映射的函数:
在这里插入图片描述
第一个函数调用者要保证参数str指向的存储区足够大。Solaris在signal.h中定义了SIG2STR_MAX,定义了最大的字符串长度。参数str指向的信号名字符串文本不带SIG前缀。

第二个函数的参数str是不带SIG前缀的信号名。

使用WAIT_XXX和TELL_XXX使父子进程交互向文件中输入计数值:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

static volatile sig_atomic_t sigflag;
static sigset_t newmask, oldmask, zeromask;

static void sig_usr(int signo) {
    sigflag = 1;
}

void TELL_WAIT() {    // prepare for the flowing functions
    if (signal(SIGUSR1, sig_usr) == SIG_ERR) {
        printf("signal(SIGUSR1) error\n");
		exit(1);
    }
    
    if (signal(SIGUSR2, sig_usr) == SIG_ERR) {
        printf("signal(SIGUSR2) error\n");
		exit(1);
    }

    sigemptyset(&zeromask);
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGUSR1);
    sigaddset(&newmask, SIGUSR2);

    if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
        printf("SIG_BLOCK error\n");
		exit(1);
    }
}

void TELL_PARENT(pid_t pid) {
    kill(pid, SIGUSR2);
}

void WAIT_PARENT() {
    while (sigflag == 0) {
        sigsuspend(&zeromask);
    }
    sigflag = 0;
}

void TELL_CHILD(pid_t pid) {
    kill(pid, SIGUSR1);
}

void WAIT_CHILD() {
    while (sigflag == 0) {
        sigsuspend(&zeromask);
    }
    sigflag = 0;
}

int main() {
    pid_t pid;
    int fd;
    
    const char *path = "test.file";
    if ((fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 777)) < 0) {
        printf("file open fail\n");
		exit(1);
    }

    TELL_WAIT();
    
    if ((pid = fork()) < 0) {
        printf("fork error\n");
		exit(1);
    } else if (pid == 0) {
        int i = 0;
		char buf[100];
		for (; i < 10; i += 2) {
		    WAIT_PARENT();
		    sprintf(buf, "from child: %d\n", i);
	        write(fd, buf, strlen(buf) + 1);
		    TELL_PARENT(getppid());
		}
    } else {
        int i = 1;
		char buf[100];
		TELL_CHILD(pid);
        for (; i < 10; i += 2) {
		    WAIT_CHILD();
		    sprintf(buf, "from parent: %d\n", i);
		    write(fd, buf, strlen(buf) + 1);
		    TELL_CHILD(pid);
	    }
    }
}

执行结果:
在这里插入图片描述

siginfo_t结构的si_uid包含的是实际用户id而非有效用户id,原因是其他进程向自己发送信号时,发信号的进程必须有root权限或是接收该信号的进程的所有者,实际用户id提供了更多信息。

一段使用fwrite函数的程序,使用一个较大缓冲区,调用fwrite前调用alarm使得1s后产生信号,在信号处理程序中打印捕捉到的信号,然后返回,fwrite函数是否完成取决于标准IO库的实现,即fwrite函数如何处理被中断的write调用,如在Linux 3.2.0上,fwrite函数直接以相同的字节数调用write,在write系统调用中,闹钟时间到,但直到写结束我们才看到信号,看上去好像write系统调用时内核阻塞了信号。而在Solaris 10中fwrite函数以8kb增量调用write,直到写完要求的字节数,闹钟时间到时SIGALRM信号会被捕捉到,中断多个write函数的连续调用回到fwrite函数,当从信号处理程序返回时,返回到fwrite函数内部循环,并继续以8kb写。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值