
1. 从内核喊你收消息说起进程信号到底是什么你有没有遇到过这种场景一个程序跑着跑着突然就崩了终端提示Segmentation fault (core dumped)或者按CtrlC想中断一个死循环的任务程序却纹丝不动又或者写了个网络服务某个客户端断开连接服务进程直接收到SIGPIPE悄悄死掉。这些听起来像是神秘事件的背后都是 Linux 的进程信号在起作用。信号本质上是内核主动通知进程发生了一个异步事件的一种机制你可以把它理解成手机上收到一条弹窗通知——不管你在干什么只要有事件发生内核就会把这条通知塞给你然后由你决定怎么处理忽略、执行默认动作或者自己写一个处理函数来应对。对做服务端开发、嵌入式开发或者还在学校啃《Linux 系统编程》的人来说信号几乎是绕不过去的一道坎。它不像是函数调用那样你按顺序执行完再来的同步流程而是随时可能插入到你正在跑的某行代码中间的异步机制。数据结构里的锁、条件变量你可能已经很熟但信号这块儿的异步、不可控、信号处理函数里啥都不能乱调的脾气很多人容易踩坑。这篇文章我会从信号的核心机制讲起逐个拆解signal、kill、raise、abort、alarm这五个最常用的信号接口再把 Core Dump 核心转储机制和硬件中断的关系讲透最后把我在实际开发中踩过的一些坑和排查思路一并整理出来。内容尽量做到能落地——每一段都有命令、有代码、有注意点拿到就能用。2. 信号机制的整体设计五步理解信号的一生2.1 信号的产生方式从硬件到软件的四条路径在写代码之前先想想信号到底是怎么来的。信号来源大致有四类来自键盘的终端信号比如你在终端按下CtrlC产生SIGINT、Ctrl\产生SIGQUIT。这是大多数人对信号的第一印象——一个中断当前前台进程的快捷键组合。来自硬件异常比如进程访问了非法内存地址CPU 触发异常内核把异常翻译成SIGSEGV发给进程执行了非法指令则发出SIGILL浮点除零发出SIGFPE。这就是硬件中断和进程信号之间最直接的联系——硬件异常是触发源信号是内核丢给用户进程的事故通知单。来自软件主动调用这就是本篇文章的主角们。kill(pid, sig)、raise(sig)、abort()、alarm()都是用户态主动发送信号的方式。来自运行环境/软件条件比如管道写端被读端关闭时进程收到SIGPIPE定时器到点后收到SIGALRM子进程退出时父进程收到SIGCHLD。理解信号来源的最大意义在于排查问题的时候你可以根据信号类型倒推这个进程到底经历了什么。比如我调试过一个诡异的服务进程崩溃问题最后发现是运行环境的看门狗超时触发了SIGTERM而不是代码本身出现 bug——从信号入手它一下给排查锁定了方向。2.2 信号的标准流程产生、注册挂起、递达、处理信号并非产生后马上被处理那么简单。一个信号的一生要经过四个阶段产生信号被某个源头生成出来。注册/挂起如果信号尚未递送到进程内核会在进程的pending信号集合里做标记。注意对于标准信号同一类信号在未递达之前只会保留一个标记——就算你连续发了 10 次SIGUSR1进程醒来也只会处理一次。这一点和高等级的实现比如后来 POSIX 化之后出现的实时信号有本质区别。递达内核在恰当时机通常是进程从内核态返回用户态之前检查信号集合决定把信号递交给进程。处理进程执行三类动作之一——忽略、默认动作、或者调用通过signal()/sigaction()注册的自定义处理函数也叫捕获信号。而默认动作又分几种终止进程、终止并产生 core 文件、忽略信号、暂停进程、恢复进程。光是这个默认动作矩阵就值得记一张表信号默认动作常见触发场景SIGINT终止进程终端按 CtrlCSIGQUIT终止并核心转储终端按 Ctrl\SIGKILL终止进程不可捕获、不可忽略kill -9SIGSEGV终止并核心转储非法内存访问SIGPIPE终止进程写一个读端已关闭的管道SIGALRM终止进程alarm 定时器到点SIGCHLD忽略子进程状态变化SIGSTOP暂停进程kill -STOP不可捕获注意SIGKILL和SIGSTOP这两个兄弟很特殊内核直接处理用户进程没有机会拦截。这话很多书都写但很多人遇到kill -9杀不掉的进程时还是不信邪——那不可能是被你程序捕获了而是进程处于不可中断的睡眠状态比如在内核态卡在某些 IO 上属于另一个完全不相关的话题。2.3 为什么建议用sigaction()而不是signal()标题里把signal放在首位但实际上在严肃的服务端项目里我几乎不会直接用signal()注册信号处理器。原因比较现实signal()在不同 Unix 衍生系统上语义有差异在某些实现中信号处理函数执行完之后该信号的处置方式会被重置为默认行为这意味如果在处理函数里二次注册不够及时同一信号再次来临时就可能直接把进程干掉。另外signal()无法设置信号屏蔽字也无法带上SA_RESTART这类标志导致某些系统调用read、write、accept等在收到信号后会被中断返回EINTR错误处理起来非常难受。所以我在这篇文章里的代码示例会刻意偏重sigaction()只是在讲解信号概念时把signal()当作入口。这不是劝退 API而是帮你少走弯路。下面讲到实操时会给出sigaction()的标准写法。3. signal() 与捕获信号的实操细节3.1 用 signal() 实现第一个信号捕获程序先从最直观的写法下手看一个简单的例子捕获SIGINT让程序在按CtrlC时不必立刻退出而是打印一条消息。#include stdio.h #include stdlib.h #include signal.h #include unistd.h void handle_sigint(int sig) { // 注意printf 在信号处理函数里其实不是完全安全的选择 // 简单 demo 可以这么用实际项目里要用异步信号安全的 write() printf(捕获到 SIGINT信号编号: %d\n, sig); } int main(void) { signal(SIGINT, handle_sigint); for (int i 0; ; i) { printf(运行中 %d 秒...\n, i); sleep(1); } return 0; }编译后跑起来按CtrlC程序不会退出而是打印提示后继续运行。你可能会想那我怎么杀它——只能另开终端用kill -9 pid因为SIGKILL是不允许被捕获的。这里要特别说一句上面代码里的printf只是拿来演示。严格来说printf并不是异步信号安全函数如果在信号处理函数里调用它而这个函数恰好打断了主流程里正在执行printf的同一份内部缓冲区操作可能导致状态错乱甚至死锁。生产级代码里信号处理函数里只应该使用write()、_exit()这类异步信号安全函数。3.2 sigaction() 的完整姿势与 SA_RESTART 之谜再来看sigaction()的正确用法。以捕获SIGINT为例#include stdio.h #include stdlib.h #include signal.h #include string.h #include unistd.h volatile sig_atomic_t g_flag 0; void handle_sigint(int sig) { // 只设置一个标志位主循环里轮询检查 // sig_atomic_t 保证读写是原子的 g_flag 1; } int main(void) { struct sigaction sa; memset(sa, 0, sizeof(sa)); sa.sa_handler handle_sigint; sigemptyset(sa.sa_mask); // 处理信号期间不额外屏蔽其他信号 sa.sa_flags 0; // 先不设置 SA_RESTART观察系统调用中断现象 if (sigaction(SIGINT, sa, NULL) -1) { perror(sigaction); exit(EXIT_FAILURE); } printf(等待信号按 CtrlC 试试...\n); while (!g_flag) { pause(); // 挂起进程等待任何信号 } printf(收到信号主循环退出\n); return 0; }这段代码里有两个关键点volatile sig_atomic_t类型的变量用来在信号处理函数和主流程之间传标志。这个类型保证了对它的读写是原子的不会被主流程的赋值打断产生撕裂问题。pause()会让进程挂起直到捕获到一个信号。这是信号编程里非常经典的写法。现在来说SA_RESTART。如果你在一个阻塞的read()上等待数据此时一个信号过来默认行为是让read()返回-1同时置errno为EINTR被中断。很多初学者会在这里懵掉为什么我的程序莫名其妙read失败其实只是被信号打扰了。解决办法是在sa_flags里设置SA_RESTART内核会在信号处理函数返回后自动重新启动被中断的系统调用对应用层来说就像信号没发生过一样。sa.sa_flags SA_RESTART;但注意SA_RESTART并不能覆盖所有系统调用。有些调用如poll、epoll_wait、accept在某些场景依然可能返回EINTR所以严谨的写法是循环重试例如while (1) { ssize_t n read(fd, buf, sizeof(buf)); if (n -1 errno EINTR) { continue; } if (n -1) { // 真正的错误 } break; }这是我在实际服务端代码里几乎处处可见的一段逻辑。别偷懒EINTR必须显式处理。3.3 sa_mask 与信号屏蔽的连锁反应sigaction结构体里的sa_mask定义的是当某个信号的处理函数正在执行时临时屏蔽哪些信号。比如你希望在处理SIGINT期间如果来了SIGUSR1先别打扰那就把它加进sa_mask。注意这个机制不是永久屏蔽当前正在处理的信号类型——同一信号在处理期间会被阻塞但处理函数返回后自动解除。这也引出一个重要现象同一个信号如果反复快速产生处理函数不会递归执行而是等待当前处理完成后再去处理下一个未决信号并且因为标准信号只保留一个未决标记所以经常丢掉中间几次。这一点想深入理解可以做个小实验给 SIGINT 的处理函数里加长延时循环然后用kill -INT pid连发 5 次你会发现处理函数只串行执行了一次或几次并不是发多少次就处理多少次。理解了未决信号集只记一笔之后对信号的理解就上了一个台阶。4. kill、raise、abort、alarm信号发送家族逐一拆解4.1 kill(2)不只是杀死而是发信号kill()这个函数名极具迷惑性——好像是专门用来杀掉进程的但它的本质是向指定进程发送任意信号。在 Linux 系统里int kill(pid_t pid, int sig);pid 0发送给指定的进程 ID。pid 0发送给调用者同进程组的所有进程。pid -1发送给所有有权限发送的进程慎用我见过有同事用它做测试结果整个终端会话的进程全被信号影响场面一度不可收拾。pid -1发送给进程组 ID 为-pid的所有进程。在运维场景里最常用的自然是kill -TERM pid和kill -KILL pid。KILL是最后手段而TERM会先通知进程你该收拾东西走了给进程一个优雅关闭的机会比如释放资源、落盘状态。我曾经写过一个服务启动脚本里一律用kill -TERM然后在服务里注册了SIGTERM的处理函数执行平滑退出——先停止接收新请求再等待处理中的请求结束最后关闭数据库连接池。这套逻辑让线上服务的重启零停机。再看kill()的返回值。如果进程不存在返回-1并设errno为ESRCH如果没权限返回-1且errno是EPERM。在脚本里判断进程是否存活用kill(pid, 0)是一个常见技巧——信号 0 表示只做检查不真正发信号可以用来探测进程是否存在。4.2 raise(3)自己给自己发信号raise(sig)等价于kill(getpid(), sig)区别不过是一个封装。但它有一个特殊价值在单线程程序里raise 一定会让信号被当前线程处理而kill(getpid(), sig)在多线程环境中信号会发送给进程内任意一个没有被阻塞该信号的线程处理线程不可控。那raise到底什么时候用我的实践是在程序里模拟一种外部事件发生的信号用来驱动测试逻辑。比如写单元测试时想测试SIGUSR1处理器能不能正确改变状态机就可以raise(SIGUSR1)手动触发。在后台服务里给自己发SIGTERM做自我终止。某些管理脚本会这么用比如进程内部发现配置严重错误决定自我了断。示例#include stdio.h #include signal.h int main(void) { printf(我自己给自己发 SIGUSR1...\n); raise(SIGUSR1); // 默认动作是终止进程所以这条语句后面的代码不会执行 printf(这行不会打印\n); return 0; }默认情况下SIGUSR1会终止进程所以如果没有注册处理函数程序会在这里安静地挂掉。这也是很多人在测试时踩到的第一个坑用完raise(SIGUSR1)没注册对应 handler程序直接退出还半天找不到原因。4.3 abort(3)最粗暴的原地自爆abort()的行为是先解除SIGABRT的阻塞然后向自己发送SIGABRT。如果该信号没有被捕获或者处理函数返回进程会终止并且大概率产生 core 文件前提是 core 限制允许。abort()的语义是异常终止。它和我们自己raise(SIGABRT)有一个细微差别abort()在发送信号之前会先把SIGABRT从阻塞状态解除确保信号一定能够递达。另外在核心转储机制生效时abort()产生的SIGABRT默认会导致 core dump方便事后用 gdb 查看崩溃现场。很多项目中会把assert宏和abort()绑定起来。C 标准库里的assert(expr)在判定失败时会打印错误信息并调用abort()。所以你在测试阶段看程序崩出一个Aborted (core dumped)时基本就能推测是某个断言没过。但需要注意线上环境千万不要滥用abort()因为它不是优雅退出不给你清理资源的机会。响应外部请求失败可以返回错误真遇到不可恢复的致命问题再考虑它。4.4 alarm(2)延时炸弹到点自动引爆alarm()是一个简单但非常有用的定时器接口unsigned int alarm(unsigned int seconds);它让内核在seconds秒后向当前进程发送SIGALRM。返回值是之前未到期的闹钟还剩多少秒如果之前没有未到期的闹钟则返回 0。从 POSIX 规范看SIGALRM的默认动作是终止进程。所以如果你设置了alarm(3)却忘了注册处理函数程序三秒后就会被终止。但实践中更常见的用法是配合一个自定义 handler实现超时控制#include stdio.h #include signal.h #include unistd.h #include errno.h void timeout_handler(int sig) { // 处理超时比如设置一个标志让主流程退出 write(STDOUT_FILENO, timeout expired\n, 16); } int main(void) { struct sigaction sa; sa.sa_handler timeout_handler; sa.sa_flags 0; sigemptyset(sa.sa_mask); sigaction(SIGALRM, sa, NULL); alarm(3); // 3 秒后闹钟到期 printf(等待闹钟...\n); int n 0; do { n read(STDIN_FILENO, NULL, 0); // 故意用一个会阻塞的调用模拟等待 } while (n -1 errno EINTR); // 走到这里说明被信号打断了 read printf(read was interrupted by signal\n); return 0; }这个例子的关键是想让你体会alarm 信号处理函数是早期 Unix 里实现超时机制最常用的手段。把闹钟定好然后去读一个可能永远没有数据的设备时间一到SIGALRM就会打断read触发超时逻辑。现在虽然我们有select/poll/epoll的超时参数但很多老代码和个别特殊场景仍然依赖alarm思路。alarm()还有一个经典应用——给服务端 socket 的accept()加上超时保护防止进程在无连接时无限期阻塞。不过现在更推荐用setsockopt(SO_RCVTIMEO)或者select来做可读性更好。还要提醒一下alarm()只能设置一个闹钟多次调用会覆盖之前的设置。如果要在同一进程里管理多个不同超时要么自己实现一个闹钟管理表要么切换到setitimer()或 POSIX Timertimer_create。这也是我在做服务端开发时建议重点掌握的替代方案#include sys/time.h #include signal.h // 每 100ms 触发一次 SIGALRM struct itimerval tv; tv.it_value.tv_sec 0; tv.it_value.tv_usec 100000; tv.it_interval.tv_sec 0; tv.it_interval.tv_usec 100000; setitimer(ITIMER_REAL, tv, NULL);setitimer相比alarm优势明显支持周期触发、支持微秒级精度。标题里既然写了alarm我就多补一句alarm其实是setitimer(ITIMER_REAL)的简化版底层用的是同一个内核定时器机制。5. Core Dump 核心转储机制崩溃现场如何还原5.1 Core Dump 是什么崩溃那一刻的内存快照进程收到某些致命信号如SIGSEGV、SIGABRT、SIGQUIT、SIGILL、SIGFPE等并即将退出时内核会尝试把进程的整个内存镜像、寄存器状态、进程元信息等内容写入一个文件这个文件就是 core 文件。它相当于崩溃现场的照片录音之后可以用调试器还原程序当时在干什么也能精确回溯变量值、函数调用栈。要理解 core dump 的价值想象你是一个侦探案发之后有一份现场完整录像当然比只看到一具尸体强得多。没有 core 文件时你只能靠日志猜有了 core 文件你可以直接看崩溃线程的调用栈定位是哪一行代码越界访问了。触发 core dump 的信号有三类典型代表SIGSEGV野指针、越界、访问已释放内存。SIGABRTassert 失败、abort()调用。SIGFPE除零整数除法直接触发 CPU 异常生成该信号。5.2 打开与关闭 Core Dumpulimit 与 sysctl大多数 Linux 发行版默认是关闭 core dump 的因为 core 文件往往动辄几十 MB 甚至几个 GB磁盘撑不住、还可能有敏感数据泄漏风险。检查当前限制ulimit -c如果输出0说明 core 文件不会生成。想打开ulimit -c unlimited但要注意ulimit是 shell 内置命令只对当前 shell 及其启动的子进程生效。如果是从 systemd 服务拉起的进程限制要看服务配置或者系统全局/etc/security/limits.conf。另外需要确认内核层面的转储开关# 查看内核是否允许核心转储 cat /proc/sys/kernel/core_pattern常见路径如core或core.%e.%p也可以自定义成带时间戳的路径。某些系统还会把 core 直接丢给 systemd-coredump 处理那就不是普通文件了要用coredumpctl查看coredumpctl list coredumpctl info pid在开发环境里调 core dump 之前我通常先确认两件事ulimit -c unlimited是否生效以及core_pattern是否是预期路径。有时候调了半天没生成最后发现是 core_pattern 被配置成了/dev/null——直接被内核无情丢弃了。5.3 用 gdb 分析 Core 文件四步还原崩溃现场等到 core 文件落下之后直接让 gdb 加载gdb ./your_program ./core.12345进入 gdb 后最有用的两个命令是bt # 打印崩溃时函数调用栈 frame N # 切换到某一帧查看局部变量如果你编的是带调试信息的版本gcc -g还能看到精确到行的代码位置。这也是为什么线上发布二进制一般要保留一份带-g的调试副本以便事后和 core 文件对应分析。给新手的排查流程确认崩溃信号gdb启动时会自动打印类似Program terminated with signal SIGSEGV, Segmentation fault。bt看调用栈找最后一次main到崩溃点之间经过的函数。层层下钻frame查看关键变量和指针有没有异常值比如指向了 0x0、0xdeadbeef。结合源码检查是越界、空指针还是逻辑问题。有一个我自己印象深刻的排查案例某模块不定时崩溃日志没有任何异常。拿到 core 文件后发现栈顶是memcpy参数里的目标地址是堆上一个已经释放的 chunk。顺着局部变量的指针往回追发现是另一个线程提前释放了对象主线程还在用——典型的典型数据争用。如果没 core 文件这类问题靠日志几乎没法排查。5.4 Core 文件的一些进阶玩法除了 gdb 手动分析现代 Linux 上还可以把 core dump 交给崩溃报告工具自动收集。有些团队会在core_pattern里指定一个管道处理程序例如|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %e这样 core 数据不进磁盘文件而是被交给系统组件管理统一查看。这个做法在生产环境里更安全、更好管控但前提是别把core_pattern设为外部网络路径——core 文件里可能包含内存中的密钥、密码、凭证泄露风险不容小觑。我在生产服务器上通常把core_pattern定向到某个专门的临时目录并且设置磁盘配额同时做好文件定期清理。不能因为 core 分析方便就无限制开抢救完现场立刻关掉或者收缩限制是我的一贯做法。6. 硬件中断与进程信号从异常到信号的翻译官6.1 CPU 异常如何变成进程收到的一个信号很多人误把硬件中断理解成信号就是硬件中断其实严格来说是两条链路。硬件中断如网卡收到数据、磁盘完成 IO由 CPU 的中断控制器通知内核内核的驱动代码处理而 CPU 在执行指令时发现异常比如访问非法地址、非法指令、除零这个异常被内核捕捉后转化为某个信号发送给当前进程。拿最典型的SIGSEGV举例。当进程访问了一个不属于它的内存页CPU 触发缺页异常/保护异常。内核的异常处理程序检查这个地址是否合法。如果虚拟地址根本没有映射或者访问权限不对内核就判定为不可修复的段错误。内核向当前进程发送SIGSEGV。如果进程没有捕获该信号默认动作是终止进程并生成 core。换句话说硬件异常是因信号是内核对外发布的果信号只是内核把硬件层的灾难翻译成用户态听得懂的事件。这也是为什么在信号处理函数里你能收到si_code通过带扩展信息的 sigaction 获取里面会区分这个SIGSEGV是内存访问越界、还是写只读区域、还是内核发来的其他原因。6.2 常见硬件触发信号的对照表信号触发硬件情况典型代码错误SIGSEGV非法内存访问空指针解引用、数组越界、访问已释放内存SIGILL非法指令CPU 执行了不存在的指令、函数指针被篡改SIGFPE算术异常整数除以零SIGBUS总线错误/非对齐访问内存映射文件访问越界、对齐问题区分SIGSEGV和SIGBUS是很多进阶开发者的必修课。SIGSEGV通常是访问没有权限访问的虚拟地址SIGBUS则是地址合法但硬件没法访问——比如在 mmap 的文件上超出了文件实际长度访问了还没扩展出去的部分就会得到SIGBUS。6.3 信号处理器不是中断处理器在嵌入式或者底层开发里经常会有一个误区把信号处理函数当作中断服务程序来用。其实两者完全不同中断处理程序运行在内核态能直接响应硬件事件限制极多不能用不安全的调用、不能睡眠。信号处理函数运行在用户态是进程正常执行的上下文衍生的旁路逻辑相对宽松但仍然只能调用异步信号安全的函数。我曾经见过一个项目在信号处理函数里用malloc分配内存然后出现偶发死锁——原因是malloc内部可能持有锁而信号恰好打断了主流程中同样执行malloc的代码两个执行流争同一把锁就卡死了。这个坑非常经典也再一次印证了那句话信号处理函数里有且仅有设置 volatile sig_atomic_t 标志、调用write、调用_exit、调用sigaction修改后续处理这类安全操作。7. 常见问题与排查技巧实录7.1 为什么我用 CtrlC 杀不掉程序按CtrlC发送SIGINT如果程序没有退出可能原因程序注册了SIGINT处理器且处理器没有退出动作最常见你自己写的 handler 里只打印消息但没调用exit。程序把SIGINT忽略掉了比如某些守护进程会主动signal(SIGINT, SIG_IGN)。进程不是当前终端前台进程CtrlC不会发给它。排查办法就是ps -o pid,stat,cmd看进程状态再用kill -TERM、kill -KILL逐级尝试。7.2 信号处理函数里能不能调用 printf、malloc、strcpy不建议。严格来说只有异步信号安全函数才可以在信号处理函数中安全调用。printf、malloc、free都可能操作全局数据结构或锁存在死锁和状态污染风险。如果确实要在处理函数里输出用write(2)行输出如果要做复杂逻辑最好的方案是只置标志位在主流程合适的位置检查标志再执行完整操作。7.3 为什么 kill -9 也会遇到杀不死的进程SIGKILL不可被捕获、不可被忽略正常情况下能终结任意进程。但有一个例外进程处于D状态不可中断睡眠通常是等在内核的磁盘 IO 或 NFS 等外部操作上信号要等它回到用户态时才能递送。遇到这种进程只能等内核操作完成或重启系统。排查时可以看进程的状态字段ps aux | awk $8 ~ /D/7.4 收到信号后 read/write 返回 EINTR怎么处理这是服务端编程里最常见也最容易忽略的问题。EINTR并不是致命错误属于信号打扰了你当前的系统调用。处理方式就是循环重试static ssize_t robust_read(int fd, void *buf, size_t count) { ssize_t n; do { n read(fd, buf, count); } while (n 0 errno EINTR); return n; }配合sigaction设置SA_RESTART可以减少这类问题的发生但终究不能完全消除所以代码层面还是要有重试逻辑。7.5 为什么程序突然莫名其妙消失了后台进程消失时优先去翻系统日志和进程退出码。如果是被信号终止shell 或者父进程能看到WIFSIGNALED(status)以及对应信号值。在 shell 里跑一个程序失败退出可以立即echo $?如果退出码大于 128基本可以断定是被信号杀掉的退出码减 128 就是信号编号。比如$?返回 139则139 - 128 11对应SIGSEGV。这个技巧我在排查脚本问题时用了无数次非常实用。7.6 使用信号时的另外几条实战心得注册处理函数时尽量用sigaction而别用signal尤其在需要跨平台和保证可靠行为的时候。不要尝试捕获SIGKILL和SIGSTOP你捕获不到白白浪费精力。多线程进程里信号默认可以发给任意线程。如果不希望某线程被某个信号打断可以设置线程级信号掩码。信号经常被用来做优雅停机的触发通道配合全局标志位实现主循环退出比直接用exit()粗暴退出更稳妥。测试信号机制时打印进程 PID 再另开终端发信号别老用系统自带的kill打断整个终端会话。8. 踩坑之后的经验沉淀自己这些年处理过的和信号相关的线上问题不少最大的感受是信号机制并不难难的是意识。它不像内存泄漏那样有明显的迹象很多时候进程崩溃就会被记录为一句Exited with signal 11然后排查看似毫无头绪。但只要养成了几个好习惯难度会大幅下降。第一个好习惯是每写一个服务端程序务必先明确要处理哪些信号。SIGTERM用于优雅停机SIGINT用于终端中断SIGSEGV、SIGABRT之类的崩溃信号宁可让系统生成 core 也别在 handler 里做太多操作保留第一现场比什么都重要。第二个好习惯是信号处理函数里只做通知不做处理。真正的工作放到主流程里去做用volatile sig_atomic_t标志位或者一个自写的无锁队列把事件传递出去。你可以先把这个原则定下来再开始写代码。我见过太多为了图方便在 handler 里顺便更新一下数据库、顺便写个日志文件而导致线上诡异问题的案例。第三个好习惯是永远保留一份带调试信息的发布产物。崩溃后要分析 core 文件却发现二进制没有符号那种感觉真是极其难受。哪怕线上跑的是-O2的版本也有办法保留单独的符号文件。只要调试信息在core 分析就能定位到具体函数和行号排查效率天差地别。如果你的代码已经开始涉及网络服务、多线程、守护进程这些场景信号必定会出现在你的代码里。别怕它先把这几个接口用熟再逐步理解信号是内核与进程之间最底层的事件通知语言之一这件事。下一篇我可以继续写写父子进程间的信号继承、信号与多线程的交互以及用signalfd把信号处理放到事件循环里的现代做法——那又是另一个非常有趣的话题。