11.4 信号

信号是UNIX和Linux系统响应某些条件而产生的一个事件。接收到该信号的进程会相应地采取一些行动。我们用术语生成(raise)表示一个信号的产生,使用术语捕获(catch)表示接收到一个信号。信号是由于某些错误条件而生成的,如内存段冲突、浮点处理器错误或非法指令等。它们由shell和终端处理器生成来引起中断,它们还可以作为在进程间传递消息或修改行为的一种方式,明确地由一个进程发送给另一个进程。无论何种情况,它们的编程接口都是相同的。信号可以被生成、捕获、响应或(至少对于一些信号)忽略。

信号的名称是在头文件signal.h中定义的。它们以SIG开头,见表11-3。

表 11-3

11.4 信号 - 图1

*系统对信号的响应视具体实现而定。

如果进程接收到这些信号中的一个,但事先没有安排捕获它,进程将会立刻终止。通常,系统将生成核心转储文件core,并将其放在当前目录下。该文件是进程在内存中的映像,它对程序的调试很有用处。

其他信号见表11-4。

表 11-4

11.4 信号 - 图2

SIGCHLD信号对于管理子进程很有用。默认情况下,它是被忽略的。其余的信号会使接收它们的进程停止运行,但SIGCONT是个例外,它的作用是让进程恢复并继续执行。shell脚本通过它来控制作业,但用户程序很少会用到它。

稍后我们将对表11-3中的信号做进一步的介绍。现在,我们只需知道如果shell和终端驱动程序是按通常情况配置的话,在键盘上敲入中断字符(通常是Ctrl+C组合键)就会向前台进程(即当前正在运行的程序)发送SIGINT信号,这将引起该程序的终止,除非它事先安排了捕获这个信号。

如果想发送一个信号给进程,而该进程并不是当前的前台进程,就需要使用kill命令。该命令需要有一个可选的信号代码或信号名称和一个接收信号的目标进程的PID(这个PID一般需要用ps命令查出来)。例如,如果要向运行在另一个终端上的PID为512的进程发送“挂断”信号,可以使用如下命令:

11.4 信号 - 图3

kill命令有一个有用的变体叫killall,它可以给运行着某一命令的所有进程发送信号。并不是所有的UNIX系统都支持它,但Linux系统一般都有该命令。如果不知道某个进程的PID,或者想给执行相同命令的许多不同的进程发送信号,这条命令就很有用了。一种常见的用法是,通知inetd程序重新读取它的配置选项,要完成这一工作,可以使用下面这条命令:

11.4 信号 - 图4

程序可以用signal库函数来处理信号,它的定义如下所示:

11.4 信号 - 图5

这个相当复杂的函数定义说明,signal是一个带有sig和func两个参数的函数。准备捕获或忽略的信号由参数sig给出,接收到指定的信号后将要调用的函数由参数func给出。信号处理函数必须有一个int类型的参数(即接收到的信号代码)并且返回类型为void。signal函数本身也返回一个同类型的函数,即先前用来处理这个信号的函数,或者也可以用表11-5中的两个特殊值之一来代替信号处理函数。

表 11-5

11.4 信号 - 图6

通过一个实例可以更清楚地理解信号的处理方法。下面我们来编写一个程序ctrlc.c,它将响应用户敲入的Ctrl+C组合键,在屏幕上打印一条适当的消息而不是终止程序的运行。当用户第二次按下Ctrl+C时,程序将结束运行。

实 验 信号处理

函数ouch对通过参数sig传递进来的信号作出响应。信号出现时,程序调用该函数,它先打印一条消息,然后将信号SIGINT(默认情况下,按下Ctrl+C将产生这个信号)的处理方式恢复为默认行为。

11.4 信号 - 图7

main函数的作用是,截获按下Ctrl+C组合键时产生的SIGINT信号。没有信号出现时,它会在一个无限循环中每隔一秒打印一条消息。

11.4 信号 - 图8

第一次按下Ctrl+C组合键会让程序作出响应,然后程序继续执行。再次按下Ctrl+C组合键时,程序将结束运行,因为SIGINT信号的处理方式已恢复为默认行为——终止程序的运行。

11.4 信号 - 图9

在此例中我们可以看到,信号处理函数使用了一个单独的整数参数,它就是引起该函数被调用的信号代码。如果需要在同一个函数中处理多个信号,这个参数就很有用。在本例中,我们打印出SIGINT的值,它的值在这个系统中恰好是2,但你不能过分依赖传统的信号数字值,而应该在新的程序中总是使用信号的名字。

在信号处理函数中,调用如printf这样的函数是不安全的。一个有用的技巧是,在信号处理函数中设置一个标志,然后在主程序中检查该标志,如需要就打印一条消息。在本章的结尾部分,你将会看到一个函数列表,表中的函数都可以在信号处理函数中被安全地调用。

