在 Linux C 编程中,信号集(Signal Set) 是用于批量管理信号的数据结构,核心作用是 “打包” 多个信号(如标记信号是否被阻塞、是否处于待处理状态),配合信号相关系统调用(如 sigprocmask、sigpending)实现对信号的精细化控制。
一、信号集的核心数据结构
Linux 中通过标准库定义的 sigset_t 类型表示信号集,其本质是一个 “位掩码”:每个信号对应 sigset_t 中的一个比特位,比特位为 1 表示信号在集合中,为 0 表示不在集合中。
- 定义位置:
<signal.h>头文件中(具体实现依赖系统,无需用户手动定义)。 - 用途限制:
sigset_t是 “opaque 类型”(抽象类型),禁止直接通过位运算操作,必须使用标准库提供的 “信号集操作函数” 修改和查询。
二、信号集的核心操作函数
所有信号集的操作都通过 <signal.h> 提供的标准函数实现,按功能可分为 “初始化 / 清空”“添加 / 删除信号”“查询信号” 三类,下表整理了常用函数:
|
函数原型 |
功能描述 |
返回值 |
|
|
初始化信号集 |
成功返回 0,失败返回 -1(设置 |
|
|
初始化信号集 |
成功返回 0,失败返回 -1(设置 |
|
|
将信号 |
成功返回 0,失败返回 -1( |
|
|
将信号 |
成功返回 0,失败返回 -1( |
|
|
判断信号 |
在集合中返回 1,不在返回 0,失败返回 -1 |
注意事项:
- 初始化必须优先:使用
sigaddset/sigdelset前,必须先调用sigemptyset或sigfillset初始化信号集,否则sigset_t的初始值不确定,操作会出错。 - 信号编号有效性:
signum必须是有效的信号编号(如SIGINT、SIGQUIT,范围1 ~ NSIG-1),不能是0或大于NSIG的值(NSIG是系统定义的最大信号编号)。
三、信号集的核心应用场景
信号集本身不直接处理信号,而是作为 “参数” 传递给系统调用,实现对信号的阻塞控制和待处理信号查询,这是信号集最核心的两个用途。
场景 1:信号阻塞(Signal Blocking)
Linux 内核为每个进程维护一个 “信号阻塞集(Signal Mask)”,若某个信号在阻塞集中,内核会暂时 “屏蔽” 该信号:信号触发后不会立即执行处理函数,而是标记为 “待处理(Pending)”,直到信号被从阻塞集中移除。
通过 sigprocmask 系统调用修改进程的信号阻塞集,语法如下:
#include <signal.h>
// how:控制阻塞集的修改方式
// set:新的信号集(若非NULL,按how修改阻塞集)
// oldset:保存原阻塞集(若非NULL,将修改前的阻塞集存入)
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
关键参数 how 的取值:
|
值 |
含义 |
|
|
将 中的信号 “添加” 到阻塞集(阻塞新信号) |
|
|
将 中的信号从阻塞集 “移除”(解除阻塞) |
|
|
将阻塞集 “替换” 为 (完全覆盖原阻塞集) |
场景 2:查询待处理信号(Pending Signals)
进程中被阻塞的信号触发后,会进入 “待处理状态”。通过 sigpending 系统调用可以查询当前进程的 “待处理信号集”,语法如下:
#include <signal.h>
// 将当前进程的待处理信号集存入 `set`
int sigpending(sigset_t *set);
返回值:成功返回 0,失败返回 -1(设置 errno)。
四、完整示例:信号集的实际使用
以下代码主要演示了信号(signal)的阻塞(block)与解除阻塞(unblock)机制,通过SIGINT信号(用户按下Ctrl+C触发)的处理,展示了如何控制信号在特定时间段内不被响应,以及何时允许响应。
#include <signal.h> // 信号处理相关函数(如signal、sigprocmask等)
#include <stdio.h> // 标准输入输出(但代码中未直接使用,主要用的是unistd.h的write)
#include <unistd.h> // 系统调用(如write、sleep)
#include <stdlib.h> // 标准库函数(如exit)
static void int_handle()
{
write(1, "123", 3); // 向标准输出(stdout)写入字符串"123"(长度3)
}
int main()
{
int i,j; // 循环控制变量
sigset_t set; // 信号集(用于存放一组信号,类似“信号容器”)
signal(SIGINT, int_handle); // 注册SIGINT信号的处理函数:按Ctrl+C时执行int_handle
/*
信号集操作函数
sigemptyset(&set); // 清空信号集
sigaddset(&set, SIGINT); // 将SIGINT信号加入信号集
sigdelset(&set, SIGINT); // 将SIGINT信号从信号集中删除
*/
sigemptyset(&set); // 清空信号集set(初始化为空,不包含任何信号)
sigaddset(&set, SIGINT); // 将SIGINT信号添加到信号集set中(现在set里只有SIGINT)
for (i = 0; i < 5; i++) // 外层循环5次,每次循环演示“阻塞信号→执行任务→解除阻塞”
{
/*
信号屏蔽函数
sigprocmask(SIG_BLOCK, &set, NULL); // 阻塞SIGINT信号
sigprocmask(SIG_UNBLOCK, &set, NULL); // 解除阻塞SIGINT信号
*/
sigprocmask(SIG_BLOCK, &set, NULL); // ① 阻塞SIGINT信号(不让SIGINT进来)
for (j = 0; j < 3; j++) // ② 执行“受保护任务”:打印3个*(耗时3秒)
{
write(1, "*", 1); // 打印1个星号*
sleep(1); // 休眠1秒(模拟任务执行时间)
}
write(1, "\n", 1); // 打印换行(3个*后换行)
sigprocmask(SIG_UNBLOCK, &set, NULL); // ③ 解除SIGINT阻塞(允许SIGINT进来)
}
exit(0);
}
/*
信号阻塞:通过sigprocmask(SIG_BLOCK, ...)可以“保护”一段代码不被特定信号打断,直到调用SIG_UNBLOCK解除阻塞后才处理信号。
• 不可靠信号特性:SIGINT是不可靠信号(不会排队),如果阻塞期间多次按Ctrl+C,解除阻塞后只会处理1次(打印1次"123")。
• 信号集作用:sigset_t、sigemptyset、sigaddset用于定义“需要阻塞的信号列表”,方便批量控制多个信号的阻塞状态。
*/
以下示例演示:使用信号集阻塞 SIGINT(Ctrl+C)信号,触发信号后查询待处理状态,最后解除阻塞并处理信号。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
// 信号处理函数(仅打印提示)
void sigint_handler(int signum) {
printf("\n收到 SIGINT 信号(编号:%d),已处理!\n", signum);
}
int main() {
sigset_t block_set, pending_set, old_set;
struct sigaction act;
// 1. 设置 SIGINT 的处理函数
act.sa_handler = sigint_handler;
sigemptyset(&act.sa_mask); // 处理信号时不阻塞其他信号
act.sa_flags = 0;
if (sigaction(SIGINT, &act, NULL) == -1) {
perror("sigaction 失败");
exit(1);
}
// 2. 初始化信号集:阻塞集中仅包含 SIGINT
sigemptyset(&block_set); // 清空信号集
sigaddset(&block_set, SIGINT); // 添加 SIGINT 到阻塞集
// 3. 修改进程阻塞集:阻塞 SIGINT,保存原阻塞集到 old_set
printf("开始阻塞 SIGINT(Ctrl+C),持续 5 秒...\n");
if (sigprocmask(SIG_BLOCK, &block_set, &old_set) == -1) {
perror("sigprocmask 失败");
exit(1);
}
// 4. 等待 5 秒:此期间按 Ctrl+C 会触发 SIGINT,但被阻塞(待处理)
sleep(5);
// 5. 查询待处理信号集
if (sigpending(&pending_set) == -1) {
perror("sigpending 失败");
exit(1);
}
// 判断 SIGINT 是否在待处理集中
if (sigismember(&pending_set, SIGINT)) {
printf("查询到:SIGINT 信号处于待处理状态\n");
} else {
printf("查询到:无待处理的 SIGINT 信号\n");
}
// 6. 解除阻塞:恢复原阻塞集(移除 SIGINT 的阻塞)
printf("开始解除 SIGINT 阻塞...\n");
if (sigprocmask(SIG_SETMASK, &old_set, NULL) == -1) {
perror("sigprocmask 恢复失败");
exit(1);
}
// 7. 解除阻塞后,待处理的 SIGINT 会立即被处理
printf("阻塞已解除,等待信号处理(按任意键退出)...\n");
getchar();
return 0;
}
示例运行流程:
- 程序启动后,先阻塞
SIGINT,并提示 “持续阻塞 5 秒”。 - 在 5 秒内按 Ctrl+C:
SIGINT被阻塞,不会立即执行处理函数。 - 5 秒后查询待处理信号:会显示 “
SIGINT处于待处理状态”。 - 解除阻塞后:待处理的
SIGINT会立即触发处理函数,打印 “收到信号”。
五、常见注意事项
- 不可阻塞的信号:
SIGKILL(9)和SIGSTOP(19)是内核强制信号,无法被阻塞、忽略或捕获,任何修改这两个信号的操作都会失败。 - 信号集的初始化:忘记调用
sigemptyset/sigfillset直接操作sigset_t,会导致未定义行为(如信号集初始值乱码)。 - 线程安全:
sigprocmask是 “进程级” 调用,在多线程程序中需使用pthread_sigmask(线程级信号阻塞集),避免影响其他线程。 - 待处理信号的生命周期:信号解除阻塞后,若存在待处理的该信号,内核会立即处理一次(即使该信号被多次触发,待处理状态也只会记录一次,即 “信号丢失”)。
1万+

被折叠的 条评论
为什么被折叠?



