2. 信号

Unix/Linux 各种不同信号及其用途

基本概念

信号是事件发生时对进程的通知机制,有时也称为软件中断。信号更多用于进程间的一种同步技术。信号定义在头文件 <signal.h>中,不同的信号以 SIGXXXX的形式命名。 信号一般在进程正在执行,且内核态到用户态的下一次切换时发送,如果该信号由进程自己产生则会马上传递信号。当信号到达后,进程可采取如下措施:

  • 忽略信号

  • 终止进程

  • 产生核心转储文件,该文件可用于后续的调试

  • 停止进程

  • 恢复运行

信号类型与默认行为

常用信号如下(默认行为,term 信号终止,core 产生核心转储文件并退出,ignore 忽略信号,stop 停止进程,cont 恢复运行):

发送信号

kill

#include <signal.h>
/*
@brief 向进程 pid 发送 sig 信号
@param pid 标识一个或多个目标进程
       - pid > 0 : 发送给 pid 指定的进程
       - pid = 0 : 发送与当前进程同组的所有进程,包括自己
       - pid < -1: 向组 ID 为该 pid 绝对值的进程组发送信号
       - pid = -1: 发送给此进程有权发送的所有进程
@return	失败返回 -1,成功返回 0
*/
int kill(pid_t pid, int sig);

可以使用 kill(pid, 0)检查目标进程是否存在,sig 为 0 表示这是一个空信号。

raise

#include <signal.h>
// 向进程自身发送信号
// 成功返回 0,负责返回非零值
int raise(int sig);

在单线程程序中,相当于调用 kill(getpid(), sig),多线程环境下为 pthread_kill(pthread_self(), sig)。调用此函数向进程自身发送信号时,信号立即传递,且在 raise 函数返回之前。此函数唯一可能发生的错误为 EINVAL,即 sig 无效。

killpg

#include <signal.h>
// 向进程组 pgrp 发送信号,即 kill(-pgrp, sig)
int killpg(pid_t pgrp, int sig);

打印信号

#define _BSD_SOURCE
#include <signal.h>

extern const char *const sys_siglist[];

#define _GNU_SOURCE
#include <string.h>
// 打印 sig 信号的描述,本地字符集相关
char *strsignal(int sig);

#include <signal.h>
// 输出 msg : strsignal(sig)
void psignal(int sig, const char *msg);

信号集

许多信号相关的系统调用需要表示一组信号,比如阻塞一组信号,返回一组目前在等待的信号。一组信号可以通过信号集这个数据结构表示,即 sigset_t

#include <signal.h>
// 下面函数用于初始化信号集,分别为空信号集和满信号集
// 为了避免移植性问题,应该用下面函数初始化 sigset_t,不能直接自己初始化
// 因为不同平台 sigset_t 实现可能不一样
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
// 向集合添加或删除信号
int sigaddset(sigset_t *set, int sig);
int sigdelset(sigset_t *set, int sig);
// 测试信号是否为 set 的成员
int sigismember(const sigset_t *set, int sig);

// GNU C 另外实现了 3 个非标准函数
#define _GNU_SOURCE
#include <signal.h>
// 集合取交,取或,是否为空
int sigandset(sigset_t *set, sigset_t *left, sigset_t *right);
int sigorset(sigset_t *set, sigset_t *left, sigset_t *right);
int sigsiemptyset(const sigset_t *set);

信号掩码与信号等待

内核为每个进程维护一个信号掩码,用于阻塞信号对该进程的传递。在掩码内的信号将会延后传递给进程,直到该信号解除阻塞,解除阻塞后会立即传递给进程,如果该信号在阻塞期间内到达多次,在解除阻塞后也只传递一次。

#include <signal.h>
/*
@brief	按 how 指定的方式设置进程信号掩码,并返回现有掩码
@param	how 修改掩码的方式,可取:
    	- SIG_BLOCK: 	将 set 信号加入掩码
        - SIG_UNBLOCK:	将 set 中的信号移除掩码
        - SIG_SETMASK:	将 set 指向的信号赋给信号掩码
*/
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