实验解析

程序中安排函数ouch来处理在按下Ctrl+C组合键时所产生的SIGINT信号。程序会在中断函数ouch处理完毕后继续执行,但信号处理方式已恢复为默认行为(不同版本的UNIX系统,特别是从Berkley UNIX衍生出来的那些版本,在对信号的处理方式上从历史上就有些细微的不同。如果想让信号的处理方式在信号发生后恢复到其默认行为,最好的方法就是自己写出具体的信号处理代码)。当它接收到第二个SIGINT信号后,程序将采取默认的行动,即终止程序的运行。

如果想保留信号处理函数,让它继续响应用户的Ctrl+C组合键,我们就需要再次调用signal函数来重新建立它。这会使信号在一段时间内无法得到处理,这段时间从调用中断函数开始,到信号处理函数的重建为止。如果在这段时间内程序接收到第二个信号,它就会违背我们的意愿终止程序的运行。

我们不推荐大家使用signal接口。之所以会在这里介绍它,是因为你可能会在许多老程序中看到它的应用。稍后我们会介绍一个定义更清晰、执行更可靠的函数sigaction,在所有的新程序中都应该使用这个函数。

signal函数返回的是先前对指定信号进行处理的信号处理函数的函数指针,如果未定义信号处理函数,则返回SIG_ERR并设置errno为一个正数值。如果给出的是一个无效的信号,或者尝试处理的信号是不可捕获或不可忽略的信号(如SIGKILL),errno将被设置为EINVAL。

11.4.1 发送信号

进程可以通过调用kill函数向包括它本身在内的其他进程发送一个信号。如果程序没有发送该信号的权限,对kill函数的调用就将失败,失败的常见原因是目标进程由另一个用户所拥有。这个函数和同名的shell命令完成相同的功能,它的定义如下所示:

11.4 信号 - 图10

kill函数把参数sig给定的信号发送给由参数pid给出的进程号所指定的进程,成功时它返回0。要想发送一个信号,发送进程必须拥有相应的权限。这通常意味着两个进程必须拥有相同的用户ID(即你只能发送信号给属于自己的进程,但超级用户可以发送信号给任何进程)。

kill调用会在失败时返回-1并设置errno变量。失败的原因可能是:给定的信号无效(errno设置为EINVAL);发送进程权限不够(errno设置为EPERM);目标进程不存在(errno设置为ESRCH)。

信号向我们提供了一个有用的闹钟功能。进程可以通过调用alarm函数在经过预定时间后发送一个SIGALRM信号。

11.4 信号 - 图11

alarm函数用来在seconds秒之后安排发送一个SIGALRM信号。但由于处理的延时和时间调度的不确定性,实际闹钟时间将比预先安排的要稍微拖后一点儿。把参数seconds设置为0将取消所有已设置的闹钟请求。如果在接收到SIGALRM信号之前再次调用alarm函数,则闹钟重新开始计时。每个进程只能有一个闹钟时间。alarm函数的返回值是以前设置的闹钟时间的余留秒数,如果调用失败则返回-1。

为了说明alarm函数的工作情况,我们通过使用fork、sleep和signal来模拟它的效果。程序可以启动一个新的进程,它专门用于在未来的某一时刻发送一个信号。

实 验 模拟一个闹钟

alarm.c程序里的第一个函数ding的作用是模拟一个闹钟。

11.4 信号 - 图12

在main函数中,我们告诉子进程在等待5秒后发送一个SIGALRM信号给它的父进程。

11.4 信号 - 图13

父进程通过一个signal调用安排好捕获SIGALRM信号的工作,然后等待它的到来。

11.4 信号 - 图14

运行这个程序时,它会暂停5秒,等待模拟闹钟的闹响。

11.4 信号 - 图15

这个程序用到了一个新的函数pause,它的作用很简单,就是把程序的执行挂起直到有一个信号出现为止。当程序接收到一个信号时,预设好的信号处理函数将开始运行,程序也将恢复正常的执行。pause函数的定义如下所示:

11.4 信号 - 图16

当它被一个信号中断时,将返回-1(如果下一个接收到的信号没有导致程序终止的话)并把errno设置为EINTR。当需要等待信号时,一个更常见的方法是使用稍后将要介绍的sigsuspend函数。

实验解析

闹钟模拟程序通过fork调用启动新的进程。这个子进程休眠5秒后向其父进程发送一个SIGALRM信号。父进程在安排好捕获SIGALRM信号后暂停运行,直到接收到一个信号为止。我们并未在信号处理函数中直接调用printf,而是通过在该函数中设置标志,然后在main函数中检查该标志来完成消息的输出。

