目录
[1. 信号集的初始化与屏蔽设置](#1. 信号集的初始化与屏蔽设置)
[2. 未决信号的查询与打印](#2. 未决信号的查询与打印)
[问题 1:"我可以将所有的信号都进行屏蔽,信号不就不会被处理了吗?"](#问题 1:“我可以将所有的信号都进行屏蔽,信号不就不会被处理了吗?”)
[问题 2:"sigset_t 是在哪里开辟的空间?"](#问题 2:“sigset_t 是在哪里开辟的空间?”)
[问题 3:"未决信号的本质是什么?"](#问题 3:“未决信号的本质是什么?”)
[场景 1:关键逻辑的 "原子性" 保障](#场景 1:关键逻辑的 “原子性” 保障)
[场景 2:批量处理未决信号](#场景 2:批量处理未决信号)
在 Linux 系统编程中,信号是进程间通信和系统事件通知的重要机制。今天我们就以一段关于 ** 信号屏蔽(sigprocmask)和未决信号(sigpending)** 的代码为切入点,深入剖析这两个概念的底层逻辑与实际应用。
一、背景:为什么要研究信号屏蔽与未决信号?
在多任务、多事件的系统环境中,进程可能会同时收到多个信号。但有些场景下,我们希望暂时屏蔽某些信号 (比如在执行关键逻辑时,不希望被干扰);同时,我们也需要知道有哪些信号被屏蔽后处于 "待处理" 状态 (即未决信号)。这两个机制 ------sigprocmask和sigpending,就是用来解决这些问题的关键工具。
二、代码拆解:从示例看核心逻辑
先看我们的示例代码,它主要做了两件事:屏蔽所有 1-31 号信号 ,并循环打印当前的未决信号集合。
1. 信号集的初始化与屏蔽设置
运行
cpp
sigset_t bset, oset;
sigemptyset(&bset);
sigemptyset(&oset);
for (int i = 1; i <= 31; i++)
{
sigaddset(&bset, i); // 将1-31号信号加入屏蔽集
}
sigprocmask(SIG_SETMASK, &bset, &oset);
sigset_t:是 Linux 中用于表示信号集合的数据结构,本质上是一个位图(每一位对应一个信号)。sigemptyset:初始化一个空的信号集合(所有位设为 0)。sigaddset:将指定信号加入集合(对应位设为 1)。sigprocmask:核心系统调用 ,用于修改进程的信号屏蔽字 (即进程当前屏蔽哪些信号)。SIG_SETMASK表示用bset完全替换当前的屏蔽字,oset用于保存旧的屏蔽字(以便后续恢复)。
2. 未决信号的查询与打印
运行
cpp
sigset_t pending;
while (true)
{
int n = sigpending(&pending); // 获取当前进程的未决信号集合
if (n < 0)
continue;
PrintPending(pending); // 打印未决信号的位图
sleep(1);
}
void PrintPending(sigset_t &pending)
{
for (int signo = 31; signo >= 1; signo--)
{
if (sigismember(&pending, signo)) // 判断信号是否在未决集合中
cout << "1";
else
cout << "0";
}
cout << "\n\n";
}
sigpending:获取进程的未决信号集合(即被屏蔽且已经发送到进程,但尚未被处理的信号)。sigismember:判断某个信号是否在信号集合中。- 打印逻辑:从 31 号到 1 号信号依次判断,输出 "1" 表示该信号未决,"0" 表示未未决,这样就能直观看到未决信号的位图状态。
三、关键问题解答:穿透代码看本质
问题 1:"我可以将所有的信号都进行屏蔽,信号不就不会被处理了吗?"
回答:大部分信号会被屏蔽,但有例外!
在 Linux 中,9号信号(SIGKILL)和19号信号(SIGSTOP)是不可屏蔽 的。这是系统级的安全设计 ------ 比如SIGKILL用于强制终止进程,若能被屏蔽,进程就可能无法被杀死,导致系统资源泄漏;SIGSTOP用于暂停进程,同理需要保证其强制性。
所以,即使代码中尝试屏蔽所有 1-31 号信号,SIGKILL和SIGSTOP仍然可以突破屏蔽,作用于进程。
问题 2:"sigset_t 是在哪里开辟的空间?"
回答:在用户栈上,属于进程的用户空间。
sigset_t bset, oset;是在main函数中定义的局部变量,因此存储在进程的用户栈 中(属于用户空间内存)。当调用sigprocmask时,内核会读取用户空间中bset的值,进而修改内核中进程task_struct里的信号屏蔽字。
问题 3:"未决信号的本质是什么?"
回答:未决信号是 "被屏蔽且已送达" 的信号的临时存储。
每个进程的task_struct中,有一个未决信号集合 (pending)和一个信号屏蔽字(mask)。当一个信号被发送到进程时:
- 若该信号未被屏蔽(mask 中对应位为 0),则进程会立即处理(执行默认动作或自定义处理器);
- 若该信号被屏蔽 (mask 中对应位为 1),则会被记录到未决集合(pending 中对应位置 1),直到屏蔽被解除,才会被处理。
四、扩展:信号屏蔽与未决的典型应用场景
场景 1:关键逻辑的 "原子性" 保障
在执行一段不希望被信号打断的关键逻辑时(比如数据库事务、文件写入的关键步骤),可以先屏蔽信号,执行完逻辑后再解除屏蔽:
运行
cpp
sigset_t oldmask, newmask;
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT); // 屏蔽Ctrl+C信号(2号)
sigprocmask(SIG_BLOCK, &newmask, &oldmask); // 保存旧屏蔽字,屏蔽SIGINT
// 执行关键逻辑...
sigprocmask(SIG_SETMASK, &oldmask, NULL); // 恢复旧屏蔽字,解除SIGINT屏蔽
场景 2:批量处理未决信号
有时我们希望 "积攒" 一批信号,然后一次性处理。可以先屏蔽信号,等业务逻辑完成后,查询未决信号并逐个处理:
运行
cpp
// 屏蔽信号
sigset_t mask, oldmask;
sigfillset(&mask); // 填充所有信号(除不可屏蔽的)
sigprocmask(SIG_SETMASK, &mask, &oldmask);
// 执行业务逻辑,期间收到的信号会进入未决状态...
// 查询并处理未决信号
sigset_t pending;
sigpending(&pending);
for (int signo = 1; signo <= 31; signo++)
{
if (sigismember(&pending, signo))
{
cout << "处理未决信号:" << signo << endl;
// 这里可以调用信号处理函数
}
}
// 恢复屏蔽字
sigprocmask(SIG_SETMASK, &oldmask, NULL);
五、总结
通过这段代码和对原理的剖析,我们可以清晰地理解:
sigprocmask是进程 "主动控制哪些信号可以打扰自己" 的工具;sigpending是进程 "查看有哪些被屏蔽的信号在排队等待处理" 的窗口;- 信号屏蔽与未决机制,是 Linux 系统中进程对信号 "精细管控" 的核心手段,在系统编程、服务器开发等场景中有着广泛应用。
希望这篇博客能帮大家穿透代码,真正理解信号屏蔽与未决的底层逻辑~