欢迎光临
我们一直在努力

【Linux】信号产生后去了哪里?从 Pending、Block 到 Core Dump,讲清信号的保存

封面

🔥个人主页:爱和冰阔乐 📚专栏传送门:《数据结构与算法》 、C++ 🐶学习方向:C++方向学习爱好者 ⭐人生格言:得知坦然 ,失之淡然

在这里插入图片描述


🏠博主简介 在这里插入图片描述

文章目录

  • 前言
  • 一、三个概念:递达、未决、阻塞
  • 二、信号在内核中的表示:三张表
    • 2.1 拿着三张表过一遍图
    • 2.2 一个信号产生很多次怎么办?
  • 三、sigset_t:信号集操作函数
  • 四、sigprocmask:读取或更改信号屏蔽字
  • 五、sigpending:获取未决信号集
  • 六、动手实验:把三张表玩一遍
    • 6.1 把所有信号都屏蔽,进程就无敌了?
    • 6.2 解除屏蔽后发生了什么
    • 6.3 pending 是先清 0,还是先执行 handler?
  • 七、Term 和 Core:两种"死法"
    • 7.1 为什么从没见过 core 文件?
    • 7.2 查看、打开、使用
    • 7.3 waitpid 状态字里的 core dump 位
  • 总结

前言

上一篇讲了信号怎么产生。产生之后呢?信号不是立即处理的——进程要先把信号记下来,在合适的时候处理。那记在哪、怎么记、凭什么有的信号来了却"不处理"?

这一篇把信号的保存讲透:先厘清递达、未决、阻塞三个概念,再进内核看三张表,然后用 sigset_t、sigprocmask、sigpending 亲手改一遍这些表,最后说说 Term 和 Core 的区别——为什么云服务器上你从没见过 core 文件。


一、三个概念:递达、未决、阻塞

先补齐术语,后面全靠它们:

  • 递达(Delivery):实际执行信号的处理动作。递达方式三种:自定义、默认、忽略;
  • 未决(Pending):信号从产生到递达之间的状态。信号在位图里,就代表还没处理;
  • 阻塞(Block):进程可以选择阻塞某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除阻塞,才执行递达动作。

用小明写作业理解:数学老师布置作业(发信号),小明正在上课不能去图书馆(不能立即处理),先记下作业内容(保存信号)——作业一直是未决的。下课去写(递达)。要是小明一直忘了写,作业虽然被记下来了但一直没写,作业被阻塞了。直到舍友提醒"明天要交",小明才想起来去写——解除阻塞。

注意:阻塞和忽略是两个概念,别混。 阻塞是"收得到、存得下,但就是不递达";忽略是递达之后的一种处理动作。两者是前后关系:只有解除阻塞才能递达,忽略是递达的其中一种动作。 阻塞信号也叫屏蔽信号。

block 表还可以理解成进程的一张黑名单:凡是里面标记为 1 的信号,来了先挂起,暂时不处理,等解除阻塞再说。


二、信号在内核中的表示:三张表

在这里插入图片描述

进程与信号相关的数据不止一张位图,是三张表:

1. pending 表(未决表)——保存收到的信号的位图。比特位的位置是第几个信号,内容是是否收到。

2. block 表——本质也是位图。位置是第几个信号,内容是是否阻塞。

3. handler 表——函数指针数组。数组下标是信号编号,数组内容是该信号的处理方法(默认动作/忽略/自定义函数地址)。

拿 2 号信号画个示意(横着看):

信号编号: 1 2 3 4 …
pending: [ 0 | 1 | 0 | 0 | …] ← 2号信号已产生,未决中
block: [ 0 | 1 | 0 | 0 | …] ← 2号信号被阻塞
handler: [ 默认 | 自定义 | 默认 | 忽略 | …] ← 每个信号的处理方法

三张表怎么联动?判断一个信号能不能被递达:

pending 表 & (~block 表) → 哪些信号可以被递达

现在回头看 signal 函数就彻底通了:传入信号编号和方法,signal 拿编号做 handler 数组的下标,把函数地址填到对应位置——signal 的本质就是修改 handler 表。

这也解释了前言里那条结论:进程在信号还没产生时就知道怎么处理——方法早就躺在 handler 表里了。

