📚 本文收录于「流浪」的系列专栏
| 🐧 Linux系统 | 进入专栏 → |
| ⚙️ C++ | 进入专栏 → |
| 📊 数据结构与算法 | 进入专栏 → |
| 🐍 Python | 进入专栏 → |
| 🔗 LangChain & LangGraph | 进入专栏 → |
| 🗄️ MySQL 数据库 | 进入专栏 → |
| 🌿 Git 工具 | 进入专栏 → |
| 🌐 计算机网络 | 进入专栏 → |
| 🤖 AI | 进入专栏 → |
| 💯 大厂面试、八股 | 进入专栏 → |
| 📚 学习筑基专栏 | 进入专栏 → |
🏠 博客主页:流浪 | 📝 原创首发于 CSDN
前言:篇28 把信号量讲透了——计数器、P/V、电影院订票。但通信系列里还有个"信号(signal)"一直没单独立篇。它和信号量只差一个字,机制却完全无关:信号是 OS 给进程的一记"异步通知",Ctrl+C、除零崩溃、管道写穿全靠它。这篇把信号从"是什么"讲到"内核怎么发",顺手把和信号量的区别钉死。
一个问题:你按下 Ctrl+C,背后到底发生了什么?
你按下 Ctrl+C,前台进程瞬间没了——可这背后 OS 到底干了什么?为什么是"瞬间",为什么只能杀前台、杀不掉后台,为什么子进程变成孤儿就按不死?
再想想:程序里写个 while(1) 除个零,进程直接被内核"崩掉"还吐个 core,这"崩"又是谁干的?
这些日常现象背后,站着同一个机制——信号。它不是你主动调的函数,是 OS 在关键时刻"拍你一下"的通知。本篇先把信号"是什么、从哪来、发给谁"讲透。
一、信号到底是什么
1.1 一句话:进程间的一记"异步通知"
信号的准确定义是:进程间的事件通知机制,更准确地说是 OS 给进程发的通知。
它和管道、消息队列最本质的区别在于:信号不带数据,只通知"某类事件发生了",具体怎么响应由进程自己定。管道是传字节流、消息队列是传数据块,信号是"有件事发生了,你自己看着办"。这一点决定了它轻量,但也决定了它不适合传业务数据。
1.2 生活里咋理解:闹钟、红绿灯、狼烟
- 闹钟:你睡觉时不知道几点响,响了你才反应——你没有被实时盯着,事件到了才响应,这就是异步。
- 红绿灯:你过马路不被交警实时指挥,灯变了你自行决定走还是停。
- 狼烟:边防不派信使跑全程,点一炷烟通知"有敌情",远处自己解读。
它们的共同点:发通知的人不盯着你处理,你该干啥干啥,事件到了你再响应。
1.3 关键:它跟进程运行是"异步"的
"异步"是信号最难理解、也最重要的一个词。
异步 = 信号在进程运行的任意时刻都可能送达,不挑进程停在哪行代码上。进程不能像 read 那样"主动阻塞等信号",信号是 OS 在中断返回、系统调用返回等时机"插进来"的。
这带来一个必然结果:信号不能到了就立刻处理,必须先记下、等合适时机再处理。(二)里讲发送机制时会看到,这个"先记下"就是内核用位图记录信号的由来。
1.4 先记住四条
1.5 查看Linux操作系统信号的命令
kill -l

