Linux信号机制全解析:kill、sigaction与Core Dump实战

发布时间:2026/10/10 6:39:37
Linux信号机制全解析:kill、sigaction与Core Dump实战 聊Linux进程管理的时候信号signal是无论如何都绕不过去的一块。很多人最初接触它的契机是kill -9 pid杀进程但kill背后的信号机制远不止“杀进程”这一个用处。信号本质上是一种异步事件通知机制它告诉进程“有事情发生了”而这个事情既可能来自用户按下 CtrlC也可能来自内核发现你访问了非法内存还可能来自另一个进程专门给你发了一条消息。这篇文章我就结合平时写服务、排查线上异常的经验把 signal、kill、raise、abort、alarm、Core Dump 机制以及它们和硬件中断之间的关联一次讲清楚。无论你是刚学进程管理还是已经被信号处理函数坑过几次都值得往下看。1. 先弄清楚信号这东西到底是什么1.1 信号是一种“让进程来不及准备”的通知我们可以把信号理解成现实里的“紧急电话”内核是调度员它有电话可以直接打到某个进程那里。这个电话什么时候响进程自己没法预测所以信号是异步的。进程收到电话之后可以选择自己接注册自定义处理函数、直接当没听见忽略信号也可以保持默认反应通常是终止进程。信号既然叫“信号”本质上就是发给进程的一个整数代码。Linux 里每个信号都有名字和编号比如SIGINT是 2SIGTERM是 15。这些编号在不同的系统调用、命令行工具、日志信息里都会冒出来认识它们是很底层但也很有用的基本功。拿常见的几个信号来看信号编号默认动作典型触发场景SIGHUP1终止进程终端挂断很多守护进程用它触发配置重载SIGINT2终止进程CtrlCSIGQUIT3终止并生成 CoreCtrl\手动触发核心转储SIGKILL9无条件终止kill -9不可捕获也不可忽略SIGSEGV11终止并生成 Core非法内存访问典型的段错误SIGPIPE13终止进程向已关闭读端的管道写数据SIGALRM14终止进程alarm 定时器到期SIGTERM15终止进程kill 默认发送的终止信号SIGCHLD17忽略子进程退出或停止父进程靠它知道“孩子状态变了”SIGSTOP19暂停进程无条件暂停不可捕获也不可忽略SIGCONT18继续运行让被暂停的进程继续跑注意SIGKILL和SIGSTOP很特殊它们不能被进程捕获也不能被忽略。这是内核故意保留的最后手段一个进程就算是把信号处理函数玩出花来也没办法永远躲掉这两条系统级的约束。1.2 信号从哪来又要到哪去一个完整信号的“一生”可以分成三步第一步是产生。产生信号的来源很多用户在终端按下 CtrlC、Ctrl\内核会向前台进程组发送 SIGINT 或 SIGQUIT。程序执行中触发了 CPU 异常比如除零、访问非法地址、执行非法指令内核会把异常转换信号发给出错的进程。某个进程主动调用kill()、raise()、abort()等接口指定把某个信号发给另一个进程或自己。内核的定时器到期比如alarm()设置的时间到了代写 SIGALRM 发送。第二步是登记。信号产生之后并不是立刻跑到目标进程的代码里“打断执行”而是先被内核记录在该进程的信号等待队列中。现代内核用位图或者链表去维护这些 pending 信号位图的每一位对应一个标准信号。第三步是投递。进程从内核态返回用户态之前内核会检查有没有 pending 信号需要处理。如果有就让进程在用户态执行对应的信号处理函数。这里有个关键点信号处理函数运行在用户态不是在内核态跑所以我们可以放心地在这个函数里写业务逻辑最多受一些可重入规则约束。处理完之后进程会从上一次被中断的系统调用或指令处继续执行具体怎么继续还取决于系统调用是否被设置成自动重启。1.3 进程收到信号后的默认行为默认行为不是只有“终止”这一种粗略分五种终止直接杀掉进程。忽略内核把这个信号丢掉进程根本不需要知道。终止加核心转储进程先写一个 Core 文件把当前内存快照、寄存器状态等现场保存下来然后再退出。停止把进程暂停不是杀掉之后还能用 SIGCONT 继续。继续把之前被暂停的进程恢复执行。这五种默认行为对应了不同场景。比如SIGCHLD的默认行为是忽略因为父进程通常想知道子进程变退出但不一定需要立刻处理SIGSEGV、SIGABRT这类信号默认就要生成 Core因为崩溃现场太珍贵了丢掉就真白崩了。2. 发信号的手段kill、raise、abort 的差异与使用场景2.1 kill() 系统调用不只是杀进程很多开发者容易把kill命令和“杀掉进程”画等号但实际上kill系统调用只是“发送任何信号”的通用入口名字沿用历史习惯而已。函数原型#include sys/types.h #include signal.h int kill(pid_t pid, int sig);pid参数的变化会决定信号发给谁pid 0发给指定 pid 的进程。pid 0发给调用进程所属进程组里的所有进程。pid -1发给调用进程有权限发送的所有进程注意这里包含系统里的很多进程用的时候要非常小心。pid -1发给进程组 id 等于|pid|的所有进程。sig参数传 0 有特殊用法叫“探活检查”kill(pid, 0)不真正发送信号只检查指定进程是否存在以及当前用户是否有权限向它发送信号。如果返回 -1 且 errno 是 ESRCH说明进程不存在如果是 EPERM说明进程存在但你没权限。这个技巧在服务健康检查脚本里很常用比/proc/pid判断要方便。使用示例#include stdio.h #include signal.h #include errno.h #include string.h #include unistd.h int main(void) { pid_t target 12345; // 假设这是目标进程 if (kill(target, SIGTERM) -1) { fprintf(stderr, kill failed: %s\n, strerror(errno)); if (errno ESRCH) { fprintf(stderr, 进程不存在\n); } else if (errno EPERM) { fprintf(stderr, 没有权限发送信号\n); } return 1; } return 0; }实际工程里kill(pid, SIGTERM)通常用来请求一个服务优雅退出程序在收到 SIGTERM 后可以清理资源、保存状态、关闭连接然后自己退出。只有在一段时间后没退才考虑SIGKILL强制结束。2.2 raise() 函数进程给自己发信号raise的语义更直接给当前进程自己发送指定信号。函数原型#include signal.h int raise(int sig);在单线程程序里raise(sig)相当于kill(getpid(), sig)。但在多线程程序里两者就有区别了raise会把信号发送给调用线程而kill(getpid(), sig)虽然也是发给整个进程但最终哪个线程拿到这个信号由内核的线程调度和信号分发规则决定一般会送到主线程或某个可用线程。所以如果你想在某个工作线程里触发当前线程的信号处理逻辑用raise更符合直觉如果你想让进程层面统一处理可以考虑用pthread_kill(pthread_self(), sig)或者kill(getpid(), sig)。实际场景中raise常用于业务自检失败时主动报警。比如程序读到一个明显不合理的数据状态希望在崩溃前把现场留下来就可以用raise(SIGABRT)触发 abort 逻辑等会儿会讲。2.3 abort() 函数让进程带着“遗书”离开abort()的职责非常明确异常终止当前进程并且尽量把崩溃现场记录下来。它的内部实现大致是先调用raise(SIGABRT)如果之后函数又返回了——也就是 SIGABRT 被进程捕获或忽略——那么它会重置 SIGABRT 的默认处理方式再次触发确保进程一定会被终止。#include stdlib.h void abort(void);它和exit()的区别在于exit()是“体面退场”会执行atexit注册的钩子函数、刷新 stdio 缓冲区而abort()的目标是“就地暴毙”尽可能保留原始出错现场。glibc 实现里会尝试刷新 stdio 流但这并不保证所有 C 库实现都一样所以别把资源清理逻辑放到 abort 之后执行。在库函数里处理不可恢复错误时abort()比exit()更合适。因为库代码根本不该替调用方决定“要不要继续跑”直接 abort 并产生 Core等开发者用调试器看到底是哪一步出了问题反而是最清晰的错误信号。3. signal 函数与信号处理函数的正确姿势3.1 注册一个信号处理函数最基础的方式就是signal()函数#include signal.h typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);handler 有几种取值SIG_IGN表示忽略信号SIG_DFL表示恢复默认行为也可以是自己定义的函数指针。函数返回值是旧的处理函数通常用在切换前后保存现场但实际项目里更多直接忽略了只有出错时返回SIG_ERR值得关注。一个常见的优雅退出实现#include stdio.h #include signal.h #include unistd.h static volatile sig_atomic_t g_running 1; static void handle_term(int sig) { (void)sig; g_running 0; // 只改标志不做复杂操作 } int main(void) { signal(SIGINT, handle_term); signal(SIGTERM, handle_term); while (g_running) { // 业务逻辑 sleep(1); } printf(clean exit\n); return 0; }这里的标志位必须用volatile sig_atomic_t。sig_atomic_t保证了在主程序正在读取的同时信号处理函数写入时不会导致数据撕裂volatile是通知编译器不要把这个变量优化到寄存器里缓存否则主循环可能永远读不到更新后的值。3.2 处理函数里能干啥这才是重点信号处理函数最坑的地方在于它在任意时刻都可能运行也就是说它会打乱主程序的正常执行流程。所以这个函数不能随便调用普通库函数。不可重入函数的意思简单说就是如果一个函数在执行过程中再次被调用结果会出错或产生竞态那它就是不可重入的。printf、malloc、free、getenv这类函数在信号处理函数里都非常危险。printf内部有 stdio 缓冲区还可能持有锁。如果主程序在 printf 内部走到一半信号来了处理函数里又调 printf就会发生缓冲区状态错乱输出重复或丢失锁未释放处理函数里的 printf 被自己锁死程序卡住。malloc更严重。主程序可能在堆分配中途被信号打断处理函数又调用 malloc堆管理数据结构处于中间状态轻则内存泄漏重则直接崩掉。处理函数里安全可以做的事比较有限最稳妥的是只设置一个volatile sig_atomic_t标志位用write()写几个字节到标准错误调用_exit()直接退出而不是exit()。static void alarm_handler(int sig) { (void)sig; write(STDERR_FILENO, timeout!\n, 9); _exit(1); }如果确实需要在信号发生时做更多工作推荐采用“信号处理函数只记录标志主循环轮询标志并执行具体逻辑”的模式。这样实际的风险都控制在主循环里行为可预测调试也方便。3.3 用 sigaction 替代 signal别在现代代码里用历史包袱signal()的历史实现有两大派系System V 和 BSD两者最大的分歧在于信号处理函数执行完后这个信号的处理方式是否重置为默认行为。System V 的做法是重置导致处理函数只能生效一次第二次同样的信号来临时进程可能直接按默认动作终止BSD 的做法是保持注册。不同平台的signal()行为不同这种不确定性在跨平台项目里很容易引发莫名其妙的边界问题。现代 Linux 编程规范里首选sigaction()#include signal.h struct sigaction { void (*sa_handler)(int); sigset_t sa_mask; int sa_flags; void (*sa_sigaction)(int, siginfo_t *, void *); };struct sigaction sa {0}; sa.sa_handler handle_term; sigemptyset(sa.sa_mask); sa.sa_flags SA_RESTART; sigaction(SIGTERM, sa, NULL);sa_mask指定在处理该信号期间额外阻塞哪些信号防止嵌套打扰。sa_flags常用几个标志SA_RESTART被信号打断的系统调用自动重启比如read、write遇到信号后不会返回 EINTR而是等信号处理完继续执行。SA_SIGINFO使用sa_sigaction回调可以拿到信号来源、附加数据等详细信息。SA_RESETHAND处理完一次后重置为默认行为恢复 System V 风格。需要获取信号更多信息时必须用sa_sigactionSA_SIGINFO。比如想判断SIGCHLD到底是因为子进程退出、停止还是继续只有siginfo_t的si_code能区分。4. alarm 与定时信号别让超时控制写成一团乱麻4.1 alarm() 很简单但背后的默认行为要记住#include unistd.h unsigned int alarm(unsigned int seconds);alarm()让内核在 seconds 秒之后向当前进程发送SIGALRM。返回值是上一次 alarm 设置后剩余的秒数。如果之前设置过定时器且还没到期再次调用 alarm 会直接覆盖旧的并返回旧定时器还剩的秒数。通常我们把这个串联成一个定时机制#include stdio.h #include signal.h #include unistd.h static volatile sig_atomic_t g_flag 0; static void on_alarm(int sig) { (void)sig; g_flag 1; } int main(void) { struct sigaction sa {0}; sa.sa_handler on_alarm; sigaction(SIGALRM, sa, NULL); alarm(3); while (!g_flag) { // 等待 3 秒或处理其他事情 } printf(alarm fired\n); return 0; }注意SIGALRM的默认动作是终止进程。如果你只调用alarm(3)却没注册处理函数那 3 秒后进程就直接被杀了这常常是初学者的疑惑点。4.2 用 alarm 做 IO 超时控制经典的 EINTR 坑一个很常见的场景我们需要给read()设置一个超时避免网络客户端一直不发送数据导致服务卡住。很多人的第一反应是alarm(5); n read(fd, buf, sizeof(buf)); alarm(0); // 读完了取消闹钟这个写法有个竞态问题如果read()还没调用alarm 已经到点信号直接送达而你还没进入阻塞读超时信号就白白浪费了。更麻烦的是即使信号在read()阻塞期间到达read()可能被中断返回-1且 errno 为EINTR你需要判断到底是“真超时”还是“被信号打断”。规范的写法是先设置信号处理函数再用sigaction的SA_RESTART让读重新开始但这样又没办法用 EINTR 来感知超时了。所以更多人直接用poll或带超时的 epoll这套机制在 IO 超时控制上比信号靠谱得多。alarm的定位更多是简单的定时任务或者配合 sleep 类操作。还有一个经典的点alarm()只能保证到点发一个信号信号不排队。多次设置只能覆盖不会累加。如果需要周期性触发定时的“心跳”可以在处理函数里再次调用alarm()自己续周期但断续之间如果有抖动需要评估精度。4.3 比 alarm 更现代的定时器选项alarm精度只有秒级因为它的粒度就是秒。要做更高精度的定时Linux 提供了几个递进方案接口精度重复触发通知方式适合场景alarm()秒级不自动重复SIGALRM简单超时控制、定时唤醒setitimer()微秒级可自动重复SIGALRM 等信号轻量周期任务timer_create()纳秒级可自动重复信号或线程回调高精度 POSIX 定时器timerfd_create()纳秒级可自动重复文件描述符可读高并发事件循环配合 epoll对于高并发服务我最推荐timerfd_create()。它把定时器变成了一个文件描述符可以直接丢给 epoll 监听。到时候 fd 可读主循环统一处理根本没有信号中断的烦恼也不会在信号处理函数里做危险操作。这个设计思路其实是“把异步信号的复杂性转化为事件循环里的普通事件”对稳定性要求高的服务非常合适。5. Core Dump 核心转储机制程序崩溃后的一手现场5.1 为什么叫 Core Dump这个名字最早来自老式内存技术“磁芯存储器”当程序异常终止时内核把进程当时的“核心”内存映像保存到磁盘文件所以叫 Core Dump。这个文件保存的不只是内存数据还包括寄存器、栈、打开的文件描述符、信号信息等可以说是一次完整的“案发现场”。默认情况下很多 Linux 环境是不生成 Core 文件的。因为虚拟内存空间很大如果程序使用了几 GB 内存直接落盘可能会瞬间写满磁盘。操作系统提供了限制开关# 查看当前限制 ulimit -c # 关闭 ulimit -c 0 # 不限制大小 ulimit -c unlimited很多同学写了段带 bug 的 C 代码跑出Segmentation fault (core dumped)提示但当前目录却没找到 core 文件多半就是 ulimit 限制的问题。先执行ulimit -c unlimited再复现崩溃。5.2 Core 文件放哪里怎么控制Core 文件的命名和路径由内核参数kernel.core_pattern控制cat /proc/sys/kernel/core_pattern不同发行版差别很大。早期系统简单输出core意思是直接写到当前工作目录下名为 core 的文件现在很多系统会通过管道交给 systemd-coredump比如|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h这种情况下普通用户看不到 core 文件需要用coredumpctl list、coredumpctl info PID之类的工具去查。如果需要自定义路径可以修改 core_pattern比如echo /var/core/core_%e_%p_%t /proc/sys/kernel/core_pattern%e是程序名%p是 PID%t是时间戳。这些占位符在排查多个崩溃实例时非常有用。生产环境里我的建议是ulimit -c设置一个合理上限比如 200MB防止 Core 文件把磁盘打满core_pattern 指向独立分区或专用目录用 coredumpctl 或自定义监控脚本定期清理和归档核心转储Core 文件可能包含敏感内存数据比如密码明文权限和保存周期都要谨慎。5.3 用 Core 文件还原崩溃现场假设我们写了一个肯定会崩的程序#include stdio.h #include string.h int main(void) { int *p NULL; *p 42; return 0; }编译运行gcc -g -o demo demo.c ulimit -c unlimited ./demo输出Segmentation fault (core dumped)目录下出现 core 文件。接着用 gdb 调试gdb ./demo core进入 gdb 后第一步一定是btbacktrace看调用栈(gdb) bt #0 0x00000000004004f5 in main () at demo.c:6 6 *p 42;这一下就定位到第 6 行的空指针赋值。如果代码复杂还可以info locals查看当前局部变量frame 1、frame 2切换调用栈帧info reg查看寄存器里的现场list显示对应源码行前提是编译时带-g调试信息。释放一个很实用的经验生产环境排查崩溃不要依赖用户描述和日志直接把 Core 文件拿回来 gdb 一把梭定栈是最高效的手段。很多“神秘崩溃”看了 Core 文件之后发现是栈溢出、double free 或者内存被踩坏几乎不用猜。6. 硬件中断与信号信号源头中的硬骨头6.1 硬件中断是信号的前置触发点很多人以为信号只是软件层的东西实际上很多关键信号来自 CPU 内部的异常响应。比如访问非法内存。当用户态程序执行一条访问地址的指令CPU 的 MMU 会做地址转换发现这个地址没有映射或权限不对CPU 触发一个页故障异常这个异常被操作系统捕获。内核查了一下发现这个地址确实不在进程地址空间里就决定给当前进程发SIGSEGV。比如整数除零。CPU 在执行除法指令时发现除数是 0会触发一个“除法错误”异常内核把这个异常对应到 SIGFPE发给当前进程。再比如执行非法指令。CPU 遇到无法识别的操作码会触发非法指令异常内核转成 SIGILL 发给进程。这一整条链路是“硬件中断/异常 - 内核处理 - 进程收到信号”而不是信号真的从硬件里直接飞出来。信号本身仍然是软件层面的机制但它的触发源带上了硬件异常的味道。这也是为什么段错误、总线错误这类信号发生时默认动作一般都会生成 Core因为内核知道这是进程“搞出了严重问题”现场对排查价值极高。6.2 外部硬件中断和信号的差别电脑上的网卡收到数据包时会通过中断通知 CPU这个过程是硬中断。内核中断处理程序把数据包放进协议栈队列后续处理完成后唤醒正在等待的进程这个过程可能对进程表现为“socket 可读了”但通常不直接发一个信号给进程。所以外部设备中断和“进程信号”是两种不同层面的事件外部中断是给内核的告诉它“硬件有事情”内核需要决定怎么处理。信号是内核给进程的告诉它“有事情涉及你”进程需要决定怎么处理。只有当硬件状态变化转化为进程必须关注的条件时内核才会发明信号。比如网卡入队的数据超过 socket 缓冲区阈值可能触发 SIGURG 通知进程有紧急数据。6.3 另一个被忽略的软件信号SIGPIPE除了硬件异常很多常见信号来自软件条件比如 SIGPIPE。如果你向一个已经关闭读端的管道或 socket 写数据内核会发送 SIGPIPE 给写进程。默认动作是终止进程这就导致很多服务在客户端断开连接后服务端在往 socket 写数据时突然“莫名其妙”死掉。要处理这个问题一般做法是signal(SIGPIPE, SIG_IGN);忽略 SIGPIPE 后内核不会再用“杀进程”来表达管道断开而是让write()调用返回-1并设置 errno 为 EPIPE。这样程序就能通过检查 write 返回值来感知连接异常做出优雅处理。这个处理在编写网络服务时几乎是必做的。7. 信号实战中的常见坑与排查技巧7.1 标准信号不排队别被“信号丢失”骗了这是信号机制里最让人郁闷的特性之一。Linux 的标准信号1 到 31不含实时信号是不排队的。进程在执行信号处理函数的过程中如果又收到一个相同的信号这个新信号会被标记为 pending但不会累积。也就是说发 10 次同一个标准信号给进程进程可能只处理一两次。比如你用信号给守护进程发送“重新加载配置”的通知结果连续来了 5 次处理函数只执行了一次有时候这符合预期重复通知合并处理反而更高效有时候却是 bug。解决方案有两个方向使用实时信号SIGRTMIN以上的信号这部分信号支持排队用signalfd读取信号把信号变成事件流由事件循环统一计数处理。signalfd的高明之处在于它把信号当作普通事件描述符看待可以用 read 把信号信息读出来还可以配合 epoll 做完整的事件驱动模型。它尤其适合写高性能网络服务时既不想用不可靠的标准信号又不想开一堆线程处理信号的场景。7.2 信号处理函数里的野操作偶发崩溃的典型来源我见过不止一个线上服务平时好好的一到高峰期就偶发崩溃查了很久才发现是在信号处理函数里写了日志。原理上面说过处理函数里调 printf 一类函数不安全但开发初期可能没人注意这些毕竟测试时很难稳定重现。解决办法很简单处理函数只设置标志变量日志改到主循环检测到标志之后由主逻辑打印实在要在处理函数里输出用write别用printf。如果是 C 程序还要额外注意处理函数里不能调用任何可能分配内存或执行非线程安全操作的标准库函数连new、delete都不应该出现。7.3 忽略 SIGCHLD 引起的僵尸进程子进程退出后不会立刻从系统里消失而是变成僵尸进程Zombie直到父进程调用wait()/waitpid()回收它的退出状态。如果父进程一直不回收系统进程表里的僵尸条目就越积越多pid 资源被白白占用。正确处理方式有两种如果你不关心子进程的退出状态最简单的是signal(SIGCHLD, SIG_IGN);这是 Linux 的一个特殊约定父进程显式忽略 SIGCHLD那么子进程退出时会被内核自动回收不产生僵尸进程。如果你需要知道子进程的退出码、终止原因可以注册处理函数在里面调用 waitpidstatic void on_sigchld(int sig) { (void)sig; int status; pid_t pid; while ((pid waitpid(-1, status, WNOHANG)) 0) { // 处理 pid 的退出状态 } }这里用WNOHANG是避免阻塞父进程并且用循环把所有已退出子进程都收一遍。7.4 排查信号问题的小工具生产环境里排查信号问题三个工具很实用第一是strace。跟踪进程的系统调用时信号相关事件也会被记录下来。比如strace -e tracesignal -p 12345可以看到进程收到了哪些信号以及系统调用被信号打断后返回了EINTR。这能很直接地告诉我们“信号来的时候程序正在干什么”。第二是gdb的信号处理。调试时你可以让 gdb 在特定信号发生时停下来(gdb) handle SIGSEGV stop (gdb) run这样崩掉时先停在信号的投递点再用bt看调用栈比直接看 Core 文件还早一步。第三是/proc/pid/status。查看进程的信号相关信息比如grep -E Sig(Pnd|Blk|Ign|Cgt) /proc/12345/statusSigPnd是 pending 的信号位图SigBlk是阻塞的信号位图SigIgn、SigCgt分别记录忽略和捕获的信号。它反映的是当前进程的信号配置快照排查“为什么这个进程没有被 SIGTERM 杀掉”时非常有用。7.5 服务端程序到底该用 handler 还是 sigwait处理信号有两种主流模型选择取舍很重要。一种是传统的 handler 全局标志。优点是小轻量适合少量信号、逻辑简单的小进程。缺点是信号处理函数在任意线程中随机执行容易和主线程产生竞态对多线程复杂状态的一致性很难保证。另一种是sigwait模型。把感兴趣的信号先阻塞掉再开一个专用线程调用sigwait()等待信号到来信号会作为普通的整数值返回然后在线程里慢慢处理。这个模型相当于把信号变成了一个普通的业务队列事件完全没有“中断主程序”的副作用。参考代码思路sigset_t set; sigemptyset(set); sigaddset(set, SIGTERM); sigaddset(set, SIGINT); sigprocmask(SIG_BLOCK, set, NULL); // 在某个线程里 int sig; while (sigwait(set, sig) 0) { if (sig SIGTERM || sig SIGINT) { break; } }对于高并发的服务端程序我更推荐 sigwait 模型。它的行为可预测不需要在信号处理函数里做任何受限操作线程化的处理也可以直接加日志或者做复杂的释放逻辑。代价是多开一个线程和稍微复杂一点的初始化顺序但这点成本换来的稳定性非常划算。最后的一点个人经验我自己的项目里曾经被SIGALRM坑过一次。当时图省事用alarm()给某个阻塞 IO 做超时为了判断超时信号处理函数里写了几条日志结果压测时服务偶尔卡死。后来用 gdb 一查发现printf在信号处理函数和主逻辑之间发生了锁竞争一个持有锁没释放另一个进去等锁整个线程全卡住。改成在信号处理函数里只设置标志位、主循环里检查标志之后再打日志问题立刻消失。后来所有需要定时、超时的服务我都优先选择 timerfd 加 epoll 的方式彻底绕开了信号处理函数的各种限制。信号这套机制看着简单真正用起来边界条件比一般代码多得多每一次踩坑都值得把原因弄透否则下次还会在同样的地方栽跟头。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询