那 handler 表里的 SIG_DFL、SIG_IGN 是啥?

#define SIG_DFL ((__sighandler_t) 0) // 默认
#define SIG_IGN ((__sighandler_t) 1) // 忽略

在这里插入图片描述

就是把 0 和 1 强转成函数指针类型,用特殊的指针值表达"默认"和"忽略",不是真的函数地址。

2.1 拿着三张表过一遍图

上图的例子逐个分析:

  • SIGHUP:未阻塞也未产生过,递达时执行默认处理动作;
  • SIGINT:产生过(pending 为 1),但正在被阻塞,暂时不能递达。虽然它的处理动作是忽略,但在解除阻塞之前连忽略都轮不到——进程仍有机会先改处理动作再解除阻塞;
  • SIGQUIT:未产生过,一旦产生将被阻塞,处理动作是自定义函数 sighandler。

每个信号都有两个标志位(阻塞 block、未决 pending)+ 一个函数指针(处理动作)。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除。

2.2 一个信号产生很多次怎么办?

如果在解除阻塞前,某信号产生了很多次?POSIX.1 允许系统递送一次或多次。Linux 的实现:常规信号在递达之前产生多次只计一次;实时信号可以依次排队。

普通信号好理解:父母喊你吃饭,喊一次和喊一百次,你记住的都是"要吃饭"这一件事,也只吃一次。


三、sigset_t:信号集操作函数

sigset_t 是 OS 提供的位图类型,能表示每个信号的"有效/无效"状态:在阻塞信号集里,有效 = 该信号被阻塞;在未决信号集里,有效 = 该信号处于未决状态。阻塞信号集也叫当前进程的信号屏蔽字(Signal Mask),这里的"屏蔽"要理解为阻塞而不是忽略。

用法上有一条铁律:sigset_t 内部的 bit 怎么存是系统实现的事,使用者不许直接解释它的内部数据——比如拿 printf 直接打印 sigset_t 变量没有任何意义。只能用下面这组函数操作:

int sigemptyset(sigset_t *set); // 信号集清空,全部置 0
int sigfillset(sigset_t *set); // 全部置 1
int sigaddset(sigset_t *set, int signo); // 第 signo 个比特位置 1
int sigdelset(sigset_t *set, int signo); // 第 signo 个比特位置 0
int sigismember(const sigset_t *set, int signo); // 判断 signo 是否在集合里

逐个说清楚:

  • sigemptyset:初始化 set 指向的信号集,所有信号的对应 bit 清零——该集合不包含任何有效信号;
  • sigfillset:初始化 set 指向的信号集,所有 bit 置位——有效信号包括系统支持的所有信号;
  • sigaddset:把某个信号加入集合,信号编号是几,第几个比特位就置 1;
  • sigdelset:从集合中删除某个信号,对应比特位置 0;
  • sigismember:布尔函数,判断集合的有效信号中是否包含某信号。

使用 sigset_t 变量之前,必须先调用 sigemptyset 或 sigfillset 初始化,让信号集处于确定的状态,之后再调 sigaddset 和 sigdelset 增删。前四个函数成功返回 0、出错返回 -1;sigismember 包含返回 1,不包含返回 0,出错返回 -1。

为什么必须初始化? sigset_t 只是栈上的一段空间,不初始化就是随机值——你根本不知道哪些信号"被包含"了。


四、sigprocmask:读取或更改信号屏蔽字

谁调用就是设置、获取、更新自己的 block 表:

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
// 成功返回 0,出错返回 -1

假设当前信号屏蔽字为 mask,how 三个选项:

  • SIG_SETMASK:用 set 整个替换 block 表(set = block);
  • SIG_BLOCK:把 set 里为 1 的信号新增进 block,相当于 mask |= set;
  • SIG_UNBLOCK:把 set 里这些信号从 block 里移除,对应比特位置 0,不再阻塞。

set 是用户传进来的信号集;oset 是输出型参数,把修改前的 block 表带出去——防止你改完后悔想恢复,留个备份。

在这里插入图片描述

