信号基础:信号的概念与作用

信号,说白了就是软件层面的中断。我刚开始学Linux编程时,觉得信号这东西挺玄乎的——不就是发个通知吗?后来踩过几次坑才明白,信号是进程间通信最基础、最轻量的方式之一。

它的作用很简单:通知进程某个事件发生了。比如你按Ctrl+C,系统就给前台进程发个SIGINT;你写了个野指针,内核就发个SIGSEGV。进程收到信号后,可以选择忽略、处理或者按默认方式行事。

核心要点:信号是一种异步事件处理机制。发送方不需要等待接收方处理完毕,接收方也不需要时刻检查有没有信号到来。

信号的分类

可靠信号 vs 不可靠信号

这个分类其实跟历史有关。早期Unix的信号是不可靠的,什么意思呢?就是信号可能会丢失。比如你连续发了两个相同的信号,进程可能只收到一个。

我曾在项目中遇到过这个问题:一个守护进程需要处理SIGCHLD信号来回收子进程,结果高并发下子进程结束太快,信号丢失了,导致僵尸进程堆积。后来改用可靠信号才解决。

特性 不可靠信号(1-31) 可靠信号(SIGRTMIN-SIGRTMAX)
是否排队 不排队,可能丢失 排队,不会丢失
是否支持携带数据 不支持 支持(通过sigqueue)
标准定义 POSIX.1标准 POSIX.1实时扩展

实时信号 vs 非实时信号

实时信号其实就是可靠信号。它们有优先级,编号越小优先级越高。而且实时信号支持排队,你发多少个就能收到多少个,一个不落。

非实时信号呢?就是前面说的不可靠信号。它们不排队,而且如果进程正在处理某个信号,同类型的信号会被阻塞,处理完后只保留一个。

我的建议:新写的代码尽量用实时信号。虽然大部分场景下非实时信号够用,但万一哪天业务量上来了,信号丢失的坑可不好填。

常见信号

Linux下信号有好几十种,但常用的就那么几个。我列个表,你一看就明白:

信号名 编号 默认行为 典型触发场景
SIGINT 2 终止进程 Ctrl+C
SIGKILL 9 终止进程(不可捕获) kill -9
SIGSEGV 11 终止进程+core dump 访问非法内存地址
SIGTERM 15 终止进程 kill命令默认发送
SIGCHLD 17 忽略 子进程状态改变
SIGPIPE 13 终止进程 向已关闭的管道写数据

注意:SIGKILL和SIGSTOP这两个信号不能被捕获、阻塞或忽略。这是系统给你的最后手段。我曾经见过有人试图捕获SIGKILL来做清理工作,结果发现根本行不通。

信号的产生方式

信号怎么来的?我总结了几种常见方式:

  • 键盘输入:Ctrl+C产生SIGINT,Ctrl+\产生SIGQUIT
  • 硬件异常:除零产生SIGFPE,非法内存访问产生SIGSEGV
  • 系统调用:kill()、raise()、alarm()、sigqueue()等
  • 软件条件:比如SIGURG(带外数据到达)、SIGALRM(定时器到期)
  • 进程间通信:一个进程向另一个进程发送信号

举个例子,你写了个死循环程序,想让它停下来。你按Ctrl+C,终端驱动检测到这个组合键,就给前台进程组发SIGINT。进程收到后,默认就终止了。简单吧?

信号的默认处理方式

每个信号都有默认行为,大致分这么几类:

  1. 终止进程:大部分信号都这样,比如SIGINT、SIGTERM
  2. 终止进程+core dump:比如SIGSEGV、SIGABRT。core dump可以帮你调试,但生产环境一般关掉
  3. 忽略:比如SIGCHLD、SIGURG
  4. 暂停进程:比如SIGSTOP
  5. 恢复进程:比如SIGCONT

避坑指南:我曾经在写一个网络服务时,忘了处理SIGPIPE。结果客户端断开连接后,服务端继续往socket写数据,直接挂了。后来加上SIGPIPE的忽略处理才稳定下来。

信号的生命周期

一个信号从产生到被处理,经历三个阶段:

  • 产生:事件发生,信号被生成
  • 递送:信号被发送给目标进程
  • 处理:进程执行信号处理函数(或默认行为)

但这里有个关键点:信号不是立即处理的。进程会在从内核态返回用户态时,检查是否有待处理的信号。所以信号处理是有延迟的。

我画了个流程图,帮你理解这个过程:

信号生命周期流程图 阶段1:信号产生 键盘/硬件/系统调用 阶段2:信号递送 内核发送给目标进程 阶段3:信号处理 执行处理函数/默认行为 信号处理的分支路径 默认处理 终止/暂停/忽略/core dump 捕获处理 调用signal()/sigaction() 忽略 SIG_IGN 信号处理时机:进程从内核态返回用户态时 阻塞:信号被屏蔽,暂不递送 → 未决:信号已产生但尚未处理 解除阻塞后,未决信号会被递送

信号的阻塞与未决

这两个概念容易搞混,我当年也迷糊过一阵子。

阻塞,就是进程告诉内核:这个信号我现在不想处理,你先帮我挡着。进程可以设置信号屏蔽字,指定哪些信号需要阻塞。

未决,就是信号已经产生了,但因为被阻塞了,还没递送给进程。这些信号会待在未决信号集里,等阻塞解除后再处理。

举个例子:你正在写一个关键的数据更新操作,不希望被SIGINT打断。你可以先阻塞SIGINT,等数据更新完了再解除阻塞。这期间如果有人按Ctrl+C,信号不会丢失,而是变成未决状态,等你解除阻塞后再处理。

关键区别:

  • 阻塞是进程主动设置的,是一种保护机制
  • 未决是信号产生后的中间状态,是被动的
  • 阻塞的信号可以变成未决,但未决的信号不一定是因为阻塞

我曾经踩过的坑:在信号处理函数里调用printf。printf不是可重入函数,信号处理函数中调用它可能导致死锁。后来我改用write系统调用,或者设置一个标志变量,在主循环中检查处理。

嗯,信号的基础知识就这些。理解这些概念后,你就能更好地掌握信号的使用了。记住一点:信号是异步的,处理时要小心竞态条件。多线程环境下更要谨慎,因为信号的处理是进程级的,不是线程级的。


公众号:蓝海资料掘金营,微信deep3321