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

📅 2026/8/18 20:48:25
【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是啥#defineSIG_DFL((__sighandler_t)0)// 默认#defineSIG_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 变量没有任何意义。只能用下面这组函数操作intsigemptyset(sigset_t*set);// 信号集清空全部置 0intsigfillset(sigset_t*set);// 全部置 1intsigaddset(sigset_t*set,intsigno);// 第 signo 个比特位置 1intsigdelset(sigset_t*set,intsigno);// 第 signo 个比特位置 0intsigismember(constsigset_t*set,intsigno);// 判断 signo 是否在集合里逐个说清楚sigemptyset初始化 set 指向的信号集所有信号的对应 bit 清零——该集合不包含任何有效信号sigfillset初始化 set 指向的信号集所有 bit 置位——有效信号包括系统支持的所有信号sigaddset把某个信号加入集合信号编号是几第几个比特位就置 1sigdelset从集合中删除某个信号对应比特位置 0sigismember布尔函数判断集合的有效信号中是否包含某信号。使用 sigset_t 变量之前必须先调用 sigemptyset 或 sigfillset 初始化让信号集处于确定的状态之后再调 sigaddset 和 sigdelset 增删。前四个函数成功返回 0、出错返回 -1sigismember 包含返回 1不包含返回 0出错返回 -1。为什么必须初始化sigset_t 只是栈上的一段空间不初始化就是随机值——你根本不知道哪些信号被包含了。四、sigprocmask读取或更改信号屏蔽字谁调用就是设置、获取、更新自己的 block 表#includesignal.hintsigprocmask(inthow,constsigset_t*set,sigset_t*oset);// 成功返回 0出错返回 -1假设当前信号屏蔽字为 maskhow 三个选项SIG_SETMASK用 set 整个替换 block 表set blockSIG_BLOCK把 set 里为 1 的信号新增进 block相当于 mask | setSIG_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获取未决信号集#includesignal.hintsigpending(sigset_t*set);// 读取当前进程的未决信号集通过 set 传出。成功返回 0出错返回 -1set 也是输出型参数。注意系统只提供了获取 pending 的接口没有提供直接修改 pending 的接口——想改 pending用发信号那一套kill、键盘、raise让 OS 去改。Linux 提供的这些信号操作全是围绕三张表展开的signal 改 handler 表已验证sigprocmask 改 block 表sigpending 读 pending 表。六、动手实验把三张表玩一遍场景默认情况下进程不屏蔽任何信号block 全 0。我们只屏蔽 2 号信号然后不停获取并打印 pending 表。voidPrintPending(sigset_tpending){printf(我是一个进程(%d),pending:,getpid());for(intsigno31;signo1;signo--){if(sigismember(pending,signo))std::cout1;elsestd::cout0;}std::cout\n;}intmain(){// 1. 屏蔽 2 号信号sigset_t block,oblock;sigemptyset(block);sigemptyset(oblock);sigaddset(block,SIGINT);intnsigprocmask(SIG_SETMASK,block,oblock);(void)n;// 2. 重复获取并打印 pendingwhile(true){sigset_t pending;intmsigpending(pending);(void)m;PrintPending(pending);sleep(1);}}跑起来后按 CtrlC2 号信号进不了递达被 block但它确实产生了于是 pending 表上 2 号位变成 1这就是未决的具象化信号收到了、记下了但没处理。6.1 把所有信号都屏蔽进程就无敌了把屏蔽 2 号改成屏蔽全部for(inti1;i32;i)sigaddset(block,i);kill -9 一发就死。9 号信号不能被捕捉也不能被阻塞——所以它又叫管理员信号是系统留给管理员的最后手段。6.2 解除屏蔽后发生了什么10 秒后解除对 2 号信号的屏蔽intcnt0;while(true){sigset_t pending;sigpending(pending);PrintPending(pending);if(cnt10){// 恢复最初的 blockoblock 里存的是全 0sigprocmask(SIG_SETMASK,oblock,nullptr);std::cout解除对2号的屏蔽std::endl;}sleep(1);cnt;}现象进程没打印解除屏蔽直接退出了。因为解除屏蔽后 2 号信号立刻递达默认动作是终止进程连打印都来不及想看到完整流程给 2 号注册自定义方法voidhandler(intsig){std::cout递达sigstd::endl;}// main 里signal(SIGINT, handler);6.3 pending 是先清 0还是先执行 handler一个很好的问题pending 位从 1 变 0是先递达再清 0还是先清 0 再递达办法在 handler 里再获取一次 pending。如果打印0000 0010说明处理完才清 0如果打印0000 0000说明执行 handler 前就清了。voidhandler(intsig){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 从没见过文件intmain(){printf(hello world\n);printf(hello world\n);printf(hello world\n);inta10;a/0;printf(hello world\n);}因为云服务器的 core dump 功能默认被关掉了。为什么实际公司项目很大项目挂掉会一直重启重启就转储转储文件会一直写——磁盘直接被写满。生产环境要禁止一切可能要 debug 去测试/开发环境。7.2 查看、打开、使用ulimit-a# 查看用户层设置core file size是 0确实被禁了。临时打开ulimit-c10240# 给 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 标志位intmain(){pid_t idfork();if(id0){sleep(2);inta10;a/0;}intstatus0;waitpid(id,status,0);printf(signal:%d, exit code:%d, core dump:%d\n,(status0x7f),(status8)0xFF,(status7)0x1);}低 7 位退出信号次低 8 位退出码第 7 位是否发生核心转储。总结一张图收尾信号产生 ↓ pending 表置 1未决 ↓ block 表对应位是 1──是──→ 挡住等解除阻塞 ↓ 否 递达先清 pending再查 handler 表 ↓ handler 表 → 默认 / 忽略 / 自定义函数 ↓ 默认动作里Term 直接走Core 先转储再走递达/未决/阻塞未决是记下了没处理阻塞是存得下但不递达忽略是递达的一种动作三张表pending 记收到、block 记屏蔽、handler 存方法pending ~block决定谁能递达signal 改 handlersigprocmask 改 blocksigpending 读 pending递达前先清 pending 再执行 handler9 号信号不可捕捉不可阻塞管理员的底线Term 与 Core 的区别是留不留遗照core 文件 gdb 事后调试。信号什么时候被处理处理自定义函数时进程在用户态还是内核态sigaction、中断、用户态内核态切换下一篇收尾。三篇连着看效果最好评论区聊聊你在生产环境见过 core 文件写满磁盘的事故吗资源分享【Linux】CtrlC 到底做了什么从键盘、kill 到 alarm一次讲清信号的产生【Linux】共享内存为什么快System V 的 key、shmget、shmat 与进程通信实战【Linux】两个毫无关系的进程怎么通信命名管道 FIFO 从原理到 Server/Client 实战