参数组合的三种情况:

  • oset 非空、set 为 NULL:只读取当前的信号屏蔽字,通过 oset 传出;
  • set 非空、oset 为 NULL:按 how 的方式直接修改信号屏蔽字;
  • 两个都非空:先备份原来的屏蔽字到 oset,再按 set 和 how 修改——所以想"改完还能恢复",就两个都传,恢复时拿 oact 再 set 回去。

到这里可以下一个判断了:Linux 提供的信号操作,一定是围绕三张表展开的——signal 改 handler 表(上一篇已验证),sigprocmask 改 block 表,sigpending 读 pending 表。接口就这三个方向,没有别的。


五、sigpending:获取未决信号集

#include <signal.h>
int sigpending(sigset_t *set);
// 读取当前进程的未决信号集,通过 set 传出。成功返回 0,出错返回 -1

set 也是输出型参数。注意:系统只提供了获取 pending 的接口,没有提供直接修改 pending 的接口——想改 pending?用发信号那一套(kill、键盘、raise),让 OS 去改。

Linux 提供的这些信号操作,全是围绕三张表展开的:signal 改 handler 表(已验证),sigprocmask 改 block 表,sigpending 读 pending 表。


六、动手实验:把三张表玩一遍

场景:默认情况下进程不屏蔽任何信号(block 全 0)。我们只屏蔽 2 号信号,然后不停获取并打印 pending 表。

void PrintPending(sigset_t &pending)
{
printf("我是一个进程(%d),pending:", getpid());
for (int signo = 31; signo >= 1; signo)
{
if (sigismember(&pending, signo))
std::cout << 1;
else
std::cout << 0;
}
std::cout << "\\n";
}

int main()
{
// 1. 屏蔽 2 号信号
sigset_t block, oblock;
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block, SIGINT);
int n = sigprocmask(SIG_SETMASK, &block, &oblock);
(void)n;

// 2. 重复获取并打印 pending
while (true)
{
sigset_t pending;
int m = sigpending(&pending);
(void)m;
PrintPending(pending);
sleep(1);
}
}

跑起来后按 Ctrl+C:2 号信号进不了递达(被 block),但它确实产生了,于是 pending 表上 2 号位变成 1:

在这里插入图片描述

这就是"未决"的具象化:信号收到了、记下了,但没处理。

6.1 把所有信号都屏蔽,进程就无敌了?

把屏蔽 2 号改成屏蔽全部:

for (int i = 1; i < 32; i++)
sigaddset(&block, i);

在这里插入图片描述

kill -9 一发就死。9 号信号不能被捕捉,也不能被阻塞——所以它又叫管理员信号,是系统留给管理员的最后手段。

6.2 解除屏蔽后发生了什么

10 秒后解除对 2 号信号的屏蔽:

int cnt = 0;
while (true)
{
sigset_t pending;
sigpending(&pending);
PrintPending(pending);

if (cnt == 10)
{
// 恢复最初的 block(oblock 里存的是全 0)
sigprocmask(SIG_SETMASK, &oblock, nullptr);
std::cout << "解除对2号的屏蔽" << std::endl;
}
sleep(1);
cnt++;
}

在这里插入图片描述

现象:进程没打印"解除屏蔽",直接退出了。 因为解除屏蔽后 2 号信号立刻递达,默认动作是终止进程,连打印都来不及:

在这里插入图片描述

想看到完整流程,给 2 号注册自定义方法:

void handler(int sig)
{
std::cout << "递达" << sig << std::endl;
}
// main 里:signal(SIGINT, handler);

在这里插入图片描述

6.3 pending 是先清 0,还是先执行 handler?

一个很好的问题:pending 位从 1 变 0,是先递达再清 0,还是先清 0 再递达?

办法:在 handler 里再获取一次 pending。如果打印 0000 0010,说明处理完才清 0;如果打印 0000 0000,说明执行 handler 前就清了。

void handler(int sig)
{
std::cout << "递达" << sig << "信号!" << std::endl;
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
std::cout << "####################" << std::endl;
}

在这里插入图片描述

结论:当我们准备递达的时候,先清空 pending 对应的比特位(1→0),再执行 handler。


七、Term 和 Core:两种"死法"

进程收到信号的默认行为大多是终止,但终止分两种:Term 和 Core。

在这里插入图片描述