如果进程接受一个被阻塞的信号,它会被加入到等待信号集中:

#include <signal.h>
// 返回等待信号集
int sigpending(sigset_t *set);

等待信号集只能说明信号是否发生,不能表面其发生次数,这些论述只针对标准信号,对于实时信号则会进行排队处理。

改变信号处置

signal

signal 系统调用是设置信号处置的原始 API,它的行为在不同 Unix 上存在差异,所以一般使用 sigaction 函数建立信号处理器函数。

#include <signal.h>
/*
@brief	设置 sig 信号的处理函数为 handler
@return	成功返回之前的处理函数(函数指针),失败则返回 SIG_ERR
*/
void (*signal(int sig, void (*handler)(int))) (int);

它无法在不改变信号处置的同时获取当前的信号处置。handler 可以采用以下定义值:

  • SIG_DFL:将信号处置重置为默认值

  • SIG_IGN:忽略该信号(不处置)

sigaction

sigaction 对信号的处置更具灵活性,也可以实现更精准的控制。

#include <signal.h>

struct sigaction {
    void (*sa_handler)(int);	// 信号处理器地址也可取 SIG_DFL,SIG_IGN
                                // 当信号不取 SIG_DFL,SIG_IGN sa_mask sa_flags 才有意义
    
    sigset_t sa_mask;	        // 调用信号处理器时阻塞的信号,避免这些信号中断信号处理器执行
                                // 会自动将引发信号处理器执行的信号添加到掩码中
    				// 意味着不会递归中断调用自己

    int sa_flags;		// 一个位掩码,用于控制信号处理过程中的各种选项,一般取 0
                                // SA_NODEFER 捕获信号时,不将此信号自动添加到掩码中
    				// SA_RESTART 自动重启由信号处理器程序中断的系统调用
                                // SA_SIGINFO 调用信号处理器时携带了额外参数
    void (*sa_restorer)(void);	// 不适用与应用程序
};

int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);

等待信号

#include <unistd.h>
// 暂停信号执行,知道信号处理器函数中断该调用
int pause(void);

信号处理器函数

信号处理器函数一般设计的越简单越好,以避免引发竞争条件的风险。

可重入函数和异步信号安全函数

如果一个进程的多条线程可以同时安全地调用此函数,则该函数为可重入的。更新全局变量或静态数据结构的函数可能是不可重入的(只用到本地变量的函数肯定是可重入的)。 如果某一函数是可重入的,又或者信号处理器函数无法将其中断时,就称该函数是异步信号安全的。 在编写信号处理器时:

  • 确保信号处理器函数代码本身是可重入的,且只调用异步信号安全函数

  • 当主程序执行不安全的函数或是去操作信号处理函数也会更新的全局数据结构时,阻塞该信号的传递

信号处理器可能会更新 errno,所以需要在函数入口保存一份拷贝,并在函数返回时重新赋值给 errno

全局变量与 sig_atomic_t 数据类型

可以使用 sig_atomic_t 数据类型以保证程序读写操作的原子性,以确保全局变量的共享是安全的。

// volatile 避免编译器将其优化到寄存器,以保证每次都从内存中读取
// SIG_ATOMIC_MIN, SIG_ATOMIC_MAX 规定了该类型的取值范围
volatile sig_atomic_t flag;

终止信号处理器函数的其他方法

除了直接返回外,还有以下终止方法:

  • _exit()函数终止进程,不能调用 exit()因为其会对缓冲区做额外处理

  • kill发送信号来杀掉进程

  • 从信号处理器函数执行非本地跳转,通过 sigsetjmp``siglongimp

  • 使用 abort 函数终止进程,并产生核心存储

SA_SIGINFO 标志

此标志会给信号处理器函数传递一些附加信息。sigaction中信号处理地址的完整申明如下:

