1. 信号基础:信号的概念与生命周期、信号的分类(标准信号与实时信号)、常见信号列表
信号,说白了就是软件层面的中断。我刚开始接触Linux编程时,总觉得信号这玩意儿有点玄乎——不就是发个通知吗?后来在项目中踩过几次坑,才真正明白它的分量。
你想想看,一个进程正在埋头苦干,突然有人拍它肩膀说「停一下」。这个「拍肩膀」的动作,就是信号。它不传递复杂数据,只传递一个编号。内核收到这个编号后,会帮进程切换到信号处理函数去执行。嗯,就是这么个机制。
1.1 信号的生命周期:从生到死
一个信号从产生到被处理,要经历三个阶段:产生、挂起、递达。我个人习惯把这三个阶段比作「发信、在路上、收信」。
- 产生:信号被某个事件触发。比如你按下 Ctrl+C,终端驱动会向前台进程组发送 SIGINT。或者你调用 kill() 系统调用,手动给另一个进程发信号。
- 挂起:信号已经生成,但目标进程还没空处理它。这时候信号被存放在进程的 pending 信号队列里。注意,标准信号在 pending 队列里只能存一份——如果你连续发 10 个 SIGINT,进程最终只收到 1 个。实时信号则不同,它会排队,发 10 个就收 10 个。
- 递达:进程调度到 CPU 后,内核检查 pending 信号,执行对应的处理动作。可能是默认动作(比如终止进程),也可能是你注册的自定义处理函数。
关键点:信号递达的时机,是在进程从内核态返回用户态的前一刻。也就是说,如果进程一直在内核态忙活(比如执行系统调用),信号会被暂时阻塞,直到系统调用返回。
我在项目中遇到过一个问题:一个网络服务进程,用 epoll 做 IO 多路复用。我给它注册了 SIGTERM 信号处理函数,希望优雅关闭。结果发现信号经常延迟很久才被处理。后来排查发现,epoll_wait 被信号中断后会返回 EINTR,但我的代码没正确处理这个错误码,导致信号处理函数迟迟没被调用。嗯,这个坑我记了很久。
1.2 信号的分类:标准信号 vs 实时信号
Linux 信号分为两大类:标准信号(编号 1-31)和 实时信号(编号 32-64)。这两者有什么区别?我直接说结论:
| 特性 | 标准信号 | 实时信号 |
|---|---|---|
| 编号范围 | 1-31 | 32-64 |
| 排队机制 | 不排队,相同信号只保留一份 | 排队,支持多个相同信号 |
| 传递顺序 | 无严格顺序 | 按发送顺序递达 |
| 携带数据 | 不能携带额外数据 | 可通过 sigqueue() 携带整型或指针数据 |
| 可靠性 | 可能丢失(相同信号覆盖) | 可靠,不会丢失 |
为什么会这样?标准信号设计于早期 Unix 时代,那时候资源紧张,内核不想为每个信号都维护一个队列。实时信号是 POSIX 标准后来加的,专门解决信号丢失和顺序问题。
我的建议:日常开发中,用标准信号就够了。只有当你需要传递少量数据,或者必须保证每个信号都被处理时,才考虑实时信号。我在一个实时数据采集项目里用过实时信号,用来通知采集线程有新的数据块到达,效果不错。
1.3 常见信号列表:你一定会遇到的几个
Linux 定义了 31 个标准信号,但实际开发中常用的就那么几个。我挑最重要的说:
- SIGINT (2):终端中断。你按 Ctrl+C 时触发。默认动作是终止进程。进程可以捕获它,用来做清理工作后再退出。
- SIGTERM (15):终止信号。这是 kill 命令默认发送的信号。它给进程一个优雅退出的机会——你可以捕获它,关闭文件描述符、释放资源、保存状态。
- SIGKILL (9):强制终止。这个信号不能被捕获、不能被忽略、不能被阻塞。说白了就是「核弹」,直接干掉进程。我建议只在万不得已时使用,比如进程卡死无法响应 SIGTERM。
- SIGSEGV (11):段错误。访问非法内存地址时触发。比如解引用空指针、数组越界。默认动作是终止并生成 core dump 文件。
- SIGPIPE (13):管道破裂。向一个已经关闭读端的管道写数据时触发。默认动作是终止进程。网络编程中常见——客户端断开连接后,服务端继续 write 就会收到 SIGPIPE。
- SIGCHLD (17):子进程状态改变。子进程退出、暂停或恢复时,父进程会收到这个信号。我习惯用它来异步回收子进程资源,避免僵尸进程。
- SIGUSR1 (10) / SIGUSR2 (12):用户自定义信号。内核不会主动产生它们,完全由开发者自己定义用途。比如用来通知进程重新加载配置文件。
避坑指南:我曾经在写一个守护进程时,想用 SIGUSR1 触发配置重载。结果发现信号处理函数里调用了 printf——这在信号处理函数中是极其危险的!因为 printf 不是异步信号安全的。正确的做法是:在信号处理函数里只设置一个 volatile sig_atomic_t 标志位,然后在主循环中检查这个标志位。
1.4 知识体系总览
下面这张图,把信号的核心概念串起来了。我建议你多看几遍,理解信号从产生到处理的完整路径。
这张图把信号从产生到递达的路径画清楚了。你注意看中间那个「挂起」阶段——标准信号和实时信号在这里分道扬镳。标准信号像邮局平信,丢了就丢了;实时信号像顺丰快递,每一件都有追踪号。
好了,信号的基础概念就聊到这儿。记住三个核心点:信号是软件中断、标准信号不排队、SIGKILL 是核弹。下一节我们聊聊怎么用代码操作信号——signal() 和 sigaction() 的区别,以及我踩过的那些坑。
一句话总结:信号是 Linux 进程间通信最轻量的方式,但也是最容易被误用的。理解它的生命周期和分类,是写出健壮信号处理代码的第一步。