news 2026/9/12 19:18:13

Linux系统篇29——信号(一):从 Ctrl+C 到内核位图,信号“是什么、从哪来“一次讲透

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux系统篇29——信号(一):从 Ctrl+C 到内核位图,信号“是什么、从哪来“一次讲透

📚本文收录于「流浪」的系列专栏

系列专栏直达链接
🐧 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. 早知道怎么处理:信号处理方式是信号还没产生前就注册好的,OS 天生给每个信号准备了默认处理动作。
  2. 不立即处理:收到信号不会马上打断当前指令去执行处理,而是等到从内核态返回用户态等合适时机才处理。
  3. OS 内置识别:进程之所以能识别信号,是因为内核在创建进程时就内置了整套信号机制。
  4. 信号源非常多:键盘、系统调用、硬件异常、软件条件、命令,都能产生信号(下一节展开)。

1.5 查看Linux操作系统信号的命令

kill -l


二、信号不等于信号量,别搞混【回指篇28】

2.1 为啥老有人混:就差一个字

"信号 signal" 和 "信号量 semaphore",中文只差一个"量"字,但机制是两个世界。很多初学者学到这就晕,所以专门拎出来说清楚。

2.2 一张表看明白

维度信号 signal信号量 semaphore
是什么异步事件通知同步互斥的计数器
干什么告诉进程"某事件发生了"(如 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
  • 软件条件:管道读端关了还写 →SIGPIPEalarm计时到点 →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); #include <signal.h> #include <iostream> int main() { while (true) { std::cout << "进程:" << getpid() << "即将结束" << std::endl; sleep(2); int n = kill(getpid(), 2); } return 0; }

raise(sig):给自己发,等价于kill(getpid(), sig)

int raise(int sig);

4.3 自己干自己:abort() 发 6 号

abort()是 C 标准库函数,它给自己发SIGABRT(6),而且它保证进程最终一定终止——即使你自定义捕获了 SIGABRT,只要 handler 返回,abort()会先把处置重置成默认,再发一次,直到进程被干掉。

void abort(void); int main() { while (true) { std::cout << "进程:" << getpid() << std::endl; abort(); } return 0; }

为什么 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 PIDSIGKILL(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 跟普通信号差在哪

维度普通信号(1–31)实时信号(34–64)
排队不排队,重复发可能丢失支持排队,不丢失
顺序多个未决时按编号从小到大按优先级 / 发送顺序
带数据不带可用 sigqueue 带附加数据

5.3 点到为止

面试常问"实时信号解决什么问题"——就是普通信号会丢失、不排队的短板。日常业务用普通信号足够,实时信号用在不能丢事件的场景(如高速数据采集)。本篇不深,知道差别即可。


六、信号发给谁:前台还是后台【回指前面文章】

6.1 什么是前台进程、后台进程

前台进程:占据终端交互界面的进程,直接接收键盘输入,终端被它占用,执行时用户不能在该 shell 敲其他命令,Ctrl+C 可以终止它。
后台进程:脱离终端输入,在系统后台默默运行,不抢占命令行,shell 可以继续接收新命令;但它的输出依旧会打印到终端,收到 SIGINT 信号不会终止,一般用&符号把程序放到后台运行。

int main() { while(true) { std::cout<<"我是一个进程:"<<getpid()<<std::endl; sleep(1); } return 0; }

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 规定SIGKILLSIGSTOP是两个不可捕获、不可阻塞、不可忽略的信号(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 展开。


八、小结

用三句话收尾:

  1. 是什么:信号是 OS 给进程的异步事件通知,不带数据,只告诉你"某事件发生了"。
  2. 从哪来:键盘、系统调用、硬件异常、软件条件、命令五路来源,最终统一收口内核;普通信号 1–31,实时信号用户态通常 34 起。
  3. 发给谁:键盘信号只发前台进程组,后台收不到;信号和信号量只差一字、机制无关。

下一篇(二)钻进内核:信号产生后怎么被记下来、又是谁、怎么"写"进目标进程的,进程收到后怎么应对,以及把信号和信号量、管道的区别彻底钉死。


💬 看完如果有一句没懂,或者你手上的课件图跟我这里解读对不上,评论区直接说,我补图补例子。也欢迎把本篇和篇28(信号量)串起来复习——它俩名字像,但真不是一个东西。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 19:15:36

Switch平台GBA模拟器优化与高清滤镜技术解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 19:15:25

Linux进程控制:创建、终止与等待的深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 19:14:50

SpringBoot+MyBatis实现动态SQL与条件编排器方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 19:14:21

VRPTW专用遗传算法:LNS增强型GA求解带时间窗车辆路径问题

简介&#xff1a;本资源是一套面向智能优化与物流调度领域的MATLAB实战代码包&#xff0c;聚焦带时间窗的车辆路径规划&#xff08;VRPTW&#xff09;这一经典NP难问题&#xff0c;适用于运筹学、智能算法课程学习者及物流系统建模研究者。包内共38个文件&#xff0c;含36个核心…

作者头像 李华
网站建设 2026/9/12 19:14:16

源代码论文分享|大学新生报到系统!

每年开学季&#xff0c;新生报到看起来只是“登记一下信息”&#xff0c;但真正拆成系统以后&#xff0c;会发现里面其实有不少可以做的功能。 学生信息录入、报到状态确认、宿舍或院系信息查询、资料审核、后台管理……这些内容很适合做成一个完整的信息管理系统。功能不算特别…

作者头像 李华
网站建设 2026/9/12 19:13:44

用Turtle库绘制柯南:Python图形编程实战

简介&#xff1a;这是一份用Python标准库turtle绘制动漫人物柯南的趣味编程源码&#xff0c;压缩包内仅1个.py文件&#xff0c;大小约2KB&#xff0c;非常适合Python初学者、图形编程爱好者及少儿编程教学场景使用。案例以柯南形象为绘制目标&#xff0c;完整展示从导入turtle库…

作者头像 李华