Chapter 10 信号
1.信号概念
信号是一种软件中断,通知程序某种事件的发生。常见的信号有SIGABRT(当进程调用abort函数的时候自动发送), SIGALRM(当timer被触发的时候自动发送),等等。
下面的情况可以产生信号:
- 按下CTRL+C产生SIGINT
- 硬件中断,如除0,非法内存访问(SIGSEV)等等
- Kill函数可以对进程发送信号
- Kill命令。实际上是对Kill函数的一个包装
- 软件中断。如当Alarm Clock超时(SIGURG),当Reader中止之后又向管道写数据(SIGPIPE),等等
当信号发生的时候,可以有三种选择(称之为Disposition of the signal或者Action associated with a signal)
1).忽略信号。大部分信号都可以Ignore,除了SIGKILL和SIGSTOP,这是为了提供一个确定的方法来Stop或者Kill一个Process。此外,如果我们忽略部分硬件异常产生的信号,进程的行为未定义。
2).捕捉信号。可以让内核来调用我们所指定的函数。SIGKILL和SIGSTOP无法捕捉。
3).执行缺省行为。如果不做任何处理,则执行缺省动作。大部分信号的缺省行为都是中止进程。
部分信号的缺省行为不仅中止进程,同时还会产生core dump,也就是生成一个名为core的文件,其中保存了退出时进程内存的镜像,可以用来调试。在下面情况,不会生成core文件:
a.当前进程不属于当前用户
b.当前进程不属于当前组
c.用户在当前目录下无写权限
d.Core文件已存在,用户无写权限
e.文件过大,超过RLIMIT_CORE
下面列信号说明
Signal |
Description |
SIGABRT |
由调用abort函数产生,进程非正常退出 |
SIGALRM |
用alarm函数设置的timer超时或setitimer函数设置的interval timer超时 |
SIGBUS |
某种特定的硬件异常,通常由内存访问引起 |
SIGCANCEL |
由Solaris Thread Library内部使用,通常不会使用 |
SIGCHLD |
进程Terminate或Stop的时候,SIGCHLD会发送给它的父进程。缺省情况下该Signal会被忽略 |
SIGCONT |
当被stop的进程恢复运行的时候,自动发送 |
SIGEMT |
和实现相关的硬件异常 |
SIGFPE |
数学相关的异常,如被0除,浮点溢出,等等 |
SIGFREEZE |
Solaris专用,Hiberate或者Suspended时候发送 |
SIGHUP |
发送给具有Terminal的Controlling Process,当terminal被disconnect时候发送 |
SIGILL |
非法指令异常 |
SIGINFO |
BSD signal。由Status Key产生,通常是CTRL+T。发送给所有Foreground Group的进程 |
SIGINT |
由Interrupt Key产生,通常是CTRL+C或者DELETE。发送给所有ForeGround Group的进程 |
SIGIO |
异步IO事件 |
SIGIOT |
实现相关的硬件异常,一般对应SIGABRT |
SIGKILL |
无法处理和忽略。中止某个进程 |
SIGLWP |
由Solaris Thread Libray内部使用 |
SIGPIPE |
在reader中止之后写Pipe的时候发送 |
SIGPOLL |
当某个事件发送给Pollable Device的时候发送 |
SIGPROF |
Setitimer指定的Profiling Interval Timer所产生 |
SIGPWR |
和系统相关。和UPS相关。 |
SIGQUIT |
输入Quit Key的时候(CTRL+/)发送给所有Foreground Group的进程 |
SIGSEGV |
非法内存访问 |
SIGSTKFLT |
Linux专用,数学协处理器的栈异常 |
SIGSTOP |
中止进程。无法处理和忽略。 |
SIGSYS |
非法系统调用 |
SIGTERM |
请求中止进程,kill命令缺省发送 |
SIGTHAW |
Solaris专用,从Suspend恢复时候发送 |
SIGTRAP |
实现相关的硬件异常。一般是调试异常 |
SIGTSTP |
Suspend Key,一般是Ctrl+Z。发送给所有Foreground Group的进程 |
SIGTTIN |
当Background Group的进程尝试读取Terminal的时候发送 |
SIGTTOU |
当Background Group的进程尝试写Terminal的时候发送 |
SIGURG |
当out-of-band data接收的时候可能发送 |
SIGUSR1 |
用户自定义signal 1 |
SIGUSR2 |
用户自定义signal 2 |
SIGVTALRM |
setitimer函数设置的Virtual Interval Timer超时的时候 |
SIGWAITING |
Solaris Thread Library内部实现专用 |
SIGWINCH |
当Terminal的窗口大小改变的时候,发送给Foreground Group的所有进程 |
SIGXCPU |
当CPU时间限制超时的时候 |
SIGXFSZ |
进程超过文件大小限制 |
SIGXRES |
Solaris专用,进程超过资源限制的时候发送 |
2.signal函数
UNIX系统的信号特性的最简单的接口是signal函数。
#include <signal.h> void (*signal(int signo, void (*func)(int)))(int) //成功返回前一个信号布署,错误返回SIG_ERR。
signo 参数只是上一节的信号名。func的值是常量SIG_IGN,常量SIG_DFL或当接到信号发生时要调用的函数的地址。如果我们指定SIG_IGN,则向内核表示忽略此信号。(记住我们不能忽略SIGKILL和SIGSTOP)。当指定SIG_DFL时,我们设置信号相关的动作为默认值,signal函数的原型说明此函数要求两个参数,返回一个函数指针,而该指针所指向的函数无返回值(void)。第一个参数signo是一个整型数,第二个参数是函数指针,它所指向的函数需要一个整型参数,无返回值。
另外如果我们检查系统的头文件<signal.h>,我们很可能发现下面形式的声明:
/* Fake signal functions. */ #define SIG_ERR ((__sighandler_t) -1) /* Error return. */ #define SIG_DFL ((__sighandler_t) 0) /* Default action. */ #define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
3.不可靠的信号
早期的UNIX系统的Signal是不稳定的:
1). 信号可能会丢失
2). 在信号处理函数中,需要重复注册信号,否则下次收不到信号
3). 不支持阻塞信号
4.中断的系统盗用
1). 早期UNIX中,一个进程在调用系统调用被阻赛的时候, 有可能被一个Signal中断。此时系统调用返回错误并且errno被设置为EINTR。
2). 系统调用也被分为两类:低速和一般。低速系统调用是那些可能永远阻塞的系统调用(disk IO是例外,一般情况下只会暂时阻塞,但也归入低速一类)
3). 为了处理这个问题,在早期UNIX系统中对于这些系统调用需要作额外的检查和判断,如果错误并且errno=EINTR,需要重新调用
4). Free BSD 4.2对于部分系统调用ioctl, read, readv, write, writev, wait实现了自动重新恢复功能,也就是当函数被中断的时候会自动恢复到原来的执行情况
5). Free BSD5.2.1, Linux 2.4.22, Mac OS X 10.3支持此功能,但POSIX.1不作强制要求
5.可重入函数
大多数函数不可重入原因:
1).已知它们使用静态数据结构
2).它们调用malloc和free函数
3).它们是标准I/O函数
6.SIGCLD语义
SIGCLD和SIGCHLD这两个信号很容易被混淆,
BSD的SIGCHLD信号的语义与其他信号的语义相类似。子进程状态改变后产生此信号,父进程需要调用一个wait函数以检测发生了什么。
对于SIGCLD早期的处理方式是:
1).如果进程特地指定对该信号的配置为SIG_IGN,则调用进程的子进程将不产生僵死进程
2).如果将SIGCLD的配置设置为捕捉,则内核立即检查是否有子进程准备好被等待,如果是这样,则调用SIGCLD处理程序
7.kill和raise函数
kill函数将信号发送给进程或进程组。raise函数则允许进程向自身发送信号。
#include <signal.h> int kill(pid_t pid, int signo); int raise(int signo); //两者成功都返回0,错误返回-1
调用raise(signo)等价于kill(getpid(), signo);
kill的pid参数有四种情况:
1).pid > 0, 信号被发送给进程ID为pid的进程;
2).pid == 0,信号被发送给与发送进程属于同一进程组的所有进程(这些进程的进程组ID等于发送进程的进程组ID),而且发送进程具有向这些进程发送信号的权限。注意术语“所有进程”不包括实现定义的系统进程集。对于多数UNIX系统,这个系统进程集包括内核进程和init(pid 1);
3).pid < 0, 将该信号发送给ID等于pid的绝对值,且发送者对其有发送信号的权限的所有进程。如上,所有进程集不包括系统进程。
4).pid == -1,将该信号发送给发送进程有权限向它们发送喜好的系统上的所有进程。和前面一样,不包含特定的系统进程。
8.alarm和pause函数
1).使用alarm函数可以设置一个时间值(闹钟时间),在将来的某个时刻该时间值会被超过。当所设置的时间值被超过后,产生SIGALRM信号。如果不忽略或不捕捉此信号,则其认动作是终止该进程。
#include <unistd.h> unsigned int alarm(unsigned int seconds); //返回0或上次设置的警报到现在的时间。
如果在调用alarm时,以前已为该进程设置过闹钟时间,而且它还没有超时,则该闹钟时间的余留值作为本次alarm函数调用的值返回。以前登记的闹钟时间则被新值代换。如果有以前登记的尚未超过的闹钟时间,而且seconds值是0,则取消以前的闹钟时间,其余留值仍作为函数的返回值。
2).pause函数使调用进程挂起直至捕捉到一个信号。
#include <unistd.h> int pause(void); //返回-1,errno设置为EINTR。
只有执行了一个信号处理程序并从其返回时, pause才返回。在这种情况下, pause返回-1,errno设置为EINTR。
9.信号集
POSIX.1定义数据类型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,错误返回-1.
函数sigemptyset初始化set指向的信号集,清除其中所有信号。函数sigfillset初始化信号集使其所有的信号。函数sigaddset向一个已存在的集合加入一个信号,而sigdelset从一个信号删除一个信号。
10.sigprocmask函数
调用函数sigprocmask可以检测或更改(或两者)进程的信号屏蔽字。
#include <signal.h> int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset); //成功返回0,错误返回-1
首先, oset是非空指针,进程的当前信号屏蔽字通过oset返回。其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。下表说明了how可选用的值。SIGBLOCK是或操作,而SIGSETMASK则是赋值操作。
how | 描述 |
---|---|
SIG_BLOCK | 进程的新信号掩码是它当前掩码和set指向的信号集的并集,也就是说,set包含了我们想阻塞的额外的信号。 |
SIG_UNBLOCK | 进程的信号掩码是当前信号掩码和set指向的信号集的反码的交集。也就是说,集合包含了我们想反阻塞的信号。 |
SIG_SETMASK | 进程的新信号掩码被set指向的信号集的指代替 |
如果set是个空指针,则不改变该进程的信号屏蔽字, how的值也无意义
11.sigpending函数
sigpending返回对于调用进程被阻塞不能递送和当前未决的信号集。该信号集通过set参数返回。
#include <signal.h> int sigpending(sigset_t *set); //成功返回0,错误返回-1
PS:被此函数阻塞的信号,不会递送调用进程,直到该信号不再被阻塞
12.sigaction函数
sigaction函数的功能是检查或修改(或两者)与指定信号相关联的处理动作。此函数取代了UNIX早期版本使用的signal函数。在本书末尾用sigaction函数实现了signal。
#include <signal.h> int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact); //成功返回0,错误返回-1.
其中,参数signo是要检测或修改具体动作的信号的编号数。若act指针非空,则要修改其动作。如果oact指针非空,则系统返回该信号的原先动作。此函数使用下列结构:
struct sigaction { void (*sa_handler)(int); /* addr of signal handler, or SIG_IGN, or SIG_DFL. */ sigset sa_mask; /* additional signals to block */ int sa_flags; /* signal options */ /* alternate handler */ void (*sa_sigaction)(int, siginfo_t *, void *); };
当更改信号动作时,如果sa_handler指向一个信号捕捉函数的地址(不是常数SIGIGN或SIGDFL),则samask字段说明了一个信号集,在调用信号捕捉函数之前,该信号集要加到进程的信号屏蔽字中。
其中siginfo_t结构体描述详见APUE
13.sigsetjmp和siglongjmp函数
POSIX.1定义了两个新函数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从其中恢复保存的信号屏蔽字。
14.sigsuspend函数
为了纠正不可靠信号机制的问题,需要在一个原子操作中实现恢复信号屏蔽字,然后使进程睡眠,这种功能是由sigsuspend函数所提供的。
#include <signal.h> int sigsuspend(const sigset_t *sigmask); //错误返回-1,errno被设为EINTR。
进程的信号屏蔽字设置为由sigmask指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程也被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且该进程的信号屏蔽字设置为调用sigsuspend之前的值.
注意,此函数没有成功返回值。如果它返回到调用者,则总是返回- 1,并且errno设置为EINTR (表示一个被中断的系统调用)。
15.abort函数
abort函数的功能是使程序异常终止
#include <stdlib.h> void abort(void); //函数决不返回。
此函数将SIGABRT信号发送给调用进程。进程不应忽略此信号。
16.system函数
System函数除了会执行可执行文件创建子进程之外,还会设置sigmask为忽略SIGINT,SIGQUIT并阻塞 SIGCHLD。
1). 忽略SIGINT和SIGQUIT的原因是,只有子进程应该处理这两个signal,而父进程无需处理
2). 阻塞SIGCHLD的原因是,System函数需要知道子进程的结束,而父进程不应该先提前知道,以免提前调用wait函数使得System函数本身无法获得进程的退出值
17.sleep函数
#include <unistd.h> unsigned int sleep(unsigned int seconds); //返回0或未睡眠的秒数。
此函数使调用进程被挂起直到:
(1) 已经过了seconds所指定的墙上时钟时间;
(2) 该进程捕捉到一个信号并从信号处理程序返回。
如同alarm信号一样,由于某些系统活动,实际返回时间比所要求的会迟一些
18.作业控制信号
SIGCHILD:子进程已经被停止或终止;
SIGCONT:如果进程停止,则使其继续运行;
SIGSTOP:停止信号(不能被捕获或忽略);
SIGTSTP:交互的停止信号;
SIGTTIN:后台进程组的成员读控制终端;
SIGTTOU:后台进程组的成员向控制终端写。
19.其他特征
不做重点,详见APUE