
1. 先搞清楚Linux信号到底是什么先说个题外话。“Sigal”这个写法十有八九是手误圈里人一般搜索的是SIGALRM或signal。如果你在搜“linux下Sigal信号值”大概率是想弄明白Linux里的信号机制是怎么回事、SIGALRM这个信号具体什么含义、它的值是多少、在编程中怎么用。我最早接触这个概念是在做嵌入式设备上的守护进程时跑着一个需要定时上报状态的程序结果发现程序过一段时间就像“死掉”了一样后来查了半天才发现是定时器信号的处理函数出了问题。从那时起我才意识到信号这个东西看着简单真正玩明白需要不少实战经验。那么信号到底是什么一句话概括信号是操作系统用来通知进程“发生了某种事件”的一种软件中断机制。它不依赖额外的库是Linux进程间通信IPC最古老、最基础的手段之一。你可以在终端跑一条命令直接杀掉一个进程kill -9 1234这里的-9就是信号值对应的信号叫SIGKILL。进程一旦收到这个信号内核会立即强制终止它不给任何“收拾东西”的机会。再看另一个经典场景。你在终端按下Ctrl C前台进程会收到一个SIGINT信号值2默认行为就是终止进程。这就是为什么很多程序在按Ctrl C后能直接退出而不是卡死在那。Linux系统一共定义了60多种信号每种信号都有固定的编号和默认行为。它们在头文件signal.h里都有宏定义比如#define SIGHUP 1 #define SIGINT 2 #define SIGQUIT 3 #define SIGILL 4 #define SIGABRT 6 #define SIGFPE 8 #define SIGKILL 9 #define SIGSEGV 11 #define SIGALRM 14 #define SIGTERM 15不同的Unix-like系统上信号值可能有差异比如有的架构上SIGALRM并不是14但我在长期实践中确认在几乎所有x86、ARM平台的Linux发行版上这套编号是稳定的标准。做跨平台开发时最好还是用宏名而不是写死的数字这点后面细说。2. 信号值全表哪路信号干什么活刚入门的时候我也很头疼信号值1到64谁记得住啊直到后来我把它们分成几个“组”来理解瞬间轻松很多。先看一张针对Linux/x86平台的标准信号值速查表信号值信号名默认行为常见触发场景1SIGHUP终止进程终端挂断、进程的控制终端关闭2SIGINT终止进程键盘按 CtrlC3SIGQUIT终止并产生核心转储键盘按 Ctrl\4SIGILL终止并产生核心转储执行非法指令6SIGABRT终止并产生核心转储调用 abort()断言失败8SIGFPE终止并产生核心转储除零、浮点运算异常9SIGKILL强制终止kill -9内核直接接管11SIGSEGV终止并产生核心转储访问非法内存地址空指针解引用13SIGPIPE终止进程向已关闭的管道写数据14SIGALRM终止进程alarm() 或 setitimer() 定时器到期15SIGTERM终止进程kill 默认信号优雅退出17SIGCHLD忽略子进程停止或退出18SIGCONT继续执行让暂停的进程继续运行19SIGSTOP暂停进程强制暂停不可捕获20SIGTSTP暂停进程键盘按 CtrlZ这里面有个关键区分SIGKILL9和SIGSTOP19是“终结者”级别的信号进程不能捕获、不能忽略、不能阻塞内核保证处理。其余大部分信号进程都可以通过signal()或sigaction()注册自己的处理函数。所以在实际操作中服务关闭我首选kill -15而不是kill -9。因为进程收到SIGTERM后可以做清理工作释放数据库连接、关闭文件描述符、保存状态。而SIGKILL是暴力断电一切清理都来不及做极端情况下还会留下锁文件或脏数据。还有一类容易被忽视的信号是SIGCHLD17。写多进程程序时父进程如果不等待子进程子进程退出后就会变成僵尸进程。正确做法是通过waitpid()回收或者注册SIGCHLD处理函数在子进程退出时自动回收。这个信号默认是忽略的如果你管理过常驻进程一定遇到过僵尸进程堆积的问题。2.1 实时信号从34开始的“进阶选手”前面说的信号值1到31属于标准信号而Linux还支持另一套实时信号编号范围是34到6432和33被系统占用分别是SIGCANCEL和SIGSETXID一般不用。标准信号有个很大的局限它们会被“合并”。意思是如果进程在信号处理函数执行之前同一个信号连续来了100次最终进程可能只收到1次。因为标准信号在内核里用位图记录一位只能表示“有”或“没有”不能记录次数。实时信号SIGRTMIN到SIGRTMAX则没有这个问题。它们支持排队每个信号都带着一个整数参数进程可以收到完整的信息。用POSIX接口sigqueue()可以发送实时信号处理函数里通过sigev_value/si_value拿参数。给一个比较实用的方向如果你在设计一个多线程服务需要不同线程之间传递“第几次重试、哪条连接断开了”之类的信息实时信号比标准信号合适得多。当然实际工程里这种场景大多数人会直接上消息队列或事件回调但了解信号的能力边界能帮你判断什么情况下该用它。2.2 为什么说信号值是“平台相关”的这一点我在做跨平台移植时踩过坑。Linux的SIGALRM是14SIGTERM是15这是x86平台上的标准。但FreeBSD、macOS、AIX这些平台上部分信号值并不一致。比如macOS上SIGALRM同样也是14但SIGUSR1和SIGUSR2在不少Unix系统上的值就不同。所以代码里永远不要写kill(pid, 14); // 你以为它在发SIGALRM应该写kill(pid, SIGALRM); // 这才是跨平台的安全写法用信号宏名还有一个好处可读性。你看代码的时候SIGTERM一眼就知道是“请求终止”而一个裸数字14还得翻表查半天。3. 重点拆解SIGALRM这个信号值回到主题。SIGALRM信号值为14是“闹钟”信号由系统的定时器机制触发。它的行为非常朴素进程设置了一个定时器时间一到内核就给这个进程发送SIGALRM。默认情况下进程收到这个信号会直接终止。3.1 怎么触发SIGALRMalarm()与setitimer()最基础的触发方式是调用alarm()#include unistd.h #include stdio.h #include signal.h int main() { alarm(3); // 3秒后发送SIGALRM printf(闹钟已设置等待触发...\n); pause(); // 挂起进程等待信号 return 0; }这段代码的预期行为是程序打印提示文字然后挂起。3秒后内核发来SIGALRM由于没有注册任何自定义处理函数进程被默认终止。alarm()只能设置秒级别的定时精度不够。更常用的是setitimer()它支持微秒级精度并且支持三种定时模式ITIMER_REAL真实时间计时超时发送SIGALRMITIMER_VIRTUAL只在进程用户态运行时间计时超时发送SIGVTALRMITIMER_PROF进程用户态和内核态运行时间都计时超时发送SIGPROFITIMER_REAL和SIGALRM是强绑定的所以如果你想实现周期性的任务执行setitimer()SIGALRM是最经典的组合。3.2 setitimer 完整示例周期执行任务下面是一个完整的示例代码实现“每隔2秒打印一次当前时间戳”#include stdio.h #include signal.h #include string.h #include sys/time.h #include time.h void timer_handler(int signum) { time_t now time(NULL); printf(信号 %d 触发当前时间: %s, signum, ctime(now)); fflush(stdout); } int main() { struct sigaction sa; struct itimerval timer; // 1. 注册信号处理函数 memset(sa, 0, sizeof(sa)); sa.sa_handler timer_handler; sigemptyset(sa.sa_mask); sa.sa_flags 0; if (sigaction(SIGALRM, sa, NULL) -1) { perror(sigaction); return 1; } // 2. 设置定时器首次2秒后触发之后每2秒触发一次 timer.it_value.tv_sec 2; timer.it_value.tv_usec 0; timer.it_interval.tv_sec 2; timer.it_interval.tv_usec 0; if (setitimer(ITIMER_REAL, timer, NULL) -1) { perror(setitimer); return 1; } printf(定时器已启动按 CtrlC 退出\n); while (1) { pause(); } return 0; }这个代码有几个细节值得强调第一要用sigaction()而不是老旧的signal()注册处理函数。signal()在不同平台上的语义有差异而且它会重置处理函数第二次信号到来时可能又走默认行为了。sigaction()是POSIX标准接口行为一致、可控性强。第二信号处理函数里只用printf和time。这只是为了方便演示。真实工程里信号处理函数能调用的函数非常有限只保证async-signal-safe的函数是安全的。比如printf就不是完全安全的只是大多数场景下能用。推荐的做法是信号处理函数里只做标志位设置真正的业务逻辑放到主循环里执行。比如volatile sig_atomic_t flag 0; void timer_handler(int signum) { flag 1; // 只设置标记不干别的 } int main() { // ... 注册信号 ... while (1) { if (flag) { flag 0; // 在这里执行真正的任务 } // 做其他事情 } }第三volatile sig_atomic_t是个关键类型。它保证了对这个变量的读写是原子的信号处理函数和主程序之间用它传递信息不会产生数据竞争。普通int虽然大多数情况下也没问题但严格来说并不保险。3.3 SIGALRM 的典型应用场景SIGALRM不是用来“杀死进程”的它的真实价值在于“时间到了该干活了”。我实际用到的场景有几类第一类是超时控制。在网络编程中recv()如果设置了阻塞可能永远等不到数据。可以通过alarm(5)设置5秒超时如果recv()还没返回SIGALRM就会打断阻塞的系统调用让它返回EINTR错误我们就能判断“超时了”然后走超时处理逻辑。第二类是定时上报。比如物联网设备的温度传感器每30秒上报一次数据。用setitimer()周期触发SIGALRM处理函数里唤醒采集线程这就是一个非常简单可靠的定时调度方案。第三类是看门狗。写一段逻辑监控某个业务线程的心跳。如果一段时间内比如60秒没有收到心跳就认为线程卡死了通过SIGALRM触发恢复逻辑或者直接重启线程。但要注意SIGALRM也有自己的弱点它和sleep()、pause()这类函数会“打架”。因为sleep()的内部实现就可能用到了定时器机制一旦同时用了alarm()两者会产生干扰导致睡眠被提前唤醒。工程上我会尽量把超时和睡眠分开管理或者改用nanosleep()和timer_create()这类更精细的接口。4. 实操用代码和命令真正玩转信号值理论说完了上实操。这一节我分两部分命令行下的信号操作以及代码层面的信号发送与接收。4.1 命令行快速上手kill、trap、killall很多人以为kill就是“杀进程”其实它的本意是“发信号”。验证方法很简单kill -l这条命令会列出当前系统支持的所有信号名称和编号。在你自己的机器上跑一下能看到SIGALRM的编号也能快速确认平台的信号表。查看某个具体信号的信息可以用man 7 signal里面有一张完整的信号对照表还包括每种信号在不同场景下的默认行为非常值得精读。如果我写了一个脚本想让它响应SIGINTCtrlC做清理操作可以这样#!/bin/bash cleanup() { echo 捕获到中断信号清理临时文件... rm -f /tmp/myapp_*.tmp exit 0 } trap cleanup SIGINT echo 脚本运行中按 CtrlC 测试信号捕获... while true; do sleep 1 done脚本里的trap命令就是shell层面的信号处理函数。这个技巧在运维脚本里非常实用。比如你用脚本跑一个长时间的数据迁移任务希望用户按 CtrlC 时能保存进度而不是直接中断就可以用trap优雅收尾。killall也是一个高频命令它的作用是按进程名发信号killall -15 my_server这个命令会向所有名为my_server的进程发送SIGTERM。要注意killall在有些系统上是按精确匹配进程名的如果名字写错它不会报错而是告诉你“no process found”处理时要先确认进程名是否正确。4.2 用C代码显式发送信号kill、raise、sigqueue命令行只是壳真正的控制力在代码里。C语言里发送信号有三个主力函数#include signal.h #include sys/types.h #include unistd.h // 向指定进程发送信号 kill(pid, SIGUSR1); // 向当前进程发送信号相当于 kill(getpid(), sig) raise(SIGUSR1); // 发送带附加参数的实时信号 union sigval val; val.sival_int 42; sigqueue(pid, SIGRTMIN 1, val);kill()是最常用的。比如父进程需要通知子进程“数据准备好了”就可以让子进程注册SIGUSR1处理函数父进程通过kill(child_pid, SIGUSR1)发送通知。一个完整的例子展示父子进程通过信号协作#include stdio.h #include stdlib.h #include signal.h #include unistd.h #include sys/wait.h #include string.h volatile sig_atomic_t child_ready 0; void child_handler(int signum) { if (signum SIGUSR1) { child_ready 1; } } int main() { pid_t pid fork(); if (pid 0) { perror(fork); exit(1); } if (pid 0) { // 子进程注册信号处理函数然后等待父进程通知 struct sigaction sa; memset(sa, 0, sizeof(sa)); sa.sa_handler child_handler; sigaction(SIGUSR1, sa, NULL); printf([子进程] PID%d等待父进程信号...\n, getpid()); while (!child_ready) { pause(); } printf([子进程] 收到信号开始干活\n); exit(0); } else { // 父进程给子进程一点准备时间然后发信号 sleep(1); printf([父进程] 向子进程发送 SIGUSR1\n); kill(pid, SIGUSR1); int status; waitpid(pid, status, 0); printf([父进程] 子进程已退出status%d\n, status); } return 0; }编译运行gcc -o signal_demo signal_demo.c ./signal_demo4.3 处理函数注册signal() 与 sigaction() 的取舍我建议直接放弃signal()统一用sigaction()。原因很明确signal()的行为在历史上不同Unix版本里有差异有的系统注册后会自动重置有的不会。sigaction()可以通过sa_mask指定在信号处理函数执行期间屏蔽哪些信号避免嵌套干扰。sigaction()可以通过sa_flags控制更多细节比如SA_RESTART让被信号打断的系统调用自动重启、SA_SIGINFO使用带参数的处理函数。sa_flags里的SA_RESTART尤其值得注意。前面提到SIGALRM会打断阻塞的系统调用让它返回EINTR。如果你不想处理EINTR只想让系统调用“假装没被打断”就可以设置SA_RESTART。但并不是所有系统调用都支持自动重启比如select()、poll()、epoll_wait()这些就不一定。所以工程上还是要做好处理EINTR的准备// 以 recv 为例在循环里重试 ssize_t n; do { n recv(fd, buf, sizeof(buf), 0); } while (n 0 errno EINTR);每次循环遇到EINTR就重试这是最稳妥的写法。5. 常见问题与排查技巧实录下面是我实际开发过程中遇到的、以及身边同事经常踩的一些信号相关的坑整理成速查表每个背后都有真实的教训。现象可能原因解决方案程序莫名其妙被终止没有日志触发了某个信号的默认行为比如 SIGSEGV 或 SIGALRM 未捕获用dmesg查内核日志用gdb跑一次看程序死在哪里信号处理函数只执行了一次后面没反应使用了signal()信号到达后处理函数被重置改用sigaction()并且注册时不要省略sa_mask进程收到信号后行为错乱或死锁在处理函数里调用了非异步信号安全函数如malloc()、printf()处理函数里只设置标志位或者只调用sig_atomic_t能承载的操作SIGALRM设置了但迟迟不触发setitimer()的it_value没设对或者被alarm()、sleep()干扰检查it_value是否清零统一用setitimer管理定时器kill -9杀不掉进程进程处于不可中断的内核态D状态常见于NFS挂死等待内核I/O恢复如果持续D状态可能需要重启机器或容器子进程变成僵尸进程父进程没有调用wait()/waitpid()注册SIGCHLD处理函数在函数里调用waitpid(-1, NULL, WNOHANG)调用recv()每次都被打断收到了信号且未设置SA_RESTART在sigaction中设置SA_RESTART或者循环处理EINTR5.1 实测SIGALRM 未捕获导致程序退出有一次我写了个守护进程用setitimer周期执行数据清理。本地测试一切正常部署到服务器上跑了两天进程神秘消失。查日志无果最后用dmesg看到这样一行[123456.789] my_daemon[5678]: segfault at 0 ip ...并不是SIGALRM的问题而是一个空指针。但排查过程中我发现了一个更隐蔽的问题团队里另一位同事在另一个模块里调用了signal(SIGALRM, SIG_IGN)导致我的定时器信号被全局屏蔽了清理逻辑从来没有真正执行过。这就是信号机制的核心特点它是进程级的不是模块级的。任何一个组件对信号处理函数的修改都会影响到整个进程。所以在大项目里对于信号的注册和使用最好有一个统一的规划别到处散着注册。5.2 排查工具与方法如果你怀疑进程的信号处理有问题有几个实用工具strace是最强的调试利器可以直接跟踪系统调用和信号传递strace -e tracesignal -p 1234这个命令可以看到进程1234收到的所有信号以及它调用sigaction、kill等系统调用的情况。排查“信号到底有没有发出去”的时候这个命令直接能给出答案。gdb则适合调试信号处理函数本身gdb -p 1234 (gdb) handle SIGALRM stop (gdb) continue这样设置之后当进程收到SIGALRM时会停在原地而不是直接执行处理函数。你就有机会查看处理函数内部的变量状态确认逻辑是否正确。cat /proc/pid/status可以看到进程当前对各个信号的屏蔽状态grep Sig /proc/1234/status输出的SigBlk、SigIgn、SigCgt分别表示被阻塞、被忽略、被捕获的信号集合按位图格式显示。拿这个和标准信号表对照能看出有哪些信号被进程屏蔽了。5.3 关于信号处理函数的安全边界最后多聊几句异步信号安全。这是很多人忽略、却在生产环境里出过事的点。信号处理函数是异步执行的——你的主程序可能正在执行任意一条指令信号来了之后处理函数就在那条指令的“中间”插入执行。如果处理函数里调用了malloc()而主程序恰好也正在执行malloc()就可能造成堆内存的重复分配或链表损坏。同理printf()内部使用锁机制如果主程序正在执行printf()处理函数又调用printf()在某些实现下可能直接死锁。所以信号处理函数里“能做什么”是一个极其严格的清单可以安全调用绝对不能调用write()printf()、fprintf()close()malloc()、free()read()pthread_*系列容易死锁getpid()syslog()sig_atomic_t赋值和读取std::cout等C流操作_exit()注意不是 exitexit()会刷新缓存可能死锁最稳健的模式还是我前面说的处理函数里只做一件事——设置一个volatile sig_atomic_t标志位。主循环轮询这个标志发现有变化就执行真正的业务逻辑。虽然看起来绕了一圈但这是踩过坑的人总结出来的稳妥方案。6. 信号值选型与业务设计建议很多人把信号当成系统底层的小工具用的时候随手抄一个SIGUSR1或SIGALRM其实信号也有一套“设计规范”尤其在大型服务里选错信号可能导致业务逻辑混乱。6.1 标准信号 vs 实时信号怎么选标准信号1-31的优点简单、通用、所有平台都有。缺点不排队、易丢失、携带的信息量少。实时信号34-64的优点排队、可携带整数参数、可用于精确的进程间通信。缺点并非所有老旧系统都完全支持编号在不同Unix间不一致SIGRTMIN的起始地址不同。如果只是通知进程“有事件发生了”不需要携带信息直接用SIGUSR1或SIGUSR2就够了。如果需要区分事件的类型比如“重载配置”“暂停服务”“恢复服务”三种指令那就要考虑三种信号或者用实时信号带上一个值来区分。我的经验是进程间通信优先用套接字或消息队列信号适合的是“轻量级的事件通知”。一旦业务逻辑复杂到需要传递各种参数信号能承载的信息量就不够用了。6.2 自定义信号使用规范如果项目里确实要用信号做业务通知建议定一套简单规则全局只允许一个模块统一注册信号处理函数其他模块不得自行注册。业务控制类信号固定用SIGUSR1重载配置和SIGUSR2优雅关闭不要混用。定时任务统一用SIGALRM或timer_create()与SIGEV_THREAD后者更灵活直接在独立线程里执行回调。代码里禁止出现裸数字信号值必须用宏名。处理函数里禁止执行复杂逻辑只做标志位设置。6.3 一个稳妥的封装示例举个例子你可以在自己的公共库里做一层封装typedef void (*user_signal_handler_t)(int); int register_user_signal(int signum, user_signal_handler_t handler) { struct sigaction sa; memset(sa, 0, sizeof(sa)); sa.sa_handler handler; sigemptyset(sa.sa_mask); sa.sa_flags SA_RESTART; return sigaction(signum, sa, NULL); }调用方只需要传信号编号和处理函数指针不需要关心底层细节。这样做的好处是将来如果要修改信号处理的策略比如加一个公共的日志钩子只需要改这一层业务代码不动。7. 把信号机制装进你的工具箱信号这个东西刚接触的时候觉得“不过是个杀进程的命令”但用熟了以后会发现它其实是深入理解Linux进程模型的一把钥匙。我在实际项目中信号最常用的三个地方一是服务优雅退出用SIGTERM做清理二是定时任务用SIGALRM做周期调度三是父子进程协作用SIGUSR1/SIGUSR2做事件通知。这三个场景覆盖了绝大多数业务需求把这几个信号吃透就够应付日常开发了。遇到信号相关的问题先别急着看代码先用kill -l、strace、grep Sig /proc/pid/status这些工具把现场信息收集齐再定位是哪一环出了问题。排查信号的bug最怕“瞎猜”有了工具辅助很快就能锁定根因。最后再分享一个小技巧写涉及信号处理的代码时我习惯在开发环境的终端里开一个watch -n 1 cat /proc/pid/status | grep Sig随时观察信号位图的变化。实际调试时非常高效能看到信号从发出到处理完成的整个状态流转比加一堆日志省力得多。信号机制不难但一定要边写边验证积累出自己的直觉。