使用信号并挂起程序的执行是Linux程序设计中的一个重要部分。这意味着程序不需要总是在执行着。程序不必在一个循环中无休止地检查某个事件是否已发生,相反,它可以等待事件的发生。这在只有一个CPU的多用户环境中尤其重要,进程共享着一个处理器,繁忙的等待将会对系统的性能造成极大的影响。程序中信号的使用将带来一个特殊的问题:“如果信号出现在系统调用的执行过程中会发生什么情况?”答案是相当让人不满意的“视情况而定”。一般来说,你只需要考虑慢系统调用,例如从终端读数据,如果在这个系统调用等待数据时出现一个信号,它就会返回一个错误。如果你开始在自己的程序中使用信号,就需要注意一些系统调用会因为接收到了一个信号而失败,而这种错误情况可能是你在添加信号处理函数之前没有考虑到的。

在编写程序中处理信号部分的代码时必须非常小心,因为在使用信号的程序中会出现各种各样的“竞态条件”。例如,如果想调用pause等待一个信号,可信号却出现在调用pause之前,就会使程序无限期地等待一个不会发生的事件。这些竞态条件都是一些对时间要求很苛刻的问题,许多编程新手都有这方面的烦恼,所以在检查和信号相关的代码时总是要非常小心。

一个健壮的信号接口

我们已经对用signal和其相关函数来生成和捕获信号做了比较深入的介绍,因为它们在传统的UNIX编程中很常见。但X/Open和UNIX规范推荐了一个更新和更健壮的信号编程接口:sigaction。它的定义如下所示:

11.4 信号 - 图17

sigaction结构定义在文件signal.h中,它的作用是定义在接收到参数sig指定的信号后应该采取的行动。该结构至少应该包括以下几个成员:

11.4 信号 - 图18

sigaction函数设置与信号sig关联的动作。如果oact不是空指针,sigaction将把原先对该信号的动作写到它指向的位置。如果act是空指针,则sigaction函数就不需要再做其他设置了,否则将在该参数中设置对指定信号的动作。

与signal函数一样,sigaction函数会在成功时返回0,失败时返回-1。如果给出的信号无效或者试图对一个不允许被捕获或忽略的信号进行捕获或忽略,错误变量errno将被设置为EINVAL。

在参数act指向的sigaction结构中,sa_handler是一个函数指针,它指向接收到信号sig时将被调用的信号处理函数。它相当于前面见到的传递给函数signal的参数func。我们可以将sa_handler字段设置为特殊值SIG_IGN和SIG_DFL,它们分别表示信号将被忽略或把对该信号的处理方式恢复为默认动作。

sa_mask成员指定了一个信号集,在调用sa_handler所指向的信号处理函数之前,该信号集将被加入到进程的信号屏蔽字中。这是一组将被阻塞且不会传递给该进程的信号。设置信号屏蔽字可以防止前面看到的信号在它的处理函数还未运行结束时就被接收到的情况。使用sa_mask字段可以消除这一竞态条件。

但是,由sigaction函数设置的信号处理函数在默认情况下是不被重置的,如果希望获得类似前面用第二次signal调用对信号处理进行重置的效果,就必须在sa_flags成员中包含值SA_RESETHAND。在深入了解sigaction函数之前,我们先用sigaction替换signal来重写程序ctrlc.c。

实 验 sigaction函数

按照下面给出的代码修改我们的程序,用sigaction来截获SIGINT信号。我们将新的程序命名为ctrlc2.c。

11.4 信号 - 图19

运行这个新版程序时,只要按下Ctrl+C组合键,就可以看到一条消息。因为sigaction函数连续处理到来的SIGINT信号。要想终止这个程序,我们只能按下Ctrl+\组合键,它在默认情况下产生SIGQUIT信号。

11.4 信号 - 图20

11.4 信号 - 图21

实验解析

这个程序用sigaction代替signal来设置Ctrl+C组合键(SIGINT信号)的信号处理函数为ouch。它首先必须设置一个sigaction结构,在该结构中包含信号处理函数、信号屏蔽字和标志。在本例中,我们不需要设置任何标志,并通过调用新的函数sigemptyset来创建空的信号屏蔽字。

运行完这个程序后,你将发现在当前目录下多了一个core文件,你可以安全地删除它。

11.4.2 信号集

头文件signal.h定义了类型sigset_t和用来处理信号集的函数。sigaction和其他函数将用这些信号集来修改进程在接收到信号时的行为。

11.4 信号 - 图22

这些函数执行的操作如它们的名字所示。sigemptyset将信号集初始化为空。sigfillset将信号集初始化为包含所有已定义的信号。sigaddset和sigdelset从信号集中增加或删除给定的信号(signo)。它们在成功时返回0,失败时返回-1并设置errno。只有一个错误代码被定义,即当给定的信号无效时,errno将设置为EINVAL。

