Linux系统(信号篇)信号的保存
本节重点:
- 信号未决与信号递达
- 进程如何保存收到的信号信息
- sigset_t类型
- 信号集的操作函数
- 三个关键结构:handler,block,pending
一、相关概念
信号递达:实际执行信号的处理动作
信号未决:信号从产生到递达之间的状态
进程可以选择阻塞某个信号,被阻塞的信号在产生后会一直处于未决状态直到进程解除对该信号的阻塞,此时信号才会被递达。
二,在内核中的表示
2.1 内核结构
我们知道在Linux系统中每一个进程都对应唯一一个PCB用来存储和描述该进程的一系列参数。在信号方面,PCB有三个关键的相关字段:
- handler字段:指向
signal_struct
结构体,存储进程共享的信号处理信息(如信号处理函数)。 blocked
字段:类型为sigset_t
,表示进程当前阻塞的信号集合(即暂时不处理的信号)。pending
字段:全称为 struct sigpending 其中包含未决信号的位图,类型为sigset_t。
我们也可以将三个字段简单理解为存储在进程PCB中描述信号的三张表,pinding表用来描述进程是否收到了信号,blocked表则描述该进程主动阻塞的信号而handler表则记录相关信号的处理函数,当进程收到某一信号时首先记录在pending表中,当确定该型号没有被进程阻塞时系统会选择在合适的时间将该信号按照handler表中记录的函数方法进行递达。
注意:信号是否被进程阻塞与进程是否收到该信号没有关系,即使pending位图中明确表示进程未收到相应信号,blocked位图中也可能说明该信号已被阻塞。
2.2 sigset_t类型
在 Linux 系统编程中,sigset_t
(也称作信号基)是一种用于表示信号集合的数据类型,主要用于信号掩码(signal mask)和未决信号(pending signals)的管理。它在内核和用户空间中都扮演着关键角色。
sigset_t
本质是一个位图(bitmap),每一位代表一个信号是否存在。其具体实现因系统而异,但通常有以下特点:
- 位数:通常为 64 位或 128 位,足以覆盖所有标准信号(Linux 中最大信号编号为 64)。
- 数据结构:可能是一个整数类型(如
unsigned long
)或结构体,具体取决于系统架构。 - 头文件:在用户空间编程中,需包含
<signal.h>
。
在内核中,sigset_t
的实现与用户空间类似,但更贴近硬件架构,在x86_64 架构中通常使用 unsigned long
数组,每个元素 64 位,可覆盖 64 个信号。
2.3 信号集操作函数
在 Linux 系统中,信号集操作函数是用于管理信号集合(sigset_t
类型)的核心工具。这些函数允许程序创建、修改和查询信号集合,从而实现对信号的精确控制。它是编写健壮、可靠的多任务程序的基础。通过合理使用这些函数,可以避免信号竞争条件、实现精确的信号处理逻辑,并提高程序的稳定性。
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
sigemptyset
sigemptyset
是 POSIX 标准中用于操作信号集(sigset_t
)的核心函数之一,其主要作用是将信号集初始化为空集,即不包含任何信号。
函数原型:
#include <signal.h>int sigemptyset(sigset_t *set);
参数:
set
指向要初始化的信号集(sigset_t
类型)。
返回值:
成功返回 0
,失败返回 -1
并设置 errno
(通常不会失败,除非 set
为无效指针)。
功能与用途:
在使用信号集前,必须使用sigemptyset进行初始化,否则信号集内容未定义。通常先调用 sigemptyset
创建空集,再通过 sigaddset
逐个添加需要的信号。
示例:创建一个只包含 SIGINT
和 SIGTERM
的信号集:
sigset_t set;
sigemptyset(&set); // 初始化为空集
sigaddset(&set, SIGINT); // 添加 SIGINT(Ctrl+C)
sigaddset(&set, SIGTERM); // 添加 SIGTERM(kill 默认信号)
sigset_t
本质是一个位图(bitmap),每位对应一个信号。sigemptyset
的实现通常是将所有位清零。
signfillset
sigfillset
是 POSIX 标准中用于操作信号集(sigset_t
)的核心函数之一,其主要作用是将信号集初始化为包含所有信号的集合。
函数原型:
#include <signal.h>int sigfillset(sigset_t *set);
参数:
set
指向要初始化的信号集(sigset_t
类型)。
返回值:
成功返回 0
,失败返回 -1
并设置 errno
(通常不会失败,除非 set
为无效指针)。
功能与用途:
- 初始化信号集:将信号集的所有位设置为
1
,表示包含系统支持的所有信号。 - 快速构建掩码:常用于需要临时阻塞所有信号的场景(如关键代码段)。
示例:阻塞所有可阻塞的信号
sigset_t block_all;
sigfillset(&block_all); // 包含所有信号
sigprocmask(SIG_BLOCK, &block_all, NULL); // 阻塞所有可阻塞的信号
与 sigemptyset
的对比:
sigfillset
:填充所有信号(所有位为 1)。sigemptyset
:清空所有信号(所有位为 0)。
sigaddset
sigaddset
是 POSIX 标准中用于操作信号集(sigset_t
)的核心函数之一,其主要作用是将特定信号添加到信号集中。
函数原型:
#include <signal.h>int sigaddset(sigset_t *set, int signum);
参数解析:
set
:指向要修改的信号集(sigset_t
类型)。signum
:要添加的信号编号(如SIGINT
、SIGTERM
)。
返回值:
- 成功:返回
0
。 - 失败:返回
-1
并设置errno
(如signum
无效)。
示例:创建一个包含 SIGINT
和 SIGTERM
的信号集:
sigset_t set;
sigemptyset(&set); // 初始化为空集
sigaddset(&set, SIGINT); // 添加 SIGINT(Ctrl+C)
sigaddset(&set, SIGTERM); // 添加 SIGTERM(kill 默认信号)
注意事项:
signum
必须是有效信号编号(1 到_NSIG-1
)。- 无效编号会导致
sigaddset
返回-1
并设置errno
为EINVAL
。 SIGKILL
和SIGSTOP
无法被阻塞,但sigaddset
不会报错。- 多次添加同一信号不会产生额外影响(位图对应位已为
1
)。
sigdelset
sigdelset
是 POSIX 标准中用于操作信号集(sigset_t
)的核心函数之一,其主要作用是将特定信号从信号集中移除。
函数原型:
#include <signal.h>int sigdelset(sigset_t *set, int signum);
参数解析:
set
:指向要修改的信号集(sigset_t
类型)。signum
:要移除的信号编号(如SIGINT
、SIGTERM
)。
返回值:
- 成功:返回
0
。 - 失败:返回
-1
并设置errno
(如signum
无效)。
示例:创建一个包含除 SIGINT
外所有信号的集合:
sigset_t set;
sigfillset(&set); // 初始化为包含所有信号
sigdelset(&set, SIGINT); // 移除 SIGINT(Ctrl+C)
sigset_t
本质是一个位图(bitmap),每位对应一个信号。sigdelset
的实现通常是将对应位置为 0。
sigismember
sigismember
是 POSIX 标准中用于操作信号集(sigset_t
)的核心函数之一,其主要作用是检查特定信号是否存在于信号集中。
函数原型:
#include <signal.h>int sigismember(const sigset_t *set, int signum);
参数解析:
set
:指向要检查的信号集(sigset_t
类型)。signum
:要查询的信号编号(如SIGINT
、SIGTERM
)。
返回值:
- 信号存在:返回
1
。 - 信号不存在:返回
0
。 - 出错(如
set
无效):返回-1
并设置errno
。
示例:验证信号集是否包含 SIGINT
:
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);if (sigismember(&set, SIGINT)) {printf("SIGINT is in the set.\n");
} else {printf("SIGINT is NOT in the set.\n");
}
sigset_t
c是一个位图(bitmap),每位对应一个信号。sigismember
的实现通常是检查对应位是否为 1。
sigprocmask
igprocmask
是 POSIX 标准中用于控制进程信号掩码(signal mask)的核心系统调用,其主要作用是阻塞或解除阻塞特定信号,实现对信号的精细控制。
函数原型:
#include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数解析:
how
:操作类型,可选值:。
how 参数 | 效果(mask 为当前掩码) | 典型场景 |
---|---|---|
SIG_BLOCK | mask = mask ∪ set (新增阻塞) | 临时屏蔽某些信号 |
SIG_UNBLOCK | mask = mask ∩ (~set) (解除阻塞) | 恢复对某些信号的阻塞 |
SIG_SETMASK | mask = set (完全覆盖) | 彻底替换当前掩码 |
set
:指向信号集(sigset_t
)的指针,指定要操作的信号。若为 NULL
,则忽略 how
参数,仅将当前掩码保存到 oldset
中。
oldset
:若不为 NULL
,则保存修改前的信号掩码,用于后续恢复。
返回值:
- 成功:返回
0
。 - 失败:返回
-1
并设置errno
(如EINVAL
表示how
参数无效)。
核心机制:
- 信号掩码是前面我们提到的blocked位图,记录了当前被进程阻塞的信号。
- 被掩码阻塞的信号不会被进程处理,而是处于 未决(pending)状态,直到掩码解除。
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中⼀ 个信号递达。
注意:SIGKILL
(9)和 SIGSTOP
(19)无法被阻塞,sigprocmask
对它们无效。
代码示例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>int main() {sigset_t new_mask, old_mask;// 初始化信号集,添加 SIGINT(Ctrl+C)和 SIGTERMsigemptyset(&new_mask);sigaddset(&new_mask, SIGINT);sigaddset(&new_mask, SIGTERM);// 阻塞 SIGINT 和 SIGTERM,并保存旧掩码if (sigprocmask(SIG_BLOCK, &new_mask, &old_mask) == -1) {perror("sigprocmask");return 1;}printf("SIGINT and SIGTERM are now blocked. Try sending them...\n");sleep(5); // 在此期间,信号会被阻塞但不会被处理// 检查未决信号(已发送但被阻塞的信号)sigset_t pending;sigpending(&pending);if (sigismember(&pending, SIGINT)) {printf("SIGINT was pending during the block.\n");}// 恢复旧掩码(解除阻塞)if (sigprocmask(SIG_SETMASK, &old_mask, NULL) == -1) {perror("sigprocmask");return 1;}printf("Signals are unblocked. Exiting in 2 seconds...\n");sleep(2);return 0;
}
sigpending
sigpending
是 POSIX 标准中用于查询进程未决信号(pending signals)的系统调用,其核心作用是获取当前已发送但被进程阻塞的信号集合。
函数原型:
#include <signal.h>int sigpending(sigset_t *set);
参数解析
set
:指向 sigset_t
类型的指针,用于存储未决信号集。
返回值:
- 成功:返回
0
,并将未决信号集写入set
。 - 失败:返回
-1
,并设置errno
(如EFAULT
表示set
指针无效)。
内核原理:
sigpending
的内核实现主要是将进程的 pending
信号集复制到用户空间:
// 伪代码:sigpending 的内核实现
int sys_sigpending(sigset_t *set) {struct task_struct *p = current; // 当前进程sigset_t pending = p->pending.signal; // 获取未决信号集if (copy_to_user(set, &pending, sizeof(sigset_t))) {return -EFAULT; // 复制失败}return 0;
}
代码示例:
#include<iostream>
#include<signal.h>
static int i=0;
void Printpend(sigset_t pending)
{for(int i=31;i>0;i--){if(sigismember(&pending,i)==1){std::cout<<"1 ";}else{std::cout<<"0 ";}}std::cout<<"["<<i<<" s]"<<std::endl;i++;
}void handler(int sign)
{std::cout<<sign<<" 号信号被递达 "<<std::endl;
}
int main()
{std::cout<<getpid()<<"号进程处理过程"<<std::endl;//首先将2号信号进行自定义捕捉:signal(2,handler);//将2号信号进行屏蔽(阻塞)sigset_t old,block;sigemptyset(&old);sigemptyset(&block);sigaddset(&block,2);sigprocmask(SIG_SETMASK,&block,&old);//在15秒内打印pending信号集,15秒后解除阻塞递达信号int cnt=15;while(1){//获取当前的pending信号集:sigset_t pending;sigemptyset(&pending);sigpending(&pending);//打印当前获取的pending:Printpend(pending);cnt--;if(cnt==0){//解除对2号信号的屏蔽(阻塞):sigprocmask(SIG_SETMASK,&old,&block);}sleep(1);}return 0;
}
在这里我们首先将2号信号自定义捕捉后再进行阻塞(屏蔽)并设置计时器15秒后解除屏蔽(阻塞)。之后不断获取并打印未决信号集。在15秒内发送2号信号给该进程时由于信号被阻塞会处于未决信号集(pending位图的第2位为1),15秒后解除阻塞2号信号立即被递达执行自定义捕捉此时我们打印出来的pending位图的第2为又变成了0。