Linux进程间通信之信号
信号的基本概念
信号是UNIX和Linux系统响应某些条件而产生的一个事件。 接收到该信号的进程会相应地采取一些行动。我们用术语生成(raise) 表示一个信号的产生,使用术语捕获(catch) 表示接收到一个信号。
信号是由于某些错误条件而生成的,如内存段冲突、浮点处理器错误或非法指令等。它们由shelI和终端处理器生成来引起中断,它们还可以作为在进程间传递消息或修改行为的一种方式, 明确地由一个进程发送给另一个进程。无论何种情况,它们的编程接口都是相同的。信号可以被生成、捕获、响应或(至少对于一些信号)忽略。
信号的名称是在头文件signal.h中定义的。它们以SIG开头,见下表1。
信号名称 | 说明 |
---|---|
SIGABORT | *进程异常终止 |
SIGALRM | 超时警告 |
SIGFPE | *浮点运算异常 |
SIGHUP | 连接挂断 |
SIGILL | *非法指令 |
SIGINT | 终端中断 |
SIGKILL | 终止进程(此信号不能被捕获或忽略) |
SIGPIPE | 向无读进程的管道写数据 |
SIGQUIT | 终端退出 |
SIGSEGV | *无效内存段访问 |
SIGTERM | 终止 |
SIGUSR1 | 用户定义信号1 |
SIGUSR2 | 用户定义信号2 |
系统对带*的信号的响应视具体实现而定。
**如果进程接收到这些信号中的一一个, 但事先没有安排捕获它,进程将会立刻终止。**通常,系统将生成核心转储文件core,并将其放在当前目录下。该文件是进程在内存中的映像,它对程序的调试很有用处。
其他信号见下表2:
信号名称 | 说明 |
---|---|
SIGCHLD | 子进程已经停止或退出 |
SIGCONT | 继续执行暂停进程 |
SIGSTOP | 停止执行(此信号不能被捕获或忽略) |
SIGTSTP | 终端挂起 |
SIGTTIN | 后台进程尝试读操作 |
SIGTTOU | 后台进程尝试写操作 |
SIGCHLD信号对于管理子进程很有用。默认情况下,它是被忽略的。其余的信号会使接收它们的进程停止运行,但SICCONT是个例外,它的作用是让进程恢复并继续执行。shell脚本通过它来控制作业,但用户程序很少会用到它。
如果shell和终端驱动程序是按通常情况配置的话,在键盘上敲入中断字符(通常是Ctrl+C组合键)就会向前台进程(即当前正在运行的程序)发送SIGINT信号,这将引起该程序的终止,除非它事先安排了捕获这个信号。
如果想发送一个信号给进程,而该进程并不是当前的前台进程,就需要使用kill命令。该命令需要有一个可选的信号代码或信号名称和一个接收信号的目标进程的PID (这个PID一般需要用ps命令查出来)。例如,如果要向运行在另一个终端上的PID为512的进程发送“挂断”信号,可以使用如下命令:
kill -HUP 512
kill命令有一个有用的变体叫killall,它可以给运行着某一命令的所有进程发送信号。并不是所有的UNIX系统都支持它,但Linux系统一般都有该命令。 如果不知道某个进程的PID,或者想给执行相同命令的许多不同的进程发送信号,这条命令就很有用了。一种常见的用法是,通知inetd程序重新读取它的配置选项,要完成这一工作, 可以使用下面这条命令:
killall -HUP inetd
信号处理函数signal
程序可以用signal库函数来处理信号,它的定义如下所示:
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
这个相当复杂的函数定义说明,signal 是一个带有sig和func两个参数的函数。准备捕获或忽略的信号由参数sig给出,接收到指定的信号后将要调用的函数由参数func给出。信号处理函数必须有一个int 类型的参数(即接收到的信号代码)并且返回类型为void。 signal函数本身也返回一个同类型的函数,即先前用来处理这个信号的函数,或者也可以用表3中的两个特殊值之一来代替信号处理函数。
表3:
SIG_IGN | 忽略信号 |
---|---|
SIG_DFL | 恢复默认行为 |
函数使用示例:
该程序将响应用户敲入的Ctrl+C组合键,在屏幕上打印一条适当的消息而不是终止程序的运行。当用户第二次按下Ctrl+C时,程序将结束运行。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void ouch(int sig)
{
printf("OUCH! - I got signal %d\n", sig);
(void) signal(SIGINT, SIG_DFL);
}
int main()
{
(void) signal(SIGINT, ouch);
while(1){
printf("Hello World!\n");
sleep(1);
}
}
第一次按下Ctrl+C组合键会让程序作出响应,然后程序继续执行。再次按下Ctrl+C组合键时,程序将结束运行,因为SIGINT信号的处理方式已恢复为默认行为——终止程序的运行。
在此例中我们可以看到,信号处理函数使用了一个单独的整数参数,它就是引起该函数被调用的信号代码。如果需要在同一个函数中处理多个信号,这个参数就很有用。在本例中,我们打印出SIGINT的值,它的值在这个系统中恰好是2,但你不能过分依赖传统的信号数字值,而应该在新的程序中总是使用信号的名字。
在信号处理函数中,调用如printf这样的函数是不安全的。一个有用的技巧是,在信号处理函数中设置一个标志,然后在主程序中检查该标志,如需要就打印一条消息。后面会给出一个函数列表,表中的函数都可以在信号处理函数中被安全地调用。
程序中安排函数ouch来处理在按下Ctrl+C组合键时所产生的SIGINT信号。程序会在中断函数ouch处理完毕后继续执行,但信号处理方式已恢复为默认行为(不同版本的UNIX系统,特别是从BerkleyUNIX衍生出来的那些版本,在对信号的处理方式上从历史上就有些细微的不同。如果想让信号的处理方式在信号发生后恢复到其默认行为,最好的方法就是自己写出具体的信号处理代码)。当它接收到第二个SIGINT信号后,程序将采取默认的行动,即终止程序的运行。
如果想保留信号处理函数,让它继续响应用户的Ctrl+C组合键,我们就需要再次调用signal函数来重新建立它。这会使信号在一段时间内无法得到处理,这段时间从调用中断函数开始,到信号处理函数的重建为止。如果在这段时间内程序接收到第二个信号,它就会违背我们的意愿终止程序的运行,所以不推荐使用signal接口。
发送信号函数kill和alarm
进程可以通过调用kill函数向包括它本身在内的其他进程发送一个信号。如果程序没有发送该信号的权限,对kill函数的调用就将失败,失败的常见原因是目标进程由另一个用户所拥有。这个函数和同名的shell命令完成相同的功能,它的定义如下所示:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
kill函数把参数sig给定的信号发送给由参数pid给出的进程号所指定的进程,成功时它返回0。要想发送一个信号,发送进程必须拥有相应的权限。这通常意味着两个进程必须拥有相同的用户ID(即你只能发送信号给属于自己的进程,但超级用户可以发送信号给任何进程)。
kill调用会在失败时返回-1并设置errno变量。失败的原因可能是:给定的信号无效(errno设置为EINVAL);发送进程权限不够(errno设置为EPERM);目标进程不存在(errno设置为ESRCH)。
信号向我们提供了一个有用的闹钟功能。进程可以通过调用alarm函数在经过预定时间后发送一个SIGALRM信号。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
alarm函数用来在seconds秒之后安排发送一个SIGALRM信号。 但由于处理的延时和时间调度的不确定性,实际闹钟时间将比预先安排的要稍微拖后一点儿。把参数seconds设置为0将取消所有已设置的闹钟请求。如果在接收到SIGALRM信号之前再次调用alarm函数,则闹钟重新开始计时。每个进程只能有一个闹钟时间。alarm函数的返回值是以前设置的闹钟时间的余留秒数,如果调用失败则返回-1。
为了说明alarm函数的工作情况,我们通过使用fork、sleep和signal来模拟它的效果。程序可以启动一个新的进程,它专门用于在未来的某一时刻发送一个信号。
程序示例:模拟一个闹钟
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
static int alarm_fired = 0;
void ding(int sig)
{
alarm_fired = 1;
}
int main()
{
pid_t pid;
printf("alarm application starting\n");
pid = fork();
switch(pid){
case -1:
perror("fork failed");
exit(1);
case 0:
sleep(5);
kill(getppid(), SIGALRM);
exit(0);
}
printf("waiting for alarm to go off\n");
(void) signal(SIGALRM, ding);
pause();
if(alarm_fired)
printf("Ding!\n");
printf("done\n");
exit(0);
}
运行这个程序时,它会暂停5秒,等待模拟闹钟的闹响。
这个程序用到了一个新的函数pause,它的作用很简单,就是把程序的执行挂起直到有一个信号出现为止。当程序接收到一个信号时,预设好的信号处理函数将开始运行,程序也将恢复正常的执行。pause函数的定义如下:
#include <unistd.h>
int pause(void);
当它被一个信号中断时,将返回-1 (如果下一个接收到的信号没有导致程序终止的话)并把errno设置为EINTR。当需要等待信号时,一个更常见的方法是使用稍后将要介绍的sigsuspend函数。
闹钟模拟程序通过fork调用启动新的进程。这个子进程休眠5秒后向其父进程发送一个SIGALRM信号。父进程在安排好捕获SIGALRM信号后暂停运行,直到接收到一个信号为止。我们并未在信号处理函数中直接调用printf,而是通过在该函数中设置标志,然后在main函数中检查该标志来完成消息的输出。
使用信号并挂起程序的执行是Linux程序设计中的一个重要部分。这意味着程序不需要总是在执行着。程序不必在一个循环中无休止地检查某个事件是否已发生,相反,它可以等待事件的发生。这在只有一个CPU的多用户环境中尤其重要,进程共享着一个处理器, 繁忙的等待将会对系统的性能造成极大的影响。
程序中信号的使用将带来一个特殊的问题:“如果信号出现在系统调用的执行过程中会发生什么情况?”答案是相当让人不满意的“视情况而定”。一般来说,你只需要考虑慢系统调用,例如从终端读数据,如果在这个系统调用等待数据时出现一个信号,它就会返回一个错误。如果你开始在自己的程序中使用信号,就需要注意一些系统调用会因为接收到了一个信号而失败,而这种错误情况可能是你在添加信号处理函数之前没有考虑到的。
在编写程序中处理信号部分的代码时必须非常小心,因为在使用信号的程序中会出现各种各样的“竞态条件”。例如,如果想调用pause等待一一个信号, 可信号却出现在调用pause之前,就会使程序无限期地等待一个不会发生的事件。
更健壮信号处理函数sigaction
函数sigaction是一个比函数signal更新更健壮的编程接口,它的定义如下所示:
#include <signal.h>
int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);
sigaction结构定义在文件signal.h中,它的作用是定义在接收到参数sig指定的信号后应该采取的行动。该结构至少包括以下几个成员:
struct sigaction{
void (*) (int) sa_handler; //function, SIG_DFL or SIG_IGN
sigset_t sa_mask; //signals to block in sa_handler
int sa_flags; //signal action modifiers
}
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。sa_flags字段可用包含下表中的取值,它们用于改变信号的行为。
SA_NOCLDSTOP | 子进程停止时不产生SIGCHLD信号 |
---|---|
SA_RESETHAND | 将对此信号的处理方式在信号处理函数的入口处重置为SIG_DFL |
SA_RESTART | 重启可中断的函数而不是给出EINTR错误 |
SA_NODEFER | 捕获到信号时不将它添加到信号屏蔽字中 |
在深入了解sigaction函数之前,我们先用sigaction替换signal来重写程序ctrlc.c。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void ouch(int sig)
{
printf("OUCH! - I got signal %d\n", sig);
}
int main()
{
struct sigaction act;
act.sa_handler = ouch;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, 0);
(void) signal(SIGINT, ouch);
while(1){
printf("Hello World!\n");
sleep(1);
}
}
运行这个新版程序时,只要按下Ctrl+C组合键,就可以看到一条消息。因为sigaction函数连续处理到来的SIGIN信号。要想终止这个程序,我们只能按下Ctrl+(组合键,它在默认情况下产生SIGQUIT信号。
信号集
头文件signal.h定义了类型sigset_t和用来处理信号集的函数。sigaction和其他函数将用这些信号集来修改进程在接收到信号时的行为。
对信号集的初始化和增删
#include <signal.h>
int sigaddset(sigset_t *set, int signo);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigdelset(sigsiet_t *set, int signo);
这些函数执行的操作如它们的名字所示。sigemptyset将信号集初始化为空。sigfillset将信号集初始化为包含所有已定义的信号。sigaddset 和sigdelset从信号集中增加或删除给定的信号(signo)。它们在成功时返回0,失败时返回-1并设置errno。只有一个错误代码被定义,即当给定的信号无效时,errno将设置为EINVAL。
在信号集中查找信号
#include <signal.h>
int sigismember(sigset_t *set, int signo);
函数sigismember判断一个给定的信号是否是一个信号集的成员。如果是就返回1;如果不是,它就返回0;如果给定的信号无效,它就返回-1并设置errno为EINVAL。
函数sigprocmask
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
进程的信号屏蔽字的设置或检查工作由函数sigprocmask来完成。信号屏蔽字是指当前被阻塞的一组信号,它们不能被当前进程接收到。
sigprocmask函数可以根据参数how指定的方法修改进程的信号屏蔽字。新的信号屏蔽字由参数set (如果它不为空)指定,而原先的信号屏蔽字将保存到信号集oset中。参数how的取值可以是下表中的一个:
SIG_BLOCK | 把参数set中的信号添加到信号屏蔽字中 |
---|---|
SIG_SETMASK | 把信号屏蔽字设置为参数set中的信号 |
SIG_UNBLOCK | 从信号屏蔽字当中删除参数set中的信号 |
如果参数set是空指针,how的值就没有意义了,此时这个调用的唯一目的就是把当前信号屏蔽字的值保存到oset中。
如果sigprocmask成功完成,它将返回0;如果参数how取值无效,它将返回-1并设置errno为EINVAL。
如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态。程序可以通过调用函数sigpending来查看它阻塞的信号中有哪些正停留在待处理状态。
函数sigpending
#include <signal.h>
int sigpending(sigset_t *set);
这个函数的作用是,将被阻塞的信号中停留在待处理状态的一组信号写到参数set指向的信号集中。成功时它将返回0,否则返回-1并设置errno以表明错误的原因。如果程序需要处理信号,同时又需要控制信号处理函数的调用时间,这个函数就很有用了。
进程可以通过调用sigsuspend函数挂起自己的执行,直到信号集中的一个信号到达为止。这是我们前面见到的pause函数更通用的一种表现形式。
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
sigsuspend函数将进程的屏蔽字替换为由参数sigmask给出的信号集,然后挂起程序的执行。程序将在信号处理函数执行完毕后继续执行。如果接收到的信号终止了程序,sigsuspend就不会返回;如果接收到的信号没有终止程序,sigsuspend就返回-1并将errno设置为EINTR。
常用信号量参考
信号名称 | 说明 |
---|---|
SIGALRM | 由alarm函数设置的定时器产生 |
SIGHUP | 有一个处于非链接状态的终端发送给控制进程,或者由控制进程在自身结束时发送给每个前台进程 |
SIGINT | 一般由从终端敲入的Ctrl+C组合键或预先设置好的中断字符产生 |
SIGKILL | 因为这个信号不能被捕获或忽略,所以一般在shell中用它来强制终止异常进程 |
SIGPIPE | 如果在向管道写数据时没有与之对应的读进程,就会产生这个信号 |
SIGTERM | 作为一个请求被发送,要求进程结束运行。UNIX在关机时用这个信号要求系统服务停止运行。它是kill命令的默认发送信号 |
SIGUSR1,SIGUSR2 | 进程之间可以用这个信号进行通信,例如让进程报告状态信息等 |
默认情况下,表11 - 10中的信号也会引起进程的异常终止。但可能还会有一些与具体实现相关的其它动作,比如创建core文件等
默认情况下,进程接收到表11 - 11中的信号时将会被挂起。
SIGCONT信号的作用是重启被暂停的进程,如果进程没有暂停,则忽略该信号。SIGCHLD信号在默认情况下被忽略。