Linux 信号机制

Posted by KalosAner on March 26, 2025

一、引言

Linux 信号机制是进程间通信的一种方式,用于在不同进程之间传递信息。它通过向目标进程发送一个特定的信号来触发目标进程执行相应的处理操作。Linux 内核中实现信号机制的关键是信号处理函数和信号传递,每个进程都有一个信号表来表示该进程对不同信号的默认处理方式。

信号的处理函数等都是进程间级共享的。

执行信号的动作称为信号递达,信号产生到递达之间的状态称为信号未决。当一个进程向另一个进程发送信号时,内核会将信号添加到目标进程的未决信号队列中。

进程可以选择阻塞某个信号,被阻塞的信号产生时将保持在未决状态,直到解除阻塞。信号阻塞不同于信号忽略,信号阻塞是一种状态,信号忽略是一种处理方式。

Snipaste_2025-04-18_09-53-41

进程(或者说线程)会在从内核态返回用户态时检查该进程是否有挂起的信号。如果存在待处理的信号,进程会在返回用户空间之前跳转到信号处理程序。所以即使信号在任意时刻到达,它们通常在下一个用户态的“安全点”被处理。不过如果进程正在执行一个被阻塞的系统调用(等待 I/O 等,非阻塞的 I/O 不会被中断)的时候有信号到达,系统调用可能(该信号处理程序没有使用 SA_RESTART 标志会被中断,如果使用了 SA_RESTART 标志系统调用会在信号处理结束后自动重新启动)会被中断然后返回错误(将 errno 设置为 EINTR),之后在“安全点”调用相应的信号处理函数。

二、信号的特性

信号声明周期

Linux 信号生命周期是指信号的产生、传递、处理和终止的整个过程。

信号的产生可以由多种事件触发,例如硬件中断、软件异常或者用户自定义信号。

信号传递是指信号产生之后由一个进程传递到另一个进程的不同线程中,传递可以被阻塞或者忽略也可以通过信号处理函数处理。

信号处理是指接收到信号后进程对信号的响应行为,可以选择默认处理方式或者自定义信号处理函数。

信号终止是指信号处理完成后,进程或者线程恢复到正常执行状态,如果进程选择了默认处理方式可以能会导致进程异常终止。

信号分类

Linux 信号一种有 64 种信号,每一个信号都有唯一整数编号,Linux 信号可以分为不可靠信号和可靠信号。

不可靠信号又称非实时信号,是指信号传递过程中可能丢失或产生不可预测行为的信号,这意味着进程接收到该信号后无法确保该信号一定被进程处理。1-31号信号为不可靠信号。

可靠信号又称实时信号,是保证传递和处理的信号。当进程接收到可靠信号后,系统会确保该信号不会丢失,并且等待进程处理完该信号后再继续执行其他操作,Linux 使用队列来保存待处理的信号,保证它们按照接收的顺序被进程处理。34-64号信号为可靠信号。

Linux 可以执行 kill -l 命令查看所有的信号。

kill 可以向指定进程发送指定信号

三、信号处理

信号处理由三种主要方式:

  • 默认:如果用户没有安装信号时,系统按照默认方式处理信号。
  • 自定义:用户可以自定义信号处理函数来执行特定的动作。
  • 忽略:接收到信号后不做任何反应。

默认处理方式有 5 种类型:忽略信号;终止进程;终止进程并生成核心转储(core)文件;停止进程;恢复进程。

信号安装

用户可以通过信号安装来自定义信号处理函数,信号安装有两种方式:

signal():不支持信号传递信息,主要用于非试试信号安装。

sigaction():支持信号传递信息,可用于所有信号安装。

函数原型:

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

struct sigaction {
	void (*sa_handler)(int);
	void (*sa_sigaction)(int, siginfo_t *, void *);
	sigset_t sa_mask;
	int sa_flags;
	void (*sa_restorer)(void);
}

sa_handler:简单信号处理函数

sa_sigaction:复杂信号处理函数(可以通过设置标志位)

sa_mask:信号集,用于设置在处理该信号时需要屏蔽的其他信号,处理完会接触屏蔽

sa_flags:标示位

  • SA_RESTART:使被信号打断的syscall重新发起。
  • SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
  • SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到SIGCHLD信号,这时子进程如果退出也不会成为僵 尸进程。
  • SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
  • SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
  • SA_SIGINFO:使用sa_sigaction成员而不是sa_handler作为信号处理函数。

sa_restorer:历史遗留字段,用于执行一个恢复函数,在一些架构上信号处理结束后需要调用特殊的恢复代码来恢复程序的上下文。

四、信号产生

信号进入未决信号集称之为注册,清除未决信号集中的信号称之为注销。

注册

每个进程的 task_struct 中都有一个成员 pending (类型为 strucct sigpending),保存当前进程所有已注册未处理的信号。

非实时信号:当非实时信号发送给进程时,如果这个信号已经存在于未决信号集(即已经注册过),系统不会再次注册,未决信号集中只会存在一份记录。

实时信号:实时信号支持多次注册,每次发送实时信号都会在未决信号集中增加一个记录,如果信号已经存在也会继续累加。

注销

当信号被处理或者清除时,信号就会注销。

