Linux 信号基本概念(signal)

Github链接

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/SIGPWRBSD系统中,输入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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值