struct sigaction {
    union {
        void (*sa_handler)(int);
        // siginfo_t 包含了信号以及发送信号进程的相关信息(暂略),最后一个参数暂时不用
        void (*sa_sigaction)(int, siginfo_t *, void *);
    } __sigaction_handler;
};

#define sa_handler __sigaction_handler.sa_handler
#define sa_sigaction __sigaction_handler.sa_sigaction

// Example
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;
sigaction(SIGINT, &act, NULL);

系统调用的中断和重启

可以利用下面的代码手动重启系统调用(也可以使用 SA_RESTART 标志,但并不是对所有函数都有效):

while((cnt = read(fd, buf, BUF_SIZE)) == -1 && errno == EINTR) {
    continue;
if(cnt == -1)
    exit()

高级特性

核心转储文件

某些信号会创建核心转储文件以方便调试。但有时候并不会产生,原因如下:

  • 进程对核心转储文件没有写权限。

  • 存在同名、可写的普通文件

  • 创建核心转储文件的路径不对

  • 文件大小超过了系统对核心转储文件的限制,可通过 ulimit 命令设置

  • 进程可创建文件大小的限制

  • 文件系统以满,工作目录文件系统仅可读

  • Set-user-ID 程序由非其属主(属组)执行时,不创建以避免用户恶意窥探程序数据

一些特殊情况

  • SIGKILL,SIGSTOP:其默认行为无法改变,除非进程处于 TASK_UNINTERRUPTIBLE 睡眠状态,此状态的进程,系统不会把信号传递给它。相反,TASK_INTERRUPTIBLE 睡眠状态则会被信号唤醒。

  • SIGCONT:若进程在停止状态,此信号总能唤醒进程,即使被阻塞或忽略。因为只能通过此信号恢复程序运行。若重新定义此信号的处理函数,当程序恢复运行后并取消阻塞后才会去调用。SIGSTOP 和 SIGCONT 分别会将其到来前的 SIGCONT 和 SIGSTOP 丢弃。

  • 由终端产生的信号若被忽略,则不应该改变其处置。

  • 信号一般是异步的,除非此信号为进程自己产生并发送给自己。此时会立即传递信号,除非其被阻塞。

实时信号

实时信号相比标准信号有以下特点:

  • 信号范围更大

  • 采取队列化管理,实时信号发送多次给进程将会多次传递,标准信号只会传送一次

  • 可指定伴随数据

  • 传递顺序有保障,会优先传递最小编号的信号

实时信号范围由 SIGRTMINSIGRTMAX定义。

// 传递给信号的伴随数据
// 一般很少使用 sival_ptr 因为只作用在进程内部
union sigval {
    int sival_int;
    void *sival_ptr;
};

#define _POSIX_C_SOURCE 199309
#include <signal.h>
// 将实时信号 sig 发送给 pid 进程,并附带信息
int sigqueue(pid_t pid, int sig, const union sigval value);

在使用 sigaction 处理信号时,siginfo_t 会设置以下字段:

  • si_signo: 信号编号

  • si_code: 信号来源,实时信号为 SIG_QUEUE

  • si_value: 即 sigval

  • si_pid,si_uid: 发送进程的进程 ID 和实际用户 ID

用掩码等待信号

#include <signal.h>

/*
@brief	将当前进程信号掩码替换为 mask,然后挂起进程直到捕获信号
        信号处理函数返回后掩码恢复为之前的值
*/
int sigsuspend(const sigset_t *mask);

// 相当于以不可中断方式调用
sigprocmask(SIG_SETMASK, &mask, &prevMask);
pause();
sigprocmask(SIG_SETMASK, &prevMask, NULL);

用同步方式等待信号

#define _POSIX_C_SOURCE 199309
#include <signal.h>
// 挂起进程知道 set 中的信号到达,信息收集到 info 中
// 成功返回收到的信号编号,失败返回 -1
int sigwaitinfo(const sigset_t *set, siginfo_t *info);
// 上面的变体,允许设置等待时限
int sigtimewait(const sigset_t *set, siginfo_t *info, const struct timespec *timeout);

最后更新于