信号
索引:
- 信号的概念
- signal函数
- 程序启动、fork和exec时信号的处理
- 低速系统调用和信号
- 不可重入函数和信号
- SIGCLD语义
- 信号产生、递送和未决
- kill和raise函数
- alarm函数
- pause函数
- 信号集
- 信号屏蔽字和sigprocmask函数
- sigpending函数
- sigaction函数
- sigsetjmp和siglongjmp
- sigsuspend函数
- abort函数
- sleep函数
1.信号的概念
每个信号都有一个名字。这些名字都以三个字符SIG开头。在头文件signal.h中,这些信号都被定义为正整数(信号编号)。没有一个信号其编号为0。SVR4和4.3+BSD均有31种不同的信号。
很多条件可以产生一个信号:
- 当用户按某些终端键时,产生信号;
- 硬件异常产生信号:除数为0、无效的存储访问等等。这些条件通常由硬件检测到,并将其通知内核,然后内核为该条件发生时正在运行的进程产生适当的信号;
- 进程用kill(2)函数可将信号发送给另一个进程或进程组;
- 用户可用kill(1)命令将信号发送给其他进程;
- 当检测到某种软件条件已经发生,并将其通知有关进程时也产生信号。
可以要求系统在某个信号出现时按照下列三种方式中的一种进行操作:
- 忽略此信号;
- 捕捉信号;
- 执行系统默认动作。
有的信号会产生core文件,但在下列条件下不产生core文件:
- 进程是设置-用户-ID,而且当前用户并非程序文件的所有者;
- 进程是设置-组-ID,而且当前用户并非该程序文件的组所有者;
- 用户没有写当前工作目录的许可权;
- 文件太大(RLIMIT_CORE)。
core文件的许可权(假定该文件在此之前并不存在)通常是用户读/写、组读和其他读。
每个信号的详细说明:
信号名称 | 解释 |
---|---|
SIGABRT | 调用abort函数时产生此信号。进程异常终止。 |
SIGALRM | 超过用alarm函数设置的时间时产生此信号。若由setitimer(2)函数设置的间隔时间已经过时,那么也产生此信号。 |
SIGBUS | 指示一个实现定义的硬件故障。 |
SIGCHLD | 在一个进程终止或停止时,SIGCHLD信号被送给其父进程。按系统默认,将忽略此信号。如果父进程希望了解其子进程的这种状态改变,则应捕捉此信号。信号捕捉函数中通常要调用wait函数以取得子进程ID和其终止状态。系统V的早期版本有一个名为SIGCLD(无H)的类似信号。这一信号具有非标准的语义,SVR2的手册页警告在新的程序中尽量不要使用这种信号。应用程序应当使用标准的SIGCHLD信号。 |
SIGCONT | 此作业控制信号送给需要继续运行的处于停止状态的进程。如果接收到此信号的进程处于停止状态,则系统默认动作是使该进程继续运行,否则默认动作是忽略此信号。 |
SIGEMT | 指示一个实现定义的硬件故障。 |
SIGFPE | 信号表示一个算术运算异常,例如除以0,浮点溢出等。 |
SIGHUP | 如果终端界面检测到一个连接断开,则将此信号送给与该终端相关的控制进程(对话期首进程)。仅当终端的CLOCAL标志没有设置时,在上述条件下才产生此信号。(如果所连接的终端是本地的,才设置该终端的CLOCAL标志。它告诉终端驱动程序忽略所有调制解调器的状态行。)注意,接到此信号的对话期首进程可能在后台,这区别于通常由终端产生的信号(中断、退出和挂起),这些信号总是传递给前台进程组。如果对话期首进程终止,则也产生此信号。在这种情况,此信号送给前台进程组中的每一个进程。 |
SIGILL | 此信号指示进程已执行一条非法硬件指令。 |
SIGINFO | 这是一种4.3+BSD信号,当用户按状态键(一般采用Ctrl-T)时,终端驱动程序产生此信号并送至前台进程组中的每一个进程。此信号通常造成在终端上显示前台进程组中各进程的状态信息。 |
SIGINT | 当用户按中断键(一般采用DELETE或Ctrl-C)时,终端驱动程序产生此信号并送至前台进程组中的每一个进程。 |
SIGIO | 此信号指示一个异步I/O事件。 |
SIGIOT | 这指示一个实现定义的硬件故障。 |
SIGKILL | 这是两个不能被捕捉或忽略信号中的一个。它向系统管理员提供了一种可以杀死任一进程的可靠方法。 |
SIGPIPE | 如果在读进程已终止时写管道,则产生此信号。当套接口的一端已经终止时,若进程写该套接口也产生此信号。 |
SIGPOLL | 这是一种SVR4信号,当在一个可轮询设备上发生一特定事件时产生此信号。它与4.3+BSD的SIGIO和SIGURG信号接近。 |
SIGPROF | 当setitimer(2)函数设置的梗概统计间隔时间已经超过时产生此信号。 |
SIGPWR | 这是一种SVR4信号,它依赖于系统。它主要用于具有不间断电源(UPS)的系统上。如果电源失效,则UPS起作用,而且通常软件会接到通知。在这种情况下,系统依靠蓄电池电源继续运行,所以无须作任何处理。但是如果蓄电池也将不能支持工作,则软件通常会再次接到通知,此时,它在15~30秒内使系统各部分都停止运行。此时应当传递SIGPWR信号。在大多数系统中使接到蓄电池电压过低的进程将信号SIGPWR发送给init进程,然后由init处理停机操作。很多系统V的init实现在inittab文件中提供了两个记录项用于此种目的;powerfail以及powerwait。目前已能获得低价格的UPS系统,它用RS-232串行连接能够很容易地将蓄电池电压过低的条件通知系统,于是这种信号也就更加重要了。 |
SIGQUIT | 当用户在终端上按退出键(一般采用Ctrl-\)时,产生此信号,并送至前台进程组中的所有进程。此信号不仅终止前台进程组,同时产生一个core文件。 |
SIGSEGV | 指示进程进行了一次无效的存储访问。 |
SIGSTOP | 这是一个作业控制信号,它停止一个进程。它类似于交互停止信号(SIGTSTP),但是SIGSTOP不能被捕捉或忽略。 |
SIGSYS | 指示一个无效的系统调用。由于某种未知原因,进程执行了一条系统调用指令,但其指示系统调用类型的参数却是无效的。 |
SIGTERM | 这是由kill(1)命令发送的系统默认终止信号。 |
SIGTRAP | 指示一个实现定义的硬件故障。 |
SIGTSTP | 交互停止信号,当用户在终端上按挂起键(一般采用Ctrl-Z)时,终端驱动程序产生此信号。 |
SIGTTIN | 当一个后台进程组进程试图读其控制终端时,终端驱动程序产生此信号。在下列例外情形下不产生此信号,此时读操作返回出错,errno设置为EIO:(a)读进程忽略或阻塞此信号,或(b)读进程所属的进程组是孤儿进程组。 |
SIGTTOU | 当一个后台进程组进程试图写其控制终端时产生此信号。与SIGTTIN信号不同,一个进程可以选择为允许后台进程写控制终端。如果不允许后台进程写,则与SIGTTIN相似也有两种特殊情况:(a)写进程忽略或阻塞此信号,或(b)写进程所属进程组是孤儿进程组。在这两种情况下不产生此信号,写操作返回出错,errno设置为EIO。不论是否允许后台进程写,某些除写以外的下列终端操作也能产生此信号:tcsetattr,tcsendbreak, tcdrain, tcflush, tcflow 以及tcsetpgrp。 |
SIGURG | 此信号通知进程已经发生一个紧急情况。在网络连接上,接到非规定波特率的数据时,此信号可选择地产生。 |
SIGUSR1 | 这是一个用户定义的信号,可用于应用程序。 |
SIGUSR2 | 这是一个用户定义的信号,可用于应用程序。 |
SIGVTALRM | 当一个由setitimer(2)函数设置的虚拟间隔时间已经超过时产生此信号。 |
SIGWINCH | SVR4和4.3+BSD内核保持与每个终端或伪终端相关联的窗口的大小。一个进程可以用ioctl函数得到或设置窗口的大小。如果一个进程用ioctl的设置-窗口-大小命令更改了窗口大小,则内核将SIGWINCH信号送至前台进程组。 |
SIGXCPU | SVR4和4.3+BSD支持资源限制的概念。如果进程超过了其软CPU时间限制,则产生此信号。 |
SIGXFSZ | 如果进程超过了其软文件长度限制,则SVR4和4.3+BSD产生此信号。 |
2.signal函数
UNIX信号机制最简单的界面是signal函数:
#include <signal.h> void (*signal (int signo, void (*func)(int))) (int); 返回:成功则为以前的信号处理配置,若出错则为SIG_ERR
signo参数是信号名。
func的值是:
- 常数SIG_IGN,表示忽略此信号(信号SIGKILL和SIGSTOP不能忽略);
- 常数SIG_DFL,表示接到此信号后的动作是系统默认动作;
- 当接到此信号后要调用的函数的地址。我们称此为捕捉此信号,函数为信号处理程序(signal handler)或信号捕捉函数(signal-catching function)。
#define SIG_ERR (void (*)())-1 #define SIG_DFL (void (*)())0 #define SIG_IGN (void (*)())1
SVR4的signal函数提供老的SVR2不可靠信号语义,提供此函数主要是为了向下兼容要求此老语义的应用程序,新应用程序不应使用它。
4.3+BSD的signal函数,但是它是用sigaction函数实现的,所以在4.3+BSD之下使用它提供新的可靠的信号语义。
3.程序启动、fork和exec时信号的处理
当执行一个程序时,所有信号的状态都是系统默认或忽略。
通常所有信号都被设置为系统默认动作,除非调用exec的进程忽略该信号。比较特殊的是,exec函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不变。
当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程存储映像,所以信号捕捉函数的地址在子进程中是有意义的。
4.低速系统调用和信号
低速系统调用是可能会使进程永远阻塞的一类系统调用,它们包括:
- 在读某些类型的文件时,如果数据并不存在则可能会使调用者永远阻塞(管道、终端设备以及网络设备)。
- 在写这些类型的文件时,如果不能立即接受这些数据,则也可能会使调用者永远阻塞。
- 打开文件,在某种条件发生之前也可能会使调用者阻塞(例如,打开终端设备,它要等待直到所连接的调制解调器回答了电话)。
- pause(按照定义,它使调用进程睡眠直至捕捉到一个信号)和wait。
- 某种ioctl操作。
- 某些进程间通信函数。
早期UNIX系统的一个特性是:如果在进程执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno设置为EINTR。
为了帮助应用程序使其不必处理被中断的系统调用,4.2BSD引进了某些被中断的系统调用的自动再起动。自动再起动的系统调用包括:ioctl、read、readv、write、writev、wait和waitpid。
5.不可重入函数和信号
不可重入函数是指:
- 已知它们使用静态数据结构;
- 它们调用malloc或free,因为malloc/free是不可重入的;
- 它们是标准I/O函数。标准I/O库的很多实现都以不可再入方式使用全局数据结构。
在信号处理程序中调用一个不可重入函数,则其结果是不可预见的。
在信号处理程序中即使调用可重入函数也有问题要注意。作为一个通用的规则,当在信号处理程序中调用可再入函数时,应当在其前保存,在其后恢复errno。(要了解经常被捕捉到的信号是SIGCHLD,其信号处理程序通常要调用一种wait函数,而各种wait函数都能改变errno。)
6.SIGCLD语义
SIGCLD和SIGCHLD这两个信号经常易于混淆。SIGCLD是系统V的一个信号名,其语义与名为SIGCHLD的BSD信号不同。由于历史原因,系统V处理SIGCLD信号的方式不同于其他信号。
如果用signal或sigset(设置信号配置的早期的与SRV3兼容性函数)设置信号配置,则SVR4继续了这一具有问题色彩的传统(即兼容性限制)。对于SIGCLD早期的处理方式是:
- 如果进程特地指定对该信号的配置为SIG_IGN,则调用进程的子进程将不产生僵尸进程。注意,这与其默认动作(SIG_DFL)忽略不同。代之以,在子进程终止时,将其状态丢弃。如果调用进程最后调用一个wait函数,那么它将阻塞到所有子进程都终止,然后该wait会返回-1,其errno则设置为ECHILD。
- 如果将SIGCLD的配置设置为捕捉,则内核立即检查是否有子进程准备好被等待,如果是这样,则调用SIGCLD处理程序。此项改变了为此信号编写处理程序的方法。
7.信号产生、递送和未决
当造成信号的事件发生时,为进程产生一个信号(或向一个进程发送一个信号),叫信号产生。
在产生了信号时,内核通常在进程表中设置某种形式的一个标志。当对信号做了这种动作时,我们说向一个进程递送了一个信号。
在信号产生(generation)和递送(delivery)之间的时间间隔内,称信号未决(pending)。
进程可以选用“信号递送阻塞”。如果为进程产生了一个选择为阻塞的信号,而且对该信号的动作是系统默认动作或捕捉该信号,则为该进程将此信号保持为未决状态,直到该进程(a)对此信号解除了阻塞,或者(b)将对此信号的动作更改为忽略。
递送一个原来被阻塞的信号给进程时,而不是在产生该信号时,内核才决定对它的处理方式。所以进程在信号递送给它之前仍可改变对它的动作。
如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,POSIX.1允许系统递送该信号一次或多次。如果递送该信号多次,则称这些信号排了队。但是大多数UNIX并不对信号排队。代之以,UNIX内核只递送这种信号一次。
8.kill和raise函数
kill函数将信号发送给进程或进程组。raise函数则允许进程向自身发送信号:
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int signo); int raise(int signo); 两个函数返回:若成功则为0,若出错则为-1
kill的pid参数有四种不同的情况:
- pid > 0:将信号发送给进程ID为pid的进程;
- pid == 0:将信号发送给其进程组ID等于发送进程的进程组ID,而且发送进程有许可权向其发送信号的所有进程(不包括系统进程集);
- pid < 0:将信号发送给其进程组ID等于pid绝对值,而且发送进程有许可权向其发送信号的所有进程;
- pid == -1:POSIX.1未定义此种情况。
发送许可权定义如下:
超级用户可将信号发送给另一个进程。对于非超级用户,其基本规则是发送者的实际或有效用户ID必须等于接收者的实际或有效用户ID。如果实现支持_POSIX_SAVED_IDS,则用保存的设置-用户-ID代替有效用户ID。在对许可权进行测试时也有一个特例:如果被发送的信号是SIGCONT,则进程可将它发送给属于同一对话期的任一其他进程。
POSIX.1将信号编号0定义为空信号。如果signo参数是0,则kill仍执行正常的错误检查,但不发送信号。这常被用来确定一个特定进程是否仍旧存在。如果向一个并不存在的进程发送空信号,则kill返回-1,errno则被设置为ESRCH。
如果调用kill为调用进程产生信号,而且此信号是不被阻塞的,那么在kill返回之前,signo或者某个其他未决的、非阻塞信号被传送至该进程。
9.alarm函数
#include <unistd.h> unsigned int alarm(unsigned int seconds); 返回:0或以前设置的闹钟时间的余留秒数
其中,参数seconds的值是秒数。使用alarm函数可以设置一个时间值(闹钟时间),在将来的某个时刻该时间值会被超过,当所设置的时间值被超过后,产生SIGALRM信号。如果不忽略或不捕捉此信号,则其默认动作是终止该进程。
每个进程只能有一个闹钟时间。如果在调用alarm时,以前已为该进程设置过闹钟时间,而且它还没有超时,则该闹钟时间的余留值作为本次alarm函数调用的值返回。以前登记的闹钟时间则被新值代换。
如果有以前登记的尚未超过的闹钟时间,而且seconds值是0,则取消以前的闹钟时间,其余留值仍作为函数的返回值。
10.pause函数
#include <unistd.h> int pause(void); 返回:-1,errno设置为EINTR
pause函数使调用进程挂起直至捕捉到一个信号。
只有执行了一个信号处理程序并从其返回时,pause才返回。在这种情况下,pause返回-1,errno设置为EINTR。
11.信号集
信号集(signal set)的数据类型是sigset_t。
#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); 四个函数返回:若成功则为0,若出错则为-1 int sigismember(const sigset_t *set, int signo); 返回:若真则为1,若假则为0
函数sigemptyset初始化由set指向的信号集,使排除其中所有信号。
函数sigfillset初始化由set指向的信号集,使其包括所有信号。
所有应用程序在使用信号集前,应该要对该信号集调用sigemptyset或sigfillset一次。
函数sigaddset将一个信号添加到现存集中,sigdelset则从信号集中删除一个信号。
12.信号屏蔽字和sigprocmask函数
一个进程的信号屏蔽字规定了当前阻塞而不能递送给该进程的信号集。调用函数sigprocmask可以检测或更改(或两者)进程的信号屏蔽字:
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 返回:若成功则为0,若出错则为-1
oset是非空指针,进程的当前信号屏蔽字通过oset返回。若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字:
- SIG_BLOCK:该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集,set包含了我们希望阻塞的附加信号。
- SIG_UNBLOCK:该进程新的信号屏蔽字是其当前信号屏蔽字和set所指向信号集的差集,set包含了我们希望解除阻塞的信号。
- SIG_SETMASK:该进程新的信号屏蔽是set指向的值。
如果set是个空指针,则不改变该进程的信号屏蔽字,how的值也无意义。
如果在调用sigprocmask后有任何未决的、不再阻塞的信号,则在sigprocmask返回前,至少将其中之一递送给该进程。
在设置新的阻塞信号时,我们保存老的屏蔽字。为了解除对该信号的阻塞,用老的屏蔽字重新设置进程信号屏蔽字(SIG_SETMASK)。另一种方法是用SIG_UNBLOCK使阻塞的信号不再阻塞。但是,应当了解如果编写一个可能由其他人使用的函数,而且需要在函数中阻塞一个信号,则不能用SIG_UNBLOCK解除对此信号的阻塞,这是因为此函数的调用者在调用本函数之前可能也阻塞了此信号。在这种情况下必须使用SIG_SETMASK将信号屏蔽字恢复为原先值。
13.sigpending函数
sigpending返回对于调用进程被阻塞不能递送和当前未决的信号集。该信号集通过set参数返回。
#include <signal.h> int sigpending(sigset_t *set); 返回:若成功则为0,若出错则为-1
14.sigaction函数
sigaction函数的功能是检查或修改(或两者)与指定信号相关联的处理动作。此函数取代了UNIX早期版本使用的signal函数:
#include <signal.h> int sigaction(int signo, const struct sigaction *act, \ struct sigaction *oact); 返回:若成功则为0,若出错则为-1
参数signo是要检测或修改具体动作的信号的编号数。若act指针非空,则要修改其动作。如果oact指针非空,则系统返回该信号的原先动作。此函数使用下列结构:
struct sigaction { void (*sa_handler)(); /*addr of handler;SIG_IGN;SIG_DFL*/ sigset_t sa_mask; /* additional signals to block */ int sa_flags; /* signal options */ };
当更改信号动作时,如果sa_handler指向一个信号捕捉函数(不是常数SIG_IGN或SIG_DFL),则sa_mask字段说明了一个信号集,在调用信号捕捉函数之前,该信号集要加到进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原先值。这样,在调用信号处理程序时就能阻塞某些信号。在信号处理程序被调用时,系统建立的新信号屏蔽字会自动包括正被递送的信号。因此保证了在处理一个给定的信号时,如果这种信号再次发生,那么它会被阻塞到对前一个信号的处理结束为止。
一旦对给定的信号设置了一个动作,那么在用sigaction改变它之前,该设置就一直有效。这与早期的不可靠信号机制不同,而符合了POSIX.1在这方面的要求。
act结构的sa_flags字段包含了对信号进行处理的各个选择项:
- SA_NOCLDSTOP:若signo是SIGCHLD,当一子进程停止时(作业控制),不产生此信号。当一子进程终止时,仍旧产生此信号;
- SA_RESTART:由此信号中断的系统调用自动再起动;
- SA_ONSTACK:若用sigaltstack(2)已说明了一替换栈,则此信号递送给替换栈上的进程;
- SA_NOCLDWAIT:若signo是SIGCHLD,则当调用进程的子进程终止时,不创建僵死进程。若调用进程在后面调用wait,则阻塞到它所有子进程都终止,此时返回-1,errno设置为ECHILD;
- SA_NODEFER:当捕捉到此信号时,在执行其信号捕捉函数时,系统不自动阻塞此信号。注意,此种类型的操作对应于早期的不可靠信号;
- SA_RESETHAND:对此信号的处理方式在此信号捕捉函数的入口处复置为SIG_DFL。注意,此种类型的信号对应于早期的不可靠信号;
- SA_SIGINFO:此选项对信号处理程序提供了附加信息。
某些系统(如SunOS)定义了SA_INTERRUPT标志。这些系统的默认方式是重新起动被中断的系统调用,而指定此标志则使系统调用被中断后不再重起动。
15.sigsetjmp和siglongjmp
#include <setjmp.h> int sigsetjmp(sigjmp_buf env, int savemask); 返回:若直接调用则为0,若从siglongjmp调用返回则为非0 void siglongjmp(sigjmp_buf env, int val);
这两个函数和setjmp、longjmp之间的唯一区别是sigsetjmp增加了一个参数。如果savemask非0,则sigsetjmp在env中保存进程的当前信号屏蔽字。调用siglongjmp时,如果带非0的savemask的sigsetjmp调用已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字。
在信号处理程序中作非局部转移时应当使用这两个函数。
只要在信号处理程序中调用siglongjmp就应使用一种技术:在调用sigsetjmp之后设一个标志,在信号处理程序中检测此标志,仅当已经设置时才调用siglongjmp。目的是提供一种保护机制,使得若在jmpbuf(跳转缓存)尚未由sigsetjmp初始化时调用信号处理程序,则不执行其处理动作就返回。在一般的C代码中(不是信号处理程序),对于longjmp并不需要这种保护措施。但是,因为信号可能在任何时候发生,所以在信号处理程序中,需要这种保护措施。
16.sigsuspend函数
#include <signal.h> int sigsuspend(const sigset_t *sigmask); 返回:-1, errno设置为EINTR
进程的信号屏蔽字设置为由sigmask指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且该进程的信号屏蔽字设置为调用sigsuspend之前的值。
注意,此函数没有成功返回值。如果它返回到调用者,则总是返回-1,并且errno设置为EINTR(表示一个被中断的系统调用)。
17.abort函数
abort函数的功能是使程序异常终止:
#include <stdlib.h> void abort(void); 此函数不返回
此函数将SIGABRT信号发送给调用进程。进程不应忽略此信号。ANSI C要求若捕捉到此信号而且相应信号处理程序返回,abort仍不会返回到其调用者。POSIX.1也说明abort覆盖了进程对此信号的阻塞和忽略。
让进程捕捉SIGABRT的意图是:在进程终止之前由其执行所需的清除操作。如果进程并不在信号处理程序中终止自己,POSIX.1说明当信号处理程序返回时,abort终止该进程。
18.sleep函数
#include <unistd.h> unsigned int sleep(unsigned int seconds); 返回:0或未睡的秒数
此函数使调用进程被挂起直到:(1)已经过了seconds所指定的墙上时钟时间,或者(2)该进程捕捉到一个信号并从信号处理程序返回。
在第(1)种情形,返回值是0。当由于捕捉到某个信号sleep提早返回时(第(2)种情形),返回值是未睡足的秒数(所要求的时间减实际睡眠时间)。