Core(核心转储):进程以 Core 模式退出时,会在当前路径下生成一个文件——进程异常退出时,把进程在内存中的核心数据拷贝到磁盘,这个机制叫核心转储(Core Dump),主要为了支持 debug。做完转储,进程退出。

Term:直接退出,不做任何转储。

7.1 为什么从没见过 core 文件?

浮点错误 SIGFPE 的默认行为就是 Core,可云服务器上跑除 0 从没见过文件:

int main()
{
printf("hello world\\n");
printf("hello world\\n");
printf("hello world\\n");
int a = 10;
a /= 0;
printf("hello world\\n");
}

在这里插入图片描述

因为云服务器的 core dump 功能默认被关掉了。为什么?实际公司项目很大,项目挂掉会一直重启,重启就转储,转储文件会一直写——磁盘直接被写满。生产环境要禁止一切可能,要 debug 去测试/开发环境。

7.2 查看、打开、使用

ulimit -a # 查看用户层设置

在这里插入图片描述

core file size 是 0,确实被禁了。临时打开:

ulimit -c 10240 # 给 core 文件大小设个上限

再跑除 0,当前路径下出现 core 文件:

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

为什么要核心转储?debug。以前查进程在哪崩的要一行行翻代码,现在 gdb 里 core-file core 直接定位到出错行:

在这里插入图片描述

core 文件里存的是进程崩溃那一刻的现场快照:进程的核心数据、调用栈、寄存器上下文——相当于事故现场的照片。gdb 拿着照片还原现场,直接指给你看崩在哪一行。

以后找不到 bug,可以试着开启 core dump,让程序跑崩,gdb 加载 core 文件定位出错行——事后调试。

顺便把 Ctrl+\\ 的账算了:它发 3 号 SIGQUIT,默认动作就是 Core。所以开完 core dump 功能后按 Ctrl+\\ 杀进程,当前目录下就会多一个 core 文件——这也是为什么它经常和 Core Dump 一起出现。

7.3 waitpid 状态字里的 core dump 位

进程控制那篇说过 status 是位图,其实它不止退出码和退出信号,还有一个 core dump 标志位:

在这里插入图片描述

int main()
{
pid_t id = fork();
if (id == 0)
{
sleep(2);
int a = 10;
a /= 0;
}
int status = 0;
waitpid(id, &status, 0);
printf("signal:%d, exit code:%d, core dump:%d\\n",
(status & 0x7f), (status >> 8) & 0xFF, (status >> 7) & 0x1);
}

在这里插入图片描述

  • 低 7 位:退出信号;
  • 次低 8 位:退出码;
  • 第 7 位:是否发生核心转储。

总结

一张图收尾:

信号产生

pending 表置 1(未决)

block 表对应位是 1?──是──→ 挡住,等解除阻塞
↓ 否
递达:先清 pending,再查 handler 表

handler 表 → 默认 / 忽略 / 自定义函数

默认动作里:Term 直接走,Core 先转储再走

  • 递达/未决/阻塞:未决是"记下了没处理",阻塞是"存得下但不递达",忽略是递达的一种动作;
  • 三张表:pending 记收到、block 记屏蔽、handler 存方法,pending & ~block 决定谁能递达;
  • signal 改 handler,sigprocmask 改 block,sigpending 读 pending;
  • 递达前先清 pending 再执行 handler;
  • 9 号信号不可捕捉不可阻塞,管理员的底线;
  • Term 与 Core 的区别是留不留"遗照",core 文件 + gdb = 事后调试。

信号什么时候被处理?处理自定义函数时进程在用户态还是内核态?sigaction、中断、用户态内核态切换,下一篇收尾。

三篇连着看效果最好,评论区聊聊你在生产环境见过 core 文件写满磁盘的事故吗?

资源分享: 【Linux】Ctrl+C 到底做了什么?从键盘、kill 到 alarm,一次讲清信号的产生 【Linux】共享内存为什么快?System V 的 key、shmget、shmat 与进程通信实战 【Linux】两个毫无关系的进程怎么通信?命名管道 FIFO 从原理到 Server/Client 实战

赞(0)
未经允许不得转载:171主机测评 » 【Linux】信号产生后去了哪里?从 Pending、Block 到 Core Dump,讲清信号的保存
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址