二、信号不等于信号量,别搞混【回指篇28】
2.1 为啥老有人混:就差一个字
"信号 signal" 和 "信号量 semaphore",中文只差一个"量"字,但机制是两个世界。很多初学者学到这就晕,所以专门拎出来说清楚。
2.2 一张表看明白
| 是什么 | 异步事件通知 | 同步互斥的计数器 |
| 干什么 | 告诉进程"某事件发生了"(如 SIGKILL、SIGSEGV) | 管"资源还剩几份、谁能进临界区" |
| 谁发的 | OS(键盘 / 异常 / 进程请求触发) | 进程自己通过 semop 操作 |
| 带数据吗 | 不带,只通知 | 带计数值 |
| 怎么用 | kill 发送 / signal 注册处理函数 | semget / semop 做 P、V |
2.3 一句口诀记住
篇28 第九节 9.1 已经专讲过这个对比,这里只回指不动重讲:"信号是叫你一下,信号量是数你一下。" 一个字之差,两个世界,面试被问到这句话答出来就够。
三、信号都是从哪冒出来的
3.1 五种来源
- 键盘:Ctrl+C → 给前台发 SIGINT。
- 系统调用:kill() / raise() / abort() 主动发。
- 硬件异常:除零 → SIGFPE;解引用空/野指针 → SIGSEGV。
- 软件条件:管道读端关了还写 → SIGPIPE;alarm 计时到点 → SIGALRM。
- 系统命令:kill 命令给进程发信号。
3.2 为啥来源这么多:OS 是唯一总入口
看起来来源五花八门,但有一个统一点:无论哪条路,最终都由内核产生并投递信号。
用户进程自己不能直接"改写"另一个进程的信号状态,必须经过内核。所以信号看似来源杂,实则统一收口在 OS——这个点先记住,下一篇讲发送机制时就是它的展开。
四、普通信号(1–31)是什么
普通信号(传统信号):属于早期 Unix 信号,编号 1‑31,不排队,同一个信号多次到来只会保留一次,信号会合并丢失,进程处理时可以被其他信号打断,不保证接收顺序,适用于终止、中断这类简单通知场景。
用 kill -l 能看到系统一共 64 个信号,编号 1–64。1–31 是标准(普通)信号,32–64 归实时信号(下一节讲)。先说普通信号怎么来的。
4.1 键盘按出来的:Ctrl+C
Ctrl+C 给当前前台进程组发 SIGINT(2),默认动作是终止进程。
注意"前台进程组"这四个字,它解释了为什么后台进程按 Ctrl+C 杀不掉——这点第六章专门讲。
4.2 代码里发:kill() 和 raise()
kill(pid, sig):给指定进程发信号(有限制:要么同用户,要么 root)。
int kill(pid_t pid, int sig);
raise(sig):给自己发,等价于 kill(getpid(), sig)。
int raise(int sig);
4.3 自己干自己:abort() 发 6 号
abort() 是 C 标准库函数,它给自己发 SIGABRT(6),而且它保证进程最终一定终止——即使你自定义捕获了 SIGABRT,只要 handler 返回,abort() 会先把处置重置成默认,再发一次,直到进程被干掉。
void abort(void);
为什么 abort 一定能终止、而 kill/raise 发的普通信号不一定?这是个经典追问,留到(二)第六篇 细讲。
4.4 硬件出错:除零、野指针
- 除零 → SIGFPE(8)(floating-point exception,虽叫浮点异常,整数除零也归它)。
- 解引用空指针/野指针 → SIGSEGV(11)(segmentation violation)。
关键点:这些不是你代码主动调函数触发的,是 CPU 执行指令出错,内核捕获后转成信号发给进程。所以你看到的"程序崩了",本质是内核给你发了个信号、默认动作是终止+core。
4.5 软件条件:管道写穿 SIGPIPE、alarm 计时
- 管道读端关闭后还往里写 → 内核发 SIGPIPE(13),默认终止写进程。写网络程序时这个信号常被忽略或自定义处理,否则一个对端断开就崩。
- alarm(seconds) 计时到点 → 内核发 SIGALRM(14),常用于超时控制。
4.6 命令行:kill 命令
kill -9 PID 发 SIGKILL(9);kill -l 列出全部信号名。记住 SIGKILL=9,它是面试和排障都高频的信号(第七章 7.4 会讲它为什么谁都拦不住)。
五、实时信号(34–64)是什么
5.1 实时信号是什么
普通信号(传统信号):属于早期 Unix 信号,编号 1‑31,不排队,同一个信号多次到来只会保留一次,信号会合并丢失,进程处理时可以被其他信号打断,不保证接收顺序,适用于终止、中断这类简单通知场景。
内核视角,实时信号编号是 32–64(共 33 个)。但 glibc 的 NPTL 线程库内部占用了 32、33 两个,所以用户程序真正可用的实时信号从 SIGRTMIN(通常是 34)起。
真实内核里 32/33 也是实时信号编号,只是被线程库内部用掉了,普通程序碰不到。面试被问到"实时信号从几号开始",答"内核32起,但用户态通常34起"最严谨。
5.2 跟普通信号差在哪
| 排队 | 不排队,重复发可能丢失 | 支持排队,不丢失 |
| 顺序 | 多个未决时按编号从小到大 | 按优先级 / 发送顺序 |
| 带数据 | 不带 | 可用 sigqueue 带附加数据 |
5.3 点到为止
面试常问"实时信号解决什么问题"——就是普通信号会丢失、不排队的短板。日常业务用普通信号足够,实时信号用在不能丢事件的场景(如高速数据采集)。本篇不深,知道差别即可。
六、信号发给谁:前台还是后台【回指前面文章】
6.1 什么是前台进程、后台进程
前台进程:占据终端交互界面的进程,直接接收键盘输入,终端被它占用,执行时用户不能在该 shell 敲其他命令,Ctrl+C 可以终止它。 后台进程:脱离终端输入,在系统后台默默运行,不抢占命令行,shell 可以继续接收新命令;但它的输出依旧会打印到终端,收到 SIGINT 信号不会终止,一般用&符号把程序放到后台运行。
6.2 关键:键盘信号只发给前台
Ctrl+C 这类键盘信号,只投递给前台进程组。后台进程不在前台进程组里,所以收不到键盘信号。
这就是你在终端跑 ./a.out & 之后,按 Ctrl+C 杀不掉它的原因——它已经不是前台了。
6.3 前台只能一个、后台能多个
一个终端同一时刻只有一个前台进程组(键盘输入总得有个明确目标),但后台可以有多个进程。这个"唯一前台"的约束,是键盘信号能精确投递的前提。
6.4 再识孤儿进程
- Ctrl+C 发送 SIGINT信号,只作用于当前前台进程组。
- 父进程退出 → 子进程成为孤儿,被 init/systemd 收养(由前台进程转变为后台进程),脱离当前终端进程组。
- 此时按 Ctrl+C ,信号不会发给孤儿进程,所以杀不死。
6.5 如何进行前后台转换
jobs //查看后台任务
fg +任务号 //将特定的进程提到前台
Ctrl+Z //暂停进程 也可以将前台进程挂到后台
bg +任务号 //将后台进程运行起来
七、面试官爱问(带答案)
7.1 信号为什么是"异步"的?
答:信号可在进程运行的任意时刻由 OS 投递,进程并不主动等待它;OS 只在从内核态返回用户态等时机才检查并处理挂起信号,所以天然异步。正是异步性,决定了信号必须"先记录、后处理"。
7.2 普通信号和实时信号到底差在哪?
答:普通信号(1–31)不排队、重复发送可能丢失;实时信号(用户态通常 34 起)支持排队、不丢失,还能通过 sigqueue 带附加数据。普通信号够日常用,实时信号用于不能丢事件的场景。
7.3 Ctrl+C 为什么只能杀前台进程?
答:按下 Ctrl+C,终端驱动会向前台进程组发送 SIGINT(中断信号)。后台进程不属于当前终端的前台进程组,因此接收不到这个键盘产生的 SIGINT 信号,所以 Ctrl+C 无法终止后台进程。想要杀掉后台进程,需要用 kill 命令主动给它发送信号。
7.4 SIGKILL(9) 为什么谁都杀得死、还拦不住?
答:POSIX 规定 SIGKILL 和 SIGSTOP 是两个不可捕获、不可阻塞、不可忽略的信号(man7 signal.7 原文:"cannot be caught, blocked, or ignored")。连默认处理函数都换不掉,所以必杀——这也是为什么 kill -9 是最后的手段。
7.5 abort() 发 6 号为什么能终止、kill/raise 却不一定?
答(钩子):abort() 发 SIGABRT(6),但它对 SIGABRT 有特殊保证——你若捕获了且 handler 返回,它会把处置重置成默认再发一次,忽略也一样重置再发,所以最终一定终止。而 kill/raise 若发的是可被捕获/忽略的信号(如 SIGTERM),进程完全可以自定义 handler 选择不终止。完整机制留(二)第六篇 6.1 展开。
八、小结
用三句话收尾:
下一篇(二)钻进内核:信号产生后怎么被记下来、又是谁、怎么"写"进目标进程的,进程收到后怎么应对,以及把信号和信号量、管道的区别彻底钉死。
💬 看完如果有一句没懂,或者你手上的课件图跟我这里解读对不上,评论区直接说,我补图补例子。也欢迎把本篇和篇28(信号量)串起来复习——它俩名字像,但真不是一个东西。
网硕互联帮助中心




评论前必须登录!
注册