本文收录于「流浪」的系列专栏系列专栏直达链接 Linux系统进入专栏 →⚙️ C进入专栏 → 数据结构与算法进入专栏 → Python进入专栏 → LangChain LangGraph进入专栏 →️ MySQL 数据库进入专栏 → Git 工具进入专栏 → 计算机网络进入专栏 → AI进入专栏 → 大厂面试、八股进入专栏 → 学习筑基专栏进入专栏 → 博客主页流浪 原创首发于 CSDN前言篇31 把被屏蔽的信号去哪了讲透了——它一直挂在 pending 位图里解除屏蔽才递达而且普通信号连发会丢。但生命周期还有半截没走完信号真递达之后进程是怎么结束的为什么有的信号退出时会吐出一个 core 文件有的却什么都不留凭什么说 kill -9 是最后手段、谁也拦不住本篇把这三个问号一次填平Term 与 Core 两种归宿、core dump 的完整实战流程、以及 9 号和 19 号为什么能绕过你设的一切防线。一、终止信号的两种归宿Term vs Core1.1 Term直接退出进程直接终止不留任何额外文件干干净净地消失。1.2 Core先把现场写进文件再退出进程异常退出时会先把自己在内存中的核心数据转储到磁盘形成一个文件然后才退出。这个文件就是 core 文件本质是进程临死前留下的一份现场快照。1.3 哪些信号会产 coreman 7 signal 实测按man 7 signal标准信号表默认动作是Core的一共10 个SIGABRTabort 触发、SIGBUS总线错误、SIGFPE算术异常如除零、SIGILL非法指令、SIGQUIT终端退出、SIGSEGV非法内存访问段错误、SIGSYS错误系统调用、SIGTRAP断点陷阱、SIGXCPUCPU 时间超限、SIGXFSZ文件大小超限。两点补充说明SIGIOT是SIGABRT的同义名、SIGUNUSED是SIGSYS的同义名不算独立信号。反直觉点SIGKILL(9)的默认动作是Term不是 Core——被kill -9杀掉的进程不会留下 core 文件。想留现场别用 9 号。1.4 core 文件里到底写了什么它不是随便 dump 一堆内存核心是三类内容寄存器上下文包括程序计数器PC和信号相关信息——这是能定位到死在哪一行的关键。进程内存中的核心数据把进程的内存映射转储到磁盘。信号相关信息是哪个信号、什么原因导致的终止。1.5 为什么要 core事后调试core 文件的全部价值就一句话直接帮我们定位到出错行。程序半夜崩了你不可能守在旁边有了 core 文件第二天照样能把当时的现场完整复现出来这叫事后调试post-mortem debugging。【衔接篇30 §6.1】篇30 讲过abort()为什么保证终止——它发的 SIGABRT 默认动作正是终止 core所以 abort 一定会留下 core 文件前提是 core dump 功能是打开的见下一节。1.6 父进程怎么知道孩子是怎么死的子进程被信号干掉后父进程waitpid()拿到的那个status不是随便一个整数它编码了死因。三个宏挨着用int status; waitpid(pid, status, 0); if (WIFEXITED(status)) // 正常退出exit / 从 main return printf(exit code %d\n, WEXITSTATUS(status)); else if (WIFSIGNALED(status)) { // 被信号杀死 printf(killed by signal %d\n, WTERMSIG(status)); if (WCOREDUMP(status)) // 仅在 WIFSIGNALED 为真时才能用 printf(并且留下了 core 文件\n); }按man 2 waitpidman-pages 6.19原文WIFSIGNALED在子进程被信号终止时返回真WTERMSIG返回导致终止的信号编号该宏只在 WIFSIGNALED 返回真时使用WCOREDUMP返回子进程是否产出了 core dump同样只在 WIFSIGNALED 为真时使用手册中该宏标注为 POSIX.1-2024。这一组和本篇主题咬得很紧WCOREDUMP为真就说明这个信号的默认动作属于 Core 那一类1.3 那 10 个之一而且 core dump 功能是开着的——比你跑到目录里去找 core 文件可靠得多。同样的编码规则在 shell 里也看得到echo $?打出139就是128 11表示进程被 11 号 SIGSEGV 干掉了bash(1) 对命令被信号 N 杀死的退出状态约定。1.7 回头重读篇16 那张 status 位图core 就记在第 8 位回顾 wait/waitpid 的 status 位图正常终止高 8 位存放退出码低 7 位全 0信号杀死低 7 位存放终止信号第 8 位是 core dump 标记低 7 位WTERMSIG(status)保存终止信号 (1~31)低 7 位全 0 代表进程不是被信号终止此时高 8 位的退出码才有效。第 8 位core dump 标志。仅当子进程触发 Core 类信号、并且成功生成 core 文件时置 1WCOREDUMP就是读取这一位。宏必须配合判断WIFSIGNALED使用status 有两套解析逻辑。进程正常终止就读高 8 位被信号杀死就读低 7 位。不做分支判断会把正常退出误判为死于 0 号信号。glibc 源码位掩码定义bits/waitstatus.h#define __WEXITSTATUS(status) (((status) 0xff00) 8) //高8位退出码 #define __WTERMSIG(status) ((status) 0x7f) //低7位终止信号 #define __WCOREDUMP(status) ((status) __WCOREFLAG) //第8位core标记 #define __WCOREFLAG 0x80代码演示int main() { pid_t pidfork(); if(pid0) { std::cout我是一个子进程getpid()我即将退出std::endl; exit(1); } int statu; waitpid(pid,statu,0); printf(我是父进程收到了子进程的退出信息signal: %d, exit code: %d, core dump: %d\n, (statu 0x7F), (statu 8) 0xFF, (statu 7) 0x1); return 0; }二、core dump 实战怎么开、怎么用、为什么默认关2.1 先看看当前开没开ulimit -a在输出里找core file size这一项。默认值通常是 0——意思是允许生成的 core 文件大小为 0也就是等于关闭。所以很多人踩过这个坑程序明明段错误崩了却死活找不到 core 文件。2.2 怎么打开ulimit -c 4096 # 放开到 4096 个 block ulimit -c unlimited # 或者干脆不限制大小注意ulimit是shell 内建命令只对当前 shell 及其子进程生效重开终端就恢复默认。要永久生效得写进配置文件。再次被core信号终止后 就会有core文件了2.3 core 文件的命名规则早期 / 简单配置就叫core生成在当前工作目录。RHEL 7 及多数现代发行版默认core.PID带上进程号避免多个进程互相覆盖。2.4 想自己定路径和名字core_patterncore.PID只是发行版给的默认值真正说了算的是/proc/sys/kernel/core_pattern。按man 5 core该文件自 Linux 2.6 与 2.4.21 起默认值是 core可以写成一个模板模板里的%占位符会被替换占位符含义依据 man 5 core%p被 dump 进程的 PID%e进程/线程的 comm 值通常就是可执行文件名不带路径最长截到 15 字符%E可执行文件路径其中的 / 替换成 !自 Linux 3.0%s导致 dump 的信号编号%tdump 发生时间Epoch 秒数%u / %g被 dump 进程的实际 UID / GID%h主机名比如写成/var/coredump/core-%e-%p-%t所有 core 就统一落到/var/coredump下还自带进程名、PID 和时间戳。注意两点模板里可以带 /会被当成目录分隔符生成的文件名最长128 字节Linux 2.6.19 之前是 64 字节。还有一个更狠的用法模板以|开头时自 Linux 2.6.19core 不再落成文件而是作为标准输入喂给一个用户态程序——systemd-coredump、abrt 这些就是靠它接管 core 的。手册给了几条硬约束程序必须用绝对路径且紧跟在|后面该程序以 root 身份运行并且走管道的 core dump 不受 RLIMIT_CORE 限制——也就是说ulimit -c 0拦不住它。这也解释了为什么有些机器上你明明没开 core却还是能在 systemd 的日志里翻到崩溃记录。如何配置让core形成在当前目录下# 1. 停止apport sudo service apport stop # 2. core文件输出到当前目录 sudo sysctl -w kernel.core_patterncore.%p # 3. 检查配置 cat /proc/sys/kernel/core_pattern ulimit -c顺带把 2.3 那个core.PID的来龙去脉补完/proc/sys/kernel/core_uses_pid非 0 时自 Linux 2.4若core_pattern里没有%p内核就会在文件名后面追加上.PID。2.5 怎么用它调试gdb 可执行文件 core.PID # 一步到位 # 或者先起 gdb 再加载 gdb ./a.out (gdb) core-file core.PID # 加载 core 文件加载之后 gdb 会直接告诉你进程是被哪个信号干掉的、死在哪一行、当时的调用栈长什么样。这就是 1.5 说的定位到出错行。进去以后最常用的几条命令(gdb) bt # 打调用栈直接看死在哪个函数调用链上 (gdb) frame 0 # 切到栈顶那一帧出事的那个函数 (gdb) list # 列出当前行附近的源码 (gdb) print 变量名 # 看崩溃那一刻这个变量的值 (gdb) info registers # 看寄存器x86-64 上重点看 rip就是 PC其中bt是性价比最高的一条——core 调试里绝大多数场景靠它就能定位到出错的函数和行号后面几条是用来回答为什么会错的。还有一个前提别漏编译时必须带-g。没调试信息的二进制加载 core只能看到地址和函数名看不到行号——这也是很多线上程序崩了却调不出行号的原因发布版本把调试信息 strip 掉了。2.6 云服务器上为什么默认把它关掉两个原因缺一不可磁盘core 文件可能非常大它装的是整个进程的内存镜像。线上服务一崩就吐几个 G几个来回就能把磁盘写满进而拖垮同一台机器上的其他服务。安全core 里是进程内存的完整快照可能包含密码、密钥、用户数据等敏感信息。文件一旦落到磁盘就多了一条泄露通道。对应的控制位置/etc/security/limits.confPAM 限制和 systemd 的DefaultLimitCORE0。想在生产环境开 core dump通常是定向开、限额开并且放在受控目录里。三、终极手段SIGKILL / SIGSTOP 为什么拦不住3.1 一个灵魂问题如果一个病毒进程启动后第一件事就是把所有信号全部屏蔽那我们不就拿它没辙了3.2 答案9 号和 19 号不受屏蔽影响不会没辙。man 7 signal里写得明明白白SIGKILL 和 SIGSTOP不可被捕获、不可被阻塞、不可被忽略原文cannot be caught, blocked, or ignored。大家可以执行下面代码 尝试先不加if判断 看看会有什么效果void hander(int sig) { std::cout收到了一个信号 sig 我可以被捕捉 std::endl; } int main() { for(int i0;i32;i) { signal(i,hander); } for(int i0;i32;i) { sleep(1); if(i9||i19) continue; raise(i); } return 0; }所以不管你把 blocked 位图填满成什么样kill -9 照样能把它送走。3.3 底层原因内核不信任你的 blocked 集【衔接篇30 §6.5】篇30 已经给过这个结论并做过9 号杀不掉后台进程的误区澄清本篇补的是为什么内核在投递信号时对这两个信号直接绕过用户 blocked 位图的检查走强制投递路径。sigprocmask能改你的 blocked 位图不假但内核在 9/19 上根本不查这张表——你改了也白改。这是操作系统刻意留的一道后门设计必须保证管理员永远握有终止失控进程的最后手段否则一个恶意进程只要屏蔽全部信号就能永久霸占 CPU整个系统就失控了。3.4 完整名单只有两个真正拦不住的只有SIGKILL(9)和SIGSTOP(19)这两个其余所有信号都可以被屏蔽或捕获。这也是为什么 9 号被称为最后手段——它是唯一保证生效的终止方式代价见 1.3它不产 core杀完不留现场。3.5 什么时候 kill -9 也杀不掉先把话说准9 号拦不住指的是进程没法通过屏蔽、捕获、忽略来躲不等于它能在任何状态下立刻生效。下面两种情况你敲kill -9就是看不到进程消失僵尸进程Z 状态它已经死了只是父进程还没wait()给它收尸内核留着它的 task_struct 只是为了给父进程一个交代。你没法杀死一个已经死的进程——正确做法是处理它的父进程结束或修复父进程的 wait 逻辑让它被 init/systemd 收养后回收。不可中断睡眠D 状态进程卡在内核态等某个 IO典型是坏掉的 NFS 挂载、故障磁盘。这个状态存在的意义就是信号打不断否则数据一致性没法保证。信号照样被记进 pending但要等它从 D 状态里出来才会被处理——所以 9 号对它不是无效是被延后。怎么判断ps -o pid,stat,comm -p PIDSTAT 列看到Z是僵尸看到D是卡在内核态 IO。所以运维排查杀不掉的进程第一步永远是先看 STAT 那一列而不是反复加 9。进阶补充容易答漏的一层严格来说D 状态一定杀不死也不绝对。内核还有一种TASK_KILLABLE睡眠它只认致命信号也就是 SIGKILLps 里同样显示成D。所以准确说法是看它卡在哪种等待上——普通不可中断等待要等 IO 完成可杀等待才会被 9 号提前唤醒。四、小结两句话收住本篇Term 与 Core 是信号递达后进程的两种归宿Term 干干净净退出、什么也不留Core 会先把寄存器上下文含 PC、进程内存中的核心数据、信号相关信息落盘成 core 文件再退出这是事后调试的黑匣子。但它默认是关的——ulimit -c为 0 等于关闭云上更因为磁盘和安全两重考虑默认不吐真要用/proc/sys/kernel/core_pattern才是定路径和名字的总开关。SIGKILL(9) 与 SIGSTOP(19) 是内核刻意留的后门投递时直接绕过用户 blocked 位图的检查sigprocmask 改了也白改保证管理员永远握有终止失控进程的最后手段。代价是 9 号默认动作是 Term 不是 Core杀完不留现场而且它只对活着且在被调度的进程有效僵尸和 D 状态这两类它照样带不走。【四篇串起来看】篇29 讲信号是什么、从哪来、发给谁 → 篇30 讲怎么存位图、怎么发只有 OS 能改、怎么收handler 三选一 → 篇31 讲被屏蔽时去哪了、会不会丢 → 本篇讲递达后进程怎么死、什么信号拦不住。四篇同属一条主线对照着看最清楚。五、面试官爱问带答案5.1 core 文件里记录了什么为什么能定位到出错行答core 文件里主要是三类——寄存器上下文含 PC、进程内存中的核心数据、信号相关信息。因为保存了PC 和调用栈gdb 加载后能直接还原死在哪一行这就是事后调试的价值。5.2 程序明明崩了为什么看不到 core 文件答最常见的原因是 core dump 功能没开——ulimit -c显示core file size为 0等于关闭。用ulimit -c 4096或unlimited打开即可另外还要注意 ulimit 只对当前 shell 及子进程生效以及 core 生成路径是否有写权限。5.3 云服务器上为什么默认关闭 core dump答两个原因。一是磁盘core 文件是进程内存的完整镜像可能非常大频繁崩溃会写满磁盘进而影响同机其他服务二是安全内存快照里可能含有密码、密钥、用户数据等敏感信息落盘就多一条泄露通道。5.4 SIGKILL 为什么拦不住内核是怎么做到的答内核对 SIGKILL(9) 和 SIGSTOP(19)直接绕过用户 blocked 位图的检查做强制投递——sigprocmask改得了 blocked 位图但内核在这两个信号上根本不查这张表。这是 OS 刻意保留的后门保证管理员永远有终止失控进程的最后手段。真正拦不住的就这两个信号。5.5 core 文件的名字和路径由什么决定答由/proc/sys/kernel/core_pattern决定自 Linux 2.6 与 2.4.21默认值 core。它支持%p(PID)、%e(进程名)、%t(时间)、%s(信号号) 等占位符也允许带/指定目录core.PID这种形态是core_uses_pid非 0 时的追加行为。若模板以|开头自 Linux 2.6.19core 会作为标准输入交给用户态程序处理且不受 RLIMIT_CORE 限制。5.6 kill -9 也有杀不掉的时候吗答有。9 号拦不住是指进程没法屏蔽/捕获它不代表任何状态下都立刻生效。僵尸进程Z已经死了只是父进程没收尸杀它没意义得处理父进程不可中断睡眠D卡在内核态 IO 上信号只能先记在 pending 里等它出来属于延后而非无效。排查时先看ps的 STAT 列。遇到不一样的结果或者没看懂的点评论区直接说。也欢迎把本篇和篇29信号一、篇30信号二、篇31信号三串起来复习——四篇是同一条主线拆成的。
