在前两篇文章中,我们已经认识了 Linux 信号,并介绍了信号的各种产生方式。
我们知道,信号可以通过终端按键、系统调用、硬件异常以及软件条件等方式产生。但信号产生之后,并不一定会被进程立即处理。
例如:
- 进程可能正在执行关键代码;
- 进程可能暂时屏蔽了某些信号;
- 多个信号可能在短时间内同时到达。
那么问题来了:
当一个信号产生后,而进程暂时无法处理它时,操作系统会将这个信号保存到哪里?
为了回答这个问题,本节将深入探讨 Linux 信号的保存机制,包括信号的未决(Pending)状态、信号集以及相关的数据结构。
1 信号的保存
1.1 引入信号的相关常见概念
• 实际执行信号处理动作的过程称为信号递达。
理解:信号被进程执行的时候称为信号递达,如默认处理、忽略、自定义捕捉。
• 信号从产生到递达之间的状态称为信号未决。
理解:信号已经产生,并且已经被进程记录下来,但尚未被进程处理,此时该信号处于未决状态。
• 进程可以选择阻塞某个信号。
理解:当一个信号被阻塞后,即使该信号已经产生,也不会立即递达给进程,而是先保持在未决状态中,等待解除阻塞后再递达。
• 阻塞和忽略是不同的
理解:阻塞:阻止信号递达,信号会暂时保存在未决集中。忽略:允许信号递达,但递达后的处理动作是不做任何处理。
1.2 信号在内核中的表示

