20.1 概述
信号是事件发生时对进程的通知机制(软件中断)。信号中断与硬件中断的相似之处在于打断了程序执行的正常流程,大多数情况下,无法预测信号到达的精确时间
一个具有合适权限的进程能向另一个进程发送信号。信号的这一用法可作为一种同步技术,甚至是进程间通信的原始方式。进程也可以向自身发送信号。然而,发往进程的信号通常都源于内核,引发内核为进程产生信号的各类事件如下:
-
硬件发生异常
硬件检测到错误条件并通知内核,再由内核发送相应信号给相关进程。硬件异常的例子包括执行一条异常的机器语言指令,如被0除,引用了无法访问的内存区域
-
用户键入了能够产生信号的终端特殊字符
如中断字符Control+ C,暂停字符Control+ Z
-
发生了软件事件
如定时器到期。进程执行的CPU时间超限,进程的某个子进程退出等
针对每个信号,都定义了一个唯一的整数,从1开始顺序展开。<signal.h>
头文件中以SIGXXX形式的符号对这些整数做了定义。每个信号的实际编号随系统不同而不同。
信号分为两大类:一类是用于内核向进程通知事件,构成标准信号。Linux中标准信号的编号范围为1~31。另一类是实时信号。
信号产生后,稍后会被传递给某一进程,而进程会采取某些措施来响应这些信号。在信号的产生和到达期间,信号处于等待(pending)状态。
通常,一旦内核要调度进程执行,该进程的等待信号会马上送达,如果进程正在执行,则会立即传递信号。然而,有时需要确保一段代码的执行不受传递来的信号中断,为了做到这一点,可以将信号添加到进程的信号掩码中,阻塞指定信号的到达,该信号将保持pending状态,直至稍后对其解除阻塞(从信号掩码中移除)。进程可以使用各种系统调用对其信号掩码添加和移除
信号到达后,进程根据信号可进行如下操作:
- 忽略信号,内核将丢弃信号,信号对进程没有任何影响
- 终止(杀死)进程,进程异常终止,如kill 某个进程
- 产生核心转储文件,同时进程终止
- 暂停进程的执行
- 在暂停进程的执行后,恢复进程的执行
除了根据特定信号而采取默认行为外,程序也能改变信号到达时的响应行为。也将此称之为对信号的处置设置(信号处理函数)。程序可以将对信号的处置设置如下:
-
采取默认行为,适用于撤销之前对信号处理函数的设置,恢复默认方法
-
忽略信号
-
执行信号处理函数,编写的函数,为了响应传递来的信号而执行适当任务。
20.2 信号类型
信号名称 | 说明 |
---|---|
SIGABRT | 进程调用abort()函数时,系统向进程发送该信号 |
SIGALRM | 调用alarm()或setitimer()而设置的定时器,到期后,产生该信号 |
SIGBUS | 发生了某种内存访问错误 |
SIGCHLD/SIGCLD | 父进程的某一子进程停止/恢复/终止时,向父进程发送该信号 |
SIGCONT | 恢复已经暂停运行的进程,如果进程正在运行,忽略该信号 |
SIGEMT | 标识依赖于实现的硬件错误 |
SIGFPE | 特定类型的算数错误,如除以0 |
SIGHUP | 当终端断开时,发送该信号给终端控制进程。还可用于守护进程,如init,httpd,守护进程收到该信号时重新进行初始化并重读配置文件 |
SIGILL | 进程试图执行非法的机器语言指令,系统向进程发送该信号 |
SIGINFO/SIGPWR | BSD系统中,输入control-T可产生该信号,用户获取前台进程组的状态信息 |
SIGINT | 用户输入终端终端字符(Control + C)时,终端驱动程序将发送信号给前台进程 |
SIGIO | 利用fcntl()系统调用时,打开的特定类型的文件描述符(如终端,套接字)发生I/O事件时产生该信号 |
SIGKILL | 必杀信号,处理程序无法将其阻塞,忽略,捕获,所以总能终止进程 |
SIGPWR | 电源故障信号 |
SIGQUIT | 用户在键盘上输入退出字符(Control + \ )时,内核将信号发往前台进程组,默认情况下,信号终止进程,并生成可用于调试的核心转储文件。进程如果陷入无限循环或无影响时,使用该信号就很合适。 |
SIGSEGV | 当应用进程对内存的引用无效时,就会产生该信号。如引用的页不存在,进程更新只读内存,在用户态访问内核的不分内存等 |
SIGSTOP | 停止进程 |
SIGSYS | 进程发起的系统调用有误 |
SIGTERM | 用来终止进程的标准信号,也是kill,killall命令所使用的默认信号。精心设计的程序应当为该信号设置处理器程序,以便于能预先清除临时文件和释放资源**** |
SIGTSTP | 作业控制的停止信号,当用户在键盘上输入挂起字符(Control +Z)时,将引发该信号给前台进程组,使其停止运行 |
SIGTTIN | 作业控制shell运行时,若后台进程组试图对终端进行read()操作,终端驱动程序则向该进程组发送此信号。该信号默认将停止进程 |
SIGTTOU | 后台作业的终端输出 |
SIGXCPU | 当进程的CPU时间超出对应的资源限制时,发送此信号给进程 |
20.3 设置信号处理函数
signal()
// 系统调用singal()是设置信号处置的原始API, 不同UNIX的实现存在差异, 可移植性较差
// 系统调用sigaction()提供了signal()不具备的功能, 是建立信号处理器的首选API
#include <signal.h>
void (*singal(int sig, void(*handler)(int))) (int);
/*
sig: 希望修改的信号编号
handler: 标识信号抵达时所调用的函数地址, 该函数无返回值, 并接收一个整型参数
return:
success: 返回之前的信号处理函数,函数指针,指向的是一个带有整型参数且无返回值的函数,
error: 返回SIG_ERR
signal.h中针对信号处理函数做出了如下定义:
typedef void(*sighandler_t)(int);
所以signal()原型函数可以改写为如下形式:
sighandler_t signal(int sig, sighandler_t handler);
*/
// 改变信号处理流程一般如下
int main() {
void (*old_handler)(int);
// sighandler_t old_handler; 等价定义
old_handler = singal(SIGINT, new_handler);
if (old_handler == SIG_ERR) {
exit(-1);
}
// do something, 在此期间, 如果信号SIGINT来了, 新的处置函数会处理该信号
// 将SIGINT信号处理函数重置为本来面目
if (singal(SIGINT, old_headler) == SIG_ERR) {
exit(-1);
}
}
在为signal()指定handler函数时,可使用如下值来代替函数地址:
- SIG_DFL(SIGNAL_DEFAULT):将信号处理函数重置为默认值
- SIG_IGN(SIGNAL_IGNORE):忽略该信号,内核会将该信号丢弃。进程甚至从未知道曾经产生了该信号
sigaction()
sigaction()的优势<推荐使用>
- 允许在获取信号处理函数的同时无需将其改变
- 设置各种属性对调用信号处理器函数时的行为施以更加精准的控制
- 可移植性更加,更加灵活
#include <signal.h>
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
return 0 on success, or -1 on error
// sig 想要获取或改变的信号编号, 不能是SIGKILL和SIGSTOP
// act 指向新的描述信号处理的数据结构
// oldact 之前信号处理的相关信息
// act为NULL, oldact不为NULL, 返回现有的信号处理方法
// oldact为NULL, 说明对之前处理不感兴趣, 无需返回
struct sigaction {
void (*sa_handler)(int); // 信号处理函数地址
sigset_t sa_mask;
int sa_flags; // 控制信号处理过程的各种选项
void (*sa_restorer)(void);// 预留, 未使用
}
sa_mask字段定义了一组信号,在调用由sa_handler所定义的信号处理函数时将阻塞该信号。即在调用sa_handler前,将sa_mask添加至进程掩码中,当sa_handler返回后,将sa_mask从进程掩码中移除。利用sa_mask字段可指定一组信号,不允许中断此信号处理函数的执行。
sa_flags字段是一个位掩码,指定用于控制信号处理过程的各种选项。该字段包含的位可以相或(|)
sa_flags | 含义 |
---|---|
SA_NOCLDSTOP | 若sig为SIGCHLD信号,则当因接受一信号而停止或恢复某一子进程时,将不会产生此信号 |
SA_NOCLDWAIT | 若sig为SIGCHLD信号,则当子进程终止时不会将其转化为僵尸进程 |
SA_NODEFER | 捕获该信号时,不会在执行信号处理函数时将该信号自动添加到进程掩码中 |
SA_ONSTACK | 针对此信号调用处理函数时,使用了由sigaltstack()安装的备选栈 |
SA_RESETHAND | 当捕获该信号时,会在处理函数调用之前将信号处理函数重置为默认值 |
SA_RESTART | 自动重启由信号处理程序中断的系统调用 |
SA_SIGINFO | 调用信号处理函数时携带了额外参数 |
20.4 信号捕捉器
信号处理器程序(信号捕捉器)是指当指定信号传递给进程时将会调用的一个函数(信号处理函数)。
调用信号处理器程序,可能会随时打断主程序流程,"内核代表进程来调用信号处理函数",当信号处理函数返回时,主程序会在处理器打断的位置恢复执行。执行流程如下图
20.5 发送信号:kill
与shell的kill命令相似,一个进程能使用kill()系统调用向另一个进程发送信号。(选择kill作为术语,是因为早期UNIX实现中大多数信号的默认行为是终止进程)。
#include <singal.h>
int kill(pid_t pid, int sig);
/*
pid: 进程号
sig: 指定了要发送的信号量
return: 0 on success, or -1 on error
如果进程无权发送信号给所请求的pid, 则调用失败, 将errno置为EPERM.若pid所指为一系列进程, 只要其中一个发送成功, 则kill调用成功
*/
pid:
-
pid > 0
发送信号给由pid指定的进程
-
pid = 0
发信号给与调用进程同组的每个进程,包括调用进程自身。
-
pid = -1
信号发送范围:调用进程有权将信号发往每个目标进程,除去init和调用进程自身
-
pid < -1
会向组ID等于该pid绝对值的进程组内所有下属进程发送信号
20.6 检查进程的存在
kill()系统调用另一个重要的功能,若将sid指定为0(即空信号),则无信号发送。kill()仅会执行错误检查,查看是否可以向目标进程发送信号。这也就意味着可以使用空信号来检测指定pid的进程是否存在。
若发送空信号失败,且errno为ESRCH,则表明进程不存在
若调用失败,且errno为EPERM(表示进程存在,但是无权限发送)或者调用成功(有权向进程发送信号),则表示进程存在。
还可以检查/proc/PID目录是否存在来检查进程是否存在
20.7 发送信号:raise
#include <signal.h>
int raise(int sig);
/*
向自身发送信号
return 0 on success, or 非0 on error
单线程程序中等于 kill(getpid(), sig)
支持线程的系统等于 phread_kill(phread_self(), sig)
*/
当进程调用raise()/kill()向自身发送信号时,信号将立即传递(即,在raise()返回调用前)。
20.8 显示信号描述
每个信号都有一串与之相关的可打印说明。位于数组sys_siglist中,取数据时,推荐使用strsignal()函数。
#include <string.h>
char *strsignal(int sig);
// return pointer to signal description string, sig无效时返回空值
// 显示信号描述时会使用本地语言
20.9 信号集
多个信号可使用一个称之为信号集的数据结构来表示,即sigset_t
#include <signal.h>
// 初始化一个未包含任何成员的信号集
int sigemptyset(sigset_t *set);
// 初始化一个包含所有信号的信号集
int sigfillset(sigset_t *set);
// 添加信号
int sigaddset(sigset_t *set, int sig);
// 移除信号
int sigdelset(sigset_t *set, int sig);
// Both return 0 on success, -1 on error;
// sig是否为set的成员
int sigismember(const sigset_t *set, int sig);
// return 1 if sig in set, else return 0;
// set是否为空
int sigisempty(const sigset_t *set);
// return 1 if set未包含任何信号, else 0
# 以下三个为非标准函数
# left 和 right 的交集置于dest中
int sigandset(sigset_t *dset, sigset_t *left, sigset_t *right);
# left 和 right 的并集置于dest中
int sigorset(sigset_t *dset, sigset_t *left, sigset_t *right);
// Both return 0 on success, -1 on error;
**必须使用sigemptyset()和sigfillset()来初始化信号集。**因为C语言不会对自动变量进行初始化,而且,借助于静态变量初始化为0的机制来表示空信号集的做法在可移植性上存在问题,因为有可能使用位掩码之外的数据结构来实现信号集。
20.10 信号掩码(阻塞信号传递)
内核会为每个进程维护一个信号掩码,即一组信号,并将阻塞其针对该进程的传递。如果将遭阻塞的信号发送给某进程,那么对该信号的传递将延后,直至从进程掩码中移除该信号,从而接触阻塞为止。(信号掩码属于线程属性,在多线程进程中,每个进程都应独立维护其信号掩码)
系统调用sigprocmask()可向信号掩码添加信号
#include<singal.h>
// 获取和修改新词的信号掩码
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
// return 0 on success, or -1 on error
// how 指定了函数给信号掩码带来的变化
// SIG_BLOCK 将set信号集内的信号添加到信号掩码中
// SIG_UNBLOCK 将set信号集内的信号从信号掩码中移除
// SIG_SETMASK 将set信号集赋给信号掩码
// set 信号集
// 若oldset不为NULL, 返回之前的信号掩码
// 如果set为空, 将忽略how参数, oldset不为空, 则为获取信号掩码
系统将忽略视图阻塞SIGKILLHESIGSTOP信号的请求。如果试图阻塞这些信号,那么sigprocmask()函数将忽略,也不会产生错误。可使用如下代码来阻塞除SIGKILL和SIGSTOP之外的所有信号
sigfillset(&block_set);
sigprocmask(SIG_BLOCK, &block_set, NULL);
20.11 处于等待的状态的信号
如果某进程接受了一个进程正在阻塞的信号,那么该信号将添加到进程的等待信号集中。当解除对该信号的阻塞后,会将该信号传递给此进程。
等待信号集只是一个掩码,仅表明一个信号是否发生,未表明发生次数
#include <signal.h>
// 获取处于等待的信号
int sigpending(sigset_t *set);
// return 0 on success, or -1 on error
20.12 不对信号进行排队处理
等待信号集只是一个掩码,仅表明一个信号是否发生,而未表明其发生的次数。换言之,如果同一信号在阻塞状态下产生多次,那么会将信号记录在等待信号集中,并在解除阻塞后仅传递一次。
如果进程没有阻塞信号,其所收到的信号可能比发送给它的要少的多。如果信号信号发送速度很快,以至于在内核将执行权交给接收进程之前,这些相同的信号就已经到达,此时在进程等待信号集中,相同的信号只会记录一次。(乱序到达的相同信号,也只会记录一次)
20.13 等待信号:pause()
#include <unistd.h>
int pause(void);
// 总是返回-1,并将errno置为EINTR
调用pause()将暂停进程的执行,直至信号处理器函数中断该调用为止。
借助于pause(),进程可暂停执行,直至信号送达为止
20.14 总结
信号是发生某种事件的通知机智,可以由内核,另一进程或进程自身发送给进程。存在一系列的标准信号类型,每种都有唯一的编号和目的。
信号传递通常是异步行为,这意味着信号中断进程执行的位置是不可预测的。有时,信号也可同步传递。
默认情况下,要么忽略信号,要么终止进程,要么停止一个正在进行的进程,要么重启一个已停止的进程。特定的默认行为取决于信号类型。此外可以使用signal()或sigaction()来显示忽略一个信号,或者建立一个由程序员自定义的信号处理函数,以供信号到达时调用。处于可移植性考虑,最好使用sigaction()来设置信号处理函数。
一个具有适当权限的进程可以使用kill()像另一个进程发送信号。发送空信号(0)是判断特定进程ID是否存在的方式之一。
如果接收的信号遭到阻塞,那么该信号将保持pending状态,直至解除对其阻塞。系统不会对标准信号进行排队处理,也就是说,将信号标记为pending状态,只会发生一次。
20.15 code
#define _BSD_SOURCE
#include <csignal>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
#include <string>
void exit_(std::string msg)
{
printf("error:%s, errno:%d, description:(%s)\n", (char*)&msg, errno, strsignal(errno));
_exit(EXIT_FAILURE);
}
/* 打印信号集合中包含信号 */
void print_signal_set(const char *prefix, const sigset_t *sigset)
{
bool empty = true;
for (int sig = 1; sig < NSIG; sig++) {
if (sigismember(sigset, sig)) {
empty = false;
printf("prefix=%s sig=%-2d description=(%s)\n", prefix, sig, strsignal(sig));
}
}
if (empty) {
printf("prefix=%s <empty signal set>\n", prefix);
}
}
/* 打印进程阻塞的信号 */
int print_signal_mask(const char *msg)
{
sigset_t mask;
if (msg != NULL) {
printf("%s", msg);
}
// 获取阻塞的信号
if (sigprocmask(SIG_BLOCK, NULL, &mask) == -1) {
return -1;
}
print_signal_set("\t\t\t", &mask);
return 0;
}
/* 打印当前进程中处于pending状态的信号 */
int print_pending_signal(const char *msg)
{
sigset_t pending_signal;
if (msg != NULL) {
printf("%s\n", msg);
}
if (sigpending(&pending_signal) == -1) {
return -1;
}
print_signal_set("pending", &pending_signal);
return 0;
}
void new_signal_handler(int sig)
{
printf("new signal handler:: signal = %d, description = %s\n",
sig, strsignal(sig)); /* Unsafe */
}
// signal()的再次封装
// Set the handler for the signal SIG to signal_handler,
// returning the old handler, or SIG_ERR on error.
//void (*signal_wrapper(int signal_, void(*signal_handler)(int))) (int) 与下述定义等价
sighandler_t signal_wrapper(int signal_, sighandler_t signal_handler)
{
// void (*old_handler)(int); 等价定义
sighandler_t old_handler;
old_handler = signal(signal_, signal_handler);
if(old_handler == SIG_ERR) {
exit_("signal_wrapper error");
}
return old_handler;
}
// 重写SIGINT的信号处理函数
void test_signal()
{
int seconds = 3;
// SIGINT:终端输入Control + C时, 终端产生此信号
void (*old_handler)(int);
printf("set SIGINT to new signal handler\n");
old_handler = signal_wrapper(SIGINT, new_signal_handler);
int times = 5;
for(int j = 0; j < times; j++) {
sleep(seconds); // 输入Control + C时, 终止了sleep(信号中断)
}
// 将SIGINT信号处理器回复为默认处理器
printf("set SIGINT handler to default\n");
signal_wrapper(SIGINT, old_handler);
// 此时输入control + c直接退出程序
sleep(seconds);
printf("test_signal finish\n");
}
// 测试raise函数, 向进程本身发送信号
void test_raise()
{
printf("set SIGINT to new signal handler\n");
signal_wrapper(SIGINT, new_signal_handler);
printf("start raise SIGINT...\n");
for(int i = 0; i < 3; i++) {
printf("\traise SIGINT %s\n", -1 == raise(SIGINT) ? "error": "success") ;
}
sleep(1);
}
// 测试struct sigset_t
void test_sigset_t()
{
sigset_t sigs;
if (sigemptyset(&sigs) == -1) {
exit_("sigemptyset error");
}
printf("sigset is empty ? %d\n", sigisemptyset(&sigs));
printf("SIGINT in sigset? %d\n", sigismember(&sigs, SIGINT));
printf("add SIGINT to sigset\n");
if (sigaddset(&sigs, SIGINT) == -1) {
exit_("sigaddset error");
}
printf("SIGINT in sigset? %d\n", sigismember(&sigs, SIGINT));
printf("Set all signals in sigset\n");
if (sigfillset(&sigs) == -1) {
exit_("sigfillset error");
}
print_signal_set("all signals", &sigs);
printf("sigset is empty ? %d\n", sigisemptyset(&sigs));
printf("SIGINT in sigset? %d\n", sigismember(&sigs, SIGINT));
printf("remove SIGINT from sigset\n");
if (sigdelset(&sigs, SIGINT) == -1) {
exit_("sigdelset error");
}
printf("SIGINT in sigset? %d\n", sigismember(&sigs, SIGINT));
}
// 信号掩码的新增和移除
void test_mask() {
// SIGTSTP = Control + z
signal_wrapper(SIGINT, new_signal_handler);
signal_wrapper(SIGTSTP, new_signal_handler);
sigset_t sigs;
if (sigemptyset(&sigs) == -1) {
exit_("sigemptyset error");
}
printf("add SIGINT,SIGTSTP(ctrl+c,z) to mask\n");
// SIGTSTP ctrl + Z
if (sigaddset(&sigs, SIGINT) == -1 || sigaddset(&sigs, SIGTSTP) == -1) {
exit_("sigaddset error");
}
// SIGTSTP 和 SIGINT添加置掩码
if (sigprocmask(SIG_BLOCK, &sigs, NULL) == -1) {
exit_("sigprocmask error");
}
if(raise(SIGTSTP) == -1 || raise(SIGINT) == -1) {
exit_("raise error");
}
print_pending_signal("mask is not empty");
printf("remove SIGINT,SIGTSTP(ctrl+c,z) from mask\n");
// SIGTSTP 和 SIGINT从掩码中移除
if (sigprocmask(SIG_UNBLOCK, &sigs, NULL) == -1) {
exit_("sigprocmask error");
}
print_pending_signal("mask is empty");
}
// pending signal
void test_signal_pending() {
sigset_t sigs;
if (sigemptyset(&sigs) == -1) {
exit_("sigemptyset error");
}
// SIGTSTP = ctrl + Z
if (sigaddset(&sigs, SIGINT) == -1 || sigaddset(&sigs, SIGTSTP)) {
exit_("sigaddset error");
}
printf("add SIGINT,SIGTSTP(ctrl+c,z) to mask\n");
if (sigprocmask(SIG_BLOCK, &sigs, NULL) == -1) {
exit_("sigprocmask error");
}
printf("raise 100*(SIGINT,SIGTSTP) to process\n");
for (int i = 0; i < 100; i++) {
if(raise(SIGTSTP) == -1 || raise(SIGINT) == -1) {
exit_("raise error");
}
}
print_pending_signal("pending signal");
}
int main(int argc, char *argv[]) {
if (argc > 1 && (!strcmp("-h", argv[1]) || !strcmp("help", argv[1]) || !strcmp("--help", argv[1]))) {
printf("argv[1] ==\n"
"0: test_signal\n"
"1: test_raise\n"
"2: test_sigset_t\n"
"3: test_mask\n"
"4: test_signal_pending\n");
return 0;
}
int type = argc > 1 ? atoi(argv[1]):0;
printf("type = %d\n", type);
switch (type) {
case 0:
test_signal();
break;
case 1:
test_raise();
break;
case 2:
test_sigset_t();
break;
case 3:
test_mask();
break;
case 4:
test_signal_pending();
break;
default:
;
}
return 0;
}