非实时信号:当非实时信号注销时只需要从未决信号集中删除该信号。

实时信号:实时信号可以重复注册,因此未决信号集中可能保存多个同一信号对应 sigqueue 。只有当所有的 sigqueue 全部处理完毕后系统才会从未决信号集中将该信号删除。

信号产生的函数

int kill(pid_t pid, int sig);:用于向进程或进程组发送信号;

int sigqueue(pid_t pid, int sig, const union sigval value);:只能向一个进程发送信号,不能像进程组发送信号;主要针对实时信号提出,支持发送附加数据,与sigaction()组合使用,当然也支持非实时信号的发送;

unsigned int alarm(unsigned int seconds);:用于调用进程指定时间后发出SIGALARM信号;

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);:设置定时器,计时达到后给进程发送SIGALRM信号,功能比alarm更强大;

void abort(void);:向进程发送SIGABORT信号,默认进程会异常退出,通常产生 core dump。

int raise(int sig);:用于向进程自身发送信号;

代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>

// 如果是 C++ 编译,需要用 extern "C"
// extern "C" {
void my_signal_handler(int signo, siginfo_t *info, void *context) {
    printf("捕获到信号: %d\n", signo);
    if (info != NULL) {
        printf("信号来自进程的 PID: %d 的数据 %d\n", info->si_pid, info->si_int);
    }
}
// }

int main(void) {
    struct sigaction sa;
    // 清零结构体,确保所有字段都初始化为 0
    memset(&sa, 0, sizeof(sa));

    // 设置扩展信号处理函数
    sa.sa_sigaction = my_signal_handler;
    // 必须设置 SA_SIGINFO 标志,否则会按照 sa_handler 处理,可能导致类型不匹配
    sa.sa_flags = SA_SIGINFO;

    // 初始化信号屏蔽集
    sigemptyset(&sa.sa_mask);

    // 为 SIGINT 信号安装信号处理程序
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    printf("程序正在运行,按 Ctrl+C 发送 SIGINT 信号...\n");

    // 无限循环等待信号
    while (1) {
        sleep(1);
        union sigval val;
        val.sival_int = 1234;
        sigqueue(getpid(), SIGINT, val);        
    }

    return 0;
}

五、信号等待

Linux 信号等待是指进程在等待接收一个特定信号时所处的状态。在 Linux 系统中,进程可以通过系统调用 sigwait()sigsuspend() 来等待特定的信号。这些函数允许进程暂时挂起自己的执行,直到接收到指定的信号。

这两个函数在处理异步通信和信号处理时非常有用。通过使用信号等待函数,进程可以避免使用信号处理函数,而是在一个特定的时间点来处理信号。这种方式可以简化程序的逻辑和流程控制,避免了竞争条件和并发问题。

在等待信号时进程会进入 TASK_INTERRUPTIBLE (睡眠)状态,等待信号到来时唤醒。

常用的等待函数

1
int pause(void);

挂起当前进程直到接收一个信号,该信号可以是任意信号。

返回 -1 并且 errno 设置为 EINTER,表示被信号中断。

1
int sigsuspend(const sigset_t *mask);

挂起当前进程,直到接收指定的信号之一。该函数挂起之前会设置信号屏蔽,返回值恢复信号屏蔽。

mask:表示进程挂起期间应该屏蔽的信号。

返回 -1 并且 errno 设置为 EINTER,表示被信号中断。

1
int sigwait(const sigset_t *set, int *sig);

挂起当前进程,直到接收指定信号集中的任意一个信号

set:指定的信号集合。

sig:将接收到的信号存储到该指针中。

成功返回 0,失败返回 errno。

六、其他函数

sigset_t 信号集操作

1
2
3
4
5
6
7
8
9
10
// 清除信号集所有信号
int sigemptyset(sigset_t *set);				// 成功返回 0, 失败返回 -1
// 设置信号集中所有信号
int sigfillset(sigset_t *set);				// 成功返回 0, 失败返回 -1
// 设置信号集指定信号
int sigaddset(sigset_t *set, int signum);	// 成功返回 0, 失败返回 -1
// 清除信号集指定信号
int sigdelset(sigset_t *set, int signum);	// 成功返回 0, 失败返回 -1
// 判断信号集指定信号是否设置
int sigismember(const sigset_t *__set, int __signo);// 是返回 0, 否返回 -1

设置阻塞信号集

1
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

设置进程屏蔽信号集合。

how:信号屏蔽集的修改方式。可以取以下三个值之一:

SIG_BLOCK:将 set 添加到进程的当前信号屏蔽集中。

SIG_UNBLOCK:将 set 从进程的当前信号屏蔽集中移除。

SIG_SETMASK:将进程的当前信号屏蔽集设置为 set。

set:保存新的信号屏蔽集。

oldset:传出参数,保存之前信号屏蔽集。

成功返回 0, 失败返回 -1,设置 errno。

查看未决信号集

1
int sigpending(sigset_t *set);

获取当前未决信号集中的信号保存到 set 中。

成功返回 0, 失败返回 -1,并设置 errno。

获取信号描述

1
void psignal(int sig, const char *s);

获取 sig 信号的信号描述保存到 s 中。