对于一个进程而言,内核维护了一组与信号相关的数据结构,其中包括 pending 表、block 表以及 handler 表,用于记录信号是否产生、是否被阻塞以及如何处理。其中 block 表和 pending 表都是位图结构,handler 表是一个函数指针数组。
block 表:比特位的位置:信号的编号,比特位的状态:1 表示阻塞,0 表示未阻塞
pending 表:比特位的位置:信号的编号,比特位的状态:1 表示对应编号的信号处于未决状态,0 表示对应编号的信号未处于未决状态
handler 表:数组下标:信号的编号,数组的内容:信号的处理方式。handler 表记录每种信号对应的处理方式,可以是默认处理(SIG_DFL)、忽略(SIG_IGN)或用户自定义处理函数。
理解信号产生 -> 信号保存 -> 信号处理的过程由于某种事件的发送,OS 得知并处理后产生对应的信号,OS 在进程控制块中(task_struct)设置该信号的未决标志(将对应的比特位设置为 1),将该信号保存下来,如果该信号未被阻塞,进程将在合适的时候处理该信号(内核会先将pending表中对应的比特位设置为 0 ,然后执行对应的处理动作)。
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1 允许操作系统产生该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。
**信号的保存本质上就是:
- pending 表记录哪些信号已经产生
- block 表记录哪些信号暂时不能递达
- handler 表记录信号应该如何处理
内核通过这三张表完成信号从产生到处理的全过程管理。**
cpp
// 内核结构
struct task_struct
{
...
/* signal handlers */
struct sighand_struct *sighand;
sigset_t blocked;
struct sigpending pending;
...
}
struct sighand_struct
{
atomic_t count;
struct k_sigaction action[_NSIG]; // #define _NSIG 64
spinlock_t siglock;
};
struct __new_sigaction
{
__sighandler_t sa_handler;
unsigned long sa_flags;
void (*sa_restorer)(void); /* Not used by Linux/SPARC */
__new_sigset_t sa_mask;
};
struct k_sigaction
{
struct __new_sigaction sa;
void __user *ka_restorer;
};
/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);
struct sigpending
{
struct list_head list;
sigset_t signal;
};
1.3 sigset_t
由于未决标志和阻塞标志本质上都是按照信号编号组织的位图,因此 Linux 使用统一的数据类型 sigset_t 来表示这类信号集合(Signal Set,信号集)。
sigset_t 本质上是一个位图结构,每一个 bit 对应一种信号:
- bit 为 1,表示该信号在当前信号集中存在;
- bit 为 0,表示该信号在当前信号集中不存在。
在未决信号集(Pending Set)中:
- 1 表示该信号处于未决状态;
- 0 表示该信号不处于未决状态。
在阻塞信号集(Block Set)中:
- 1 表示该信号被阻塞;
- 0 表示该信号未被阻塞。
阻塞信号集也称为当前进程的信号屏蔽字(Signal Mask)。这里的"屏蔽"应理解为阻塞,而不是忽略。被屏蔽的信号不会立即递达,但仍然会保存在 Pending 集合中;而忽略属于信号递达后的一种处理动作。
cpp
typedef __sigset_t sigset_t;
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
1.4 sigset_t 操作函数
sigset_t 类型使用位图的方式记录信号状态,每一种信号对应一个 bit 位,用于表示该信号在当前信号集中是否存在。
至于这些 bit 位在内存中具体如何组织和存储,则由系统实现决定。对于使用者而言,无需关心 sigset_t 的内部实现细节,而应该通过系统提供的接口来操作它。
因此,我们通常使用 sigemptyset、sigemptyset、sigaddset、sigdelset、sigismember 等函数对信号集进行管理,而不应该直接访问或解释 sigset_t 内部的数据。
换句话说,使用者只需要将 sigset_t 看作一个抽象的数据类型,而不必关心其底层存储结构。例如,直接使用 printf 打印一个 sigset_t 变量通常是没有意义的,因为其内部表示方式并不是标准化的,且可能因系统实现而异。
1.4.1 sigemptyset
cpp
#include <signal.h>
int sigemptyset(sigset_t *set);
功能:将信号集初始化为空集,即不包含任何信号。 本质是将所有信号的 bit 位全部置为 0。
1.4.2 sigfillset
cpp
#include <signal.h>
int sigfillset(sigset_t *set);
功能:将信号集初始化为全集,即包含所有信号。本质是将所有信号的 bit 位全部置为 1。
1.4.3 sigaddset
cpp
#include <signal.h>
int sigaddset(sigset_t *set, int signum);
功能:向信号集中添加一个指定信号。本质是将对应信号编号的 bit 位置置为 1。
1.4.4 sigdelset
cpp
#include <signal.h>
int sigdelset(sigset_t *set, int signum);
功能:向信号集中删除一个指定信号。本质是将对应信号编号的 bit 位置置为 0。
这四个函数的返回值都是成功返回 0,失败返回 -1。
1.4.5 sigismember
cpp
#include <signal.h>
int sigismember(const sigset_t *set, int signum);
功能:判断指定信号是否存在信号集中。本质是判断指定信号对应的 bit 位置是否为 1。
返回值:指定信号存在返回 1,不存在返回 0,出错返回 -1。
注意,在使用 sigset_ t 类型的变量之前,一定要调用 sigemptyset 或 sigfillset 做初始化,使信号集处于确定的状态。初始化 sigset_t 变量之后就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种信号。
1.5 sigprocmask
cpp
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:检查或修改当前进程的信号屏蔽字(阻塞信号集、block 表)。本质是修改 block 位图。
参数说明:
how:修改方式
SIG_BLOCK:添加阻塞信号
SIG_UNBLOCK:解除阻塞信号
SIG_SETMASK:直接替换信号屏蔽字
set:要操作的信号集
oldset:输出型参数,保存原来的信号屏蔽字,如果不使用可以传 NULL。
返回值:成功返回 0,失败返回 -1。
1.6 sigpending
cpp
#include <signal.h>
int sigpending(sigset_t *set);
功能:获取当前进程中的未决信号集(pending 表)。本质是读取 pending 位图。
参数说明:set:输出型参数,用于保存当前进程的 pending 表。
返回值:成功返回 0,失败返回 -1。
1.7 样例:巩固所学的信号集相关的函数
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
// 打印 pending 表
void PrintPending(sigset_t& pending)
{
std::cout << "pending 表: ";
for (int i = 31; i >= 1; --i)
{
if (sigismember(&pending, i))
{
std::cout << 1;
}
else
{
std::cout << 0;
}
}
std::cout << std::endl;
}
void handler(int signum)
{
std::cout << signum << "号信号未决" << std::endl;
std::cout << "--------------------------" << std::endl;
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
std::cout << "--------------------------" << std::endl;
}
int main()
{
// 捕捉 2 号信号
signal(SIGINT, handler);
// 屏蔽 2 号信号
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
sigprocmask(SIG_SETMASK, &block_set, &old_set);
int cnt = 10;
while (true)
{
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
sleep(1);
--cnt;
if (cnt == 0)
{
std::cout << "2号信号阻塞解除" << std::endl;
sigprocmask(SIG_SETMASK, &old_set, nullptr);
}
}
return 0;
}