函数sigismember判断一个给定的信号是否是一个信号集的成员。如果是就返回1;如果不是,它就返回0;如果给定的信号无效,它就返回-1并设置errno为EINVAL。

11.4 信号 - 图23

进程的信号屏蔽字的设置或检查工作由函数sigprocmask来完成。信号屏蔽字是指当前被阻塞的一组信号,它们不能被当前进程接收到。

11.4 信号 - 图24

sigprocmask函数可以根据参数how指定的方法修改进程的信号屏蔽字。新的信号屏蔽字由参数set(如果它不为空)指定,而原先的信号屏蔽字将保存到信号集oset中。

参数how的取值可以是表11-6中的一个。

表 11-6

11.4 信号 - 图25

如果参数set是空指针,how的值就没有意义了,此时这个调用的唯一目的就是把当前信号屏蔽字的值保存到oset中。

如果sigprocmask成功完成,它将返回0;如果参数how取值无效,它将返回-1并设置errno为EINVAL。

如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态。程序可以通过调用函数sigpending来查看它阻塞的信号中有哪些正停留在待处理状态。

11.4 信号 - 图26

这个函数的作用是,将被阻塞的信号中停留在待处理状态的一组信号写到参数set指向的信号集中。成功时它将返回0,否则返回-1并设置errno以表明错误的原因。如果程序需要处理信号,同时又需要控制信号处理函数的调用时间,这个函数就很有用了。

进程可以通过调用sigsuspend函数挂起自己的执行,直到信号集中的一个信号到达为止。这是我们前面见到的pause函数更通用的一种表现形式。

11.4 信号 - 图27

sigsuspend函数将进程的屏蔽字替换为由参数sigmask给出的信号集,然后挂起程序的执行。程序将在信号处理函数执行完毕后继续执行。如果接收到的信号终止了程序,sigsuspend就不会返回;如果接收到的信号没有终止程序,sigsuspend就返回-1并将errno设置为EINTR。

1.sigaction标志

用在sigaction函数里的sigaction结构中的sa_flags字段可以包含表11-7中的取值,它们用于改变信号的行为。

表 11-7

11.4 信号 - 图28

①这里指的是进城暂停,当子进程终止时,仍旧会产生SIGCHLD信号。——译者注

当一个信号被捕获时,SA_RESETHAND标志可以用来自动清除它的信号处理函数,就如同我们在前面所看到的那样。

程序中使用的许多系统调用都是可中断的。也就是说,当接收到一个信号时,它们将返回一个错误并将errno设置为EINTR,表明函数是因为一个信号而返回的。使用了信号的应用程序需要特别注意这一行为。如果sigaction调用中的sa_flags字段设置了SA_RESTART标志,那么在信号处理函数执行完之后,函数将被重启而不是被信号中断。

一般的做法是,信号处理函数正在执行时,新接收到的信号将在该处理函数的执行期间被添加到进程的信号屏蔽字中。这防止了同一信号的不断出现引起信号处理函数的再次运行。如果信号处理函数是一个不可重入的函数,在它结束对第一个信号的处理之前又让另一个信号再次调用它就有可能引起问题。但如果设置了SA_NODEFER标志,当程序接收到这个信号时就不会改变信号屏蔽字。

信号处理函数可以在其执行期间被中断并再次被调用。当返回到第一次调用时,它能否继续正确操作是很关键的。这不仅仅是递归(调用自身)的问题,而是可重入(可以安全地进入和再次执行)的问题。Linux内核中,在同一时间负责处理多个设备的中断服务例程就需要是可重入的,因为优先级更高的中断可能会在同一段代码的执行期间“插入”进来。

表11-8中列出的是可以在信号处理函数中安全调用的函数。X/Open规范保证它们都是可重入的或者本身不会再生成信号的。

所有未列在表11-8中的函数,在涉及信号处理时,都被认为是不安全的。

表 11-8

11.4 信号 - 图29

2.常用信号参考

在这一小节,我们列出Linux和UNIX程序常用的信号及其默认行为。

表11-9中信号的默认动作都是异常终止进程,进程将以_exit调用方式退出(它类似exit,但在返回到内核之前不作任何清理工作)。但进程的结束状态会传递到wait和waitpid函数中去,从而表明进程是因某个特定的信号而异常终止的。

表 11-9

11.4 信号 - 图30

默认情况下,表11-10中的信号也会引起进程的异常终止。但可能还会有一些与具体实现相关的其他动作,比如创建core文件等。

表 11-10

11.4 信号 - 图31

默认情况下,进程接收到列在表11-11中的信号时将会被挂起。

表 11-11

11.4 信号 - 图32

SIGCONT信号的作用是重启被暂停的进程,如果进程没有暂停,则忽略该信号。SIGCHLD信号在默认情况下被忽略。

表 11-12

11.4 信号 - 图33