第10章 信号
介绍
信号是软件中断。信号提供异步事件处理方法。
早期的信号模型 不可靠,信号可能丢失。POSIX.1对可靠信号例程进行了标准化。
调用kill(2)函数可将任意信号发送给另一个进程或进程组,但有限制:
- 信号的发送进程和接收进程的所有者必须相同,或者,
- 发送信号进程的所有者是超级用户
当某个信号出现时,内核有3种处理方式:
- 忽略:有2种信号不能被忽略 - SIGKILL 和 SIGSTOP , 原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法;
- 捕捉:比如,若捕捉到SIGCHLD信号,则表示一个子进程已经终止,所以SIGCHLD信号的捕捉函数可调用 waitpid 以取得该子进程的进程ID和它的终止状态。又比如,若进程创建了临时文件,那么要为 SIGTERM 信号编写一个信号捕捉函数以清除临时文件(SIGTERM是终止信号)
- 执行默认动作:图10-1给出了每一种信号的系统默认动作(P251)。大多数信号的系统默认动作是终止该进程。
常见信号及发生原因:
SIGABRT |
调用abort函数时产生此信号;进程异常终止。 |
SIGALRM |
进程所设置的定时器超时 |
SIGCHLD |
当一个进程终止或停止时,SIGCHLD被送给其父进程;系统默认忽略此信号;若父进程希望被告知,则其应该捕捉此信号,在信号处理函数中调用一种wait函数以取得子进程的ID和终止状态。 |
SIGCONT |
这是作业控制信号,发送给需要继续运行,但当前处于停止状态的进程。若进程没有处于停止状态,则默认动作是忽略此信号。 |
SIGFPE |
算术运算异常,如除以0、浮点移出 |
SIGHUP |
终端接口检测到一个连接断开,则将此信号发送给与该终端相关的控制进程(即会话首进程) |
SIGINT |
中断信号,由Ctrl+C产生,用来停止程序; |
SIGIO |
一个异步I/O事件产生了 |
SIGKILL |
用来杀死进程,不能被忽略 |
SIGPIPE |
在管道的读进程已终止后,一个进程写此管道,则产生此信号; |
SIGQUIT |
Ctrl+\ ,不仅终止前台进程组,而且产生一个core文件 |
SIGSEGV |
无效的内存访问 |
SIGSTOP |
这是一个作业控制信号,停止一个进程;不能被忽略 |
SIGTERM |
一般让该信号的捕捉函数在程序退出之前做好清理工作,从而优雅地终止 |
SIGTSTP |
交互停止信号;当用户在终端上按 Ctrl+Z 后,终端驱动程序产生此信号。该信号发送给前台进程组的所有进程。 |
函数 signal
#include <signal.h>
void (*signal(int signo, void(*func)(int)))(int);
- signo是信号名
- func的值是3种之一: SIG_IGN、SIG_DFL(default)、以及信号处理函数的地址。
这个原型太复杂了,使用下面的typedef会简单一些:
typedef void Sigfunc(int);
Sigfunc *signal(int, Sigfunc *);
简单来讲,signal函数原型是:
- 有2个参数,返回1个函数指针;
- 第1个参数signo是一个整型数,代表信号;
- 第2个参数是一个函数指针,它所指向的函数带1个int型的参数而无返回值;
- 返回值是1个函数指针,它所指向的函数带1个int型的参数而无返回值;
查看系统的头文件signal.h,可以看到下列形式的声明:
#define SIG_ERR (void (*)())-1
#define SIG_DFL (void (*)())0
#define SIG_IGN (void (*)())1
这些常量用于表示“指向函数的指针,该函数要求一个整型参数,而无返回值”。这些常量所用的3个值不一定是-1、0、1,但它们必须是3个值而决不能是任一函数的地址。
程序示例:
#include <signal.h>
// one handler for 2 signals
static void sig_user(int)
{
if (signo == SIGUSR1)
printf("received SIG_USR1\n");
else if (signo == SIGUSR2)
printf("received SIG_USR2\n");
else
printf("received signal %d\n", signo);
}
int main()
{
if (signal(SIGUSR1, sig_usr) == SIG_ERR)
printf("cannot catch SIGUSR1\n");
if (signal(SIGUSR2, sig_usr) == SIG_ERR)
printf("cannot catch SIGUSR2\n");
while(1) {
pause();
}
return 0;
}
程序的启动
exec函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不变(一个进程原先要捕捉的信号,当其执行一个新程序后,就不能再捕捉了,因为信号捕捉函数的地址一般在所执行的新程序文件中已无意义)。
当进程调用fork后,子进程复制了父进程的内存映像,所以信号捕捉函数的地址在子进程中是有意义的。
很多捕捉到SIG_INT和SIG_IGN的程序具有下列形式的代码:
void sig_int(int);
void 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);
不可靠的信号
不可靠指的是,信号可能会丢失。
有时候用户希望通知内核阻塞某个信号,即:不忽略该信号,在其发生时记住它,在进程做好了准备时再通知它。这种阻塞信号的能力,在早期并不具备。
中断的系统调用
早期的UNIX系统:如果进程在执行一个低速系统调用而阻塞时,捕捉到一个信号,则该系统调用就被中断而不再执行。
后来的UNIX系统:捕捉到信号并处理后,会返回已读或已写的部分,从而视该系统调用为成功。
4.2BSD引入了一些被中断后可以自动重启动的系统调用:ioctl、read、readv、write、writev、wait、waitpid.
前5个函数只有对低速设备操作时才会被信号中断,而wait和waitpid在捕捉到信号时则总是被中断。
4.3BSD允许进程基于每个信号禁用此自动重启动的功能。
系统调用可分为2类:低速系统调用和其他系统调用。
低速系统调用是可能会使进程永远阻塞的一类系统调用。如,读管道、终端、网络设备时,数据不存在。
可重入函数
下列是一些不可重入函数的特征:
- 使用了静态的数据结构
- 调用了malloc或free
- 是标准I/O函数,因为标准I/O库的很多实现都以不可重入的方式使用全局数据结构
作为一种通用的规则,当在信号处理程序中调用图10-4中的这些可重入函数时,应当在调用前保存errno,在调用后恢复errno.
在信号处理程序中,调用一个不可重入函数,其结果是不可预知的。举个栗子,当主程序中调用free时被信号中断,而信号处理程序中也调用了free,那么malloc和free维护的数据结构就遭到了破坏,从而程序会运行出错。
SIGCLD语义
SIGCLD是SystemV的一个信号名,其语义与名为SIGCHLD的BSD信号不同。POSIX.1采用BSD的SIGCHLD信号。
BSD的SIGCHLD的语义是,子进程改变状态后产生此信号,父进程需要调用一个wait函数以检测发生了什么。
对于SIGCLD的处理方式(略)
注意,Linux3.2.0和Solaris 10定义了SIGCLD,其等同于SIGCHLD.
可靠信号术语与语义
当一个信号产生时,内核通常在进程表中以某种形式设置一个标志。当内核做这个动作时,我们称为“向进程递送了一个信号”。
在信号产生(generation)和递送(delivery)之间的时间间隔,称信号是未决的(pending)。
进程可以“阻塞信号递送”。如果为进程产生了一个阻塞的信号,而且信号处理动作是系统默认动作(SIG_DFL)或捕捉该信号,那么该进程将此信号保持为未决状态,直到该进程对此信号解除了阻塞或对此信号的动作更改为忽略。
进程调用 sigpending 函数来判定哪些信号是设置为阻塞并处于未决状态的。
如果在进程解除对某个信号的阻塞之前,该信号发生了多次,将如何呢?
POSIX.1允许系统递送该信号一次或多次。若递送多次,则称这些信号排队了。但除非支持POSIX.1的实时扩展,否则大多数UNIX并不对信号排队,而只递送一次。
若有多个信号要递送给一个进程,POSIX.1并没有规定这些信号的递送顺序。但POSIX.1基础部分建议:在其他信号之前递送与进程当前状态有关的信号,如SIGSEGV.
每个进程都有一个信号屏蔽字(signal mask),它规定了当前要阻塞而不递送到该进程的信号集。对于每种可能的信号,该屏蔽字中都有一位与之对应。对于某种信号,若其对应位已经设置,则它当前是被阻塞的。进程可以调用 sigprocmask 函数来检测和更改其当前的信号屏蔽字。
信号编号可能会超过一个整型的二进制位数,因此POSIX.1定义了一个新数据类型 sigset_t , 它可以容纳一个信号集。
信号集
数据类型信号集(signal_set)被函数sigprocmask用于告诉内核不允许发生在该信号集中的信号。
下面是处理信号集的函数
#include <signal.h>
int sigemptyset(sigset_t *set); // 清除信号集中的所有信号
int sigfillset(sigset_t *set); // 初始化由set指向的信号集,使其包括所有信号
int sigaddset(sigset_t *set, int signo); // 讲一个信号signo添加到信号集set中
int sigdelset(sigset_t *set, int signo); // 从信号集中删除一个信号
// 以上4个函数,若成功,返回0,失败返回-1
int sigismember(const sigset_t * set, int signo); // 判断信号signo是否在信号集set内
信号相关函数
- kill(): 将信号发送给进程或进程组。
- raise(): 允许进程向自身发信号。 raise(signo); = kill(getpid(), signo);
- alarm(): 可利用此函数设置一个定时器,将来超时的时候,产生SIGALRM信号。如果忽略或不捕捉该信号,则默认动作是终止调用该alarm函数的进程。
- pause():使调用进程挂起,直至捕捉到一个信号。
- abort(): 将SIGABRT信号发送给调用进程,使其异常终止。让进程捕捉SIGABRT信号的意图是,在进程终止之前执行所需的清理工作。
- sigprocmask(): 检测或更改进程的信号屏蔽字
- sigpending(): 返回一个信号集,对于调用进程而言, 其中各信号是阻塞而不能递送的,因而也一定是当前未决的。
- sigaction(): 常被用来实现signal函数,功能是检查或修改与指定信号相关联的处理动作。此函数取代了UNIX早期版本的 signal 函数。
- sigsuspend(): 在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且从该信号的处理程序返回了,则sigsuspend()返回,并且该进程的信号屏蔽字设置为调用sigsuspend()之前的值。
- sigqueue(): 大部分UNIX系统不对信号进行排队,而POSIX.1的实时扩展中,有些系统开始增加对信号排队的支持。
- sigsetjmp()和siglongjmp(): 从多重嵌套的信号处理程序中一下返回到主程序中。
作业控制信号
POSIX.1认为有以下6个与作业控制有关的信号:
- SIGCHLD: 子进程已经停止或终止
- SIGCONT: 如果进程停止,则使其继续运行
- SIGSTOP: 停止信号(不能被捕捉或忽略)
- SIGTSTP: 交互式停止信号
- SIGTTIN: 后台进程组成员读控制终端
- SIGTTOU: 后台进程组成员写控制终端
其他略
信号名和编号
略
(完)