:从 Ctrl+C 到内核位图,信号“是什么、从哪来“一次讲透)
本文收录于「流浪」的系列专栏系列专栏直达链接 Linux系统进入专栏 →⚙️ C进入专栏 → 数据结构与算法进入专栏 → Python进入专栏 → LangChain LangGraph进入专栏 →️ MySQL 数据库进入专栏 → Git 工具进入专栏 → 计算机网络进入专栏 → AI进入专栏 → 大厂面试、八股进入专栏 → 学习筑基专栏进入专栏 → 博客主页流浪 原创首发于 CSDN前言篇28 把信号量讲透了——计数器、P/V、电影院订票。但通信系列里还有个信号(signal)一直没单独立篇。它和信号量只差一个字机制却完全无关信号是 OS 给进程的一记异步通知CtrlC、除零崩溃、管道写穿全靠它。这篇把信号从是什么讲到内核怎么发顺手把和信号量的区别钉死。一个问题你按下 CtrlC背后到底发生了什么你按下CtrlC前台进程瞬间没了——可这背后 OS 到底干了什么为什么是瞬间为什么只能杀前台、杀不掉后台为什么子进程变成孤儿就按不死再想想程序里写个while(1)除个零进程直接被内核崩掉还吐个 core这崩又是谁干的这些日常现象背后站着同一个机制——信号。它不是你主动调的函数是 OS 在关键时刻拍你一下的通知。本篇先把信号是什么、从哪来、发给谁讲透。一、信号到底是什么1.1 一句话进程间的一记异步通知信号的准确定义是进程间的事件通知机制更准确地说是 OS 给进程发的通知。它和管道、消息队列最本质的区别在于信号不带数据只通知某类事件发生了具体怎么响应由进程自己定。管道是传字节流、消息队列是传数据块信号是有件事发生了你自己看着办。这一点决定了它轻量但也决定了它不适合传业务数据。1.2 生活里咋理解闹钟、红绿灯、狼烟闹钟你睡觉时不知道几点响响了你才反应——你没有被实时盯着事件到了才响应这就是异步。红绿灯你过马路不被交警实时指挥灯变了你自行决定走还是停。狼烟边防不派信使跑全程点一炷烟通知有敌情远处自己解读。它们的共同点发通知的人不盯着你处理你该干啥干啥事件到了你再响应。1.3 关键它跟进程运行是异步的异步是信号最难理解、也最重要的一个词。异步 信号在进程运行的任意时刻都可能送达不挑进程停在哪行代码上。进程不能像read那样主动阻塞等信号信号是 OS 在中断返回、系统调用返回等时机插进来的。这带来一个必然结果信号不能到了就立刻处理必须先记下、等合适时机再处理。二里讲发送机制时会看到这个先记下就是内核用位图记录信号的由来。1.4 先记住四条早知道怎么处理信号处理方式是信号还没产生前就注册好的OS 天生给每个信号准备了默认处理动作。不立即处理收到信号不会马上打断当前指令去执行处理而是等到从内核态返回用户态等合适时机才处理。OS 内置识别进程之所以能识别信号是因为内核在创建进程时就内置了整套信号机制。信号源非常多键盘、系统调用、硬件异常、软件条件、命令都能产生信号下一节展开。1.5 查看Linux操作系统信号的命令kill -l二、信号不等于信号量别搞混【回指篇28】2.1 为啥老有人混就差一个字信号 signal 和 信号量 semaphore中文只差一个量字但机制是两个世界。很多初学者学到这就晕所以专门拎出来说清楚。2.2 一张表看明白维度信号 signal信号量 semaphore是什么异步事件通知同步互斥的计数器干什么告诉进程某事件发生了如 SIGKILL、SIGSEGV管资源还剩几份、谁能进临界区谁发的OS键盘 / 异常 / 进程请求触发进程自己通过 semop 操作带数据吗不带只通知带计数值怎么用kill 发送 / signal 注册处理函数semget / semop 做 P、V2.3 一句口诀记住篇28 第九节 9.1 已经专讲过这个对比这里只回指不动重讲信号是叫你一下信号量是数你一下。一个字之差两个世界面试被问到这句话答出来就够。三、信号都是从哪冒出来的3.1 五种来源键盘CtrlC → 给前台发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 键盘按出来的CtrlCCtrlC 给当前前台进程组发SIGINT(2)默认动作是终止进程。注意前台进程组这四个字它解释了为什么后台进程按 CtrlC 杀不掉——这点第六章专门讲。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 PID发SIGKILL(9)kill -l列出全部信号名。记住SIGKILL9它是面试和排障都高频的信号第七章 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 敲其他命令CtrlC 可以终止它。后台进程脱离终端输入在系统后台默默运行不抢占命令行shell 可以继续接收新命令但它的输出依旧会打印到终端收到 SIGINT 信号不会终止一般用符号把程序放到后台运行。int main() { while(true) { std::cout我是一个进程getpid()std::endl; sleep(1); } return 0; }6.2 关键键盘信号只发给前台CtrlC 这类键盘信号只投递给前台进程组。后台进程不在前台进程组里所以收不到键盘信号。这就是你在终端跑./a.out 之后按 CtrlC 杀不掉它的原因——它已经不是前台了。6.3 前台只能一个、后台能多个一个终端同一时刻只有一个前台进程组键盘输入总得有个明确目标但后台可以有多个进程。这个唯一前台的约束是键盘信号能精确投递的前提。6.4 再识孤儿进程CtrlC 发送 SIGINT信号只作用于当前前台进程组。父进程退出 → 子进程成为孤儿被 init/systemd 收养由前台进程转变为后台进程脱离当前终端进程组。此时按 CtrlC 信号不会发给孤儿进程所以杀不死。6.5 如何进行前后台转换jobs //查看后台任务 fg 任务号 //将特定的进程提到前台 CtrlZ //暂停进程 也可以将前台进程挂到后台 bg 任务号 //将后台进程运行起来七、面试官爱问带答案7.1 信号为什么是异步的答信号可在进程运行的任意时刻由 OS 投递进程并不主动等待它OS 只在从内核态返回用户态等时机才检查并处理挂起信号所以天然异步。正是异步性决定了信号必须先记录、后处理。7.2 普通信号和实时信号到底差在哪答普通信号1–31不排队、重复发送可能丢失实时信号用户态通常 34 起支持排队、不丢失还能通过 sigqueue 带附加数据。普通信号够日常用实时信号用于不能丢事件的场景。7.3 CtrlC 为什么只能杀前台进程答按下CtrlC终端驱动会向前台进程组发送SIGINT中断信号。后台进程不属于当前终端的前台进程组因此接收不到这个键盘产生的 SIGINT 信号所以 CtrlC 无法终止后台进程。想要杀掉后台进程需要用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 展开。八、小结用三句话收尾是什么信号是 OS 给进程的异步事件通知不带数据只告诉你某事件发生了。从哪来键盘、系统调用、硬件异常、软件条件、命令五路来源最终统一收口内核普通信号 1–31实时信号用户态通常 34 起。发给谁键盘信号只发前台进程组后台收不到信号和信号量只差一字、机制无关。下一篇二钻进内核信号产生后怎么被记下来、又是谁、怎么写进目标进程的进程收到后怎么应对以及把信号和信号量、管道的区别彻底钉死。 看完如果有一句没懂或者你手上的课件图跟我这里解读对不上评论区直接说我补图补例子。也欢迎把本篇和篇28信号量串起来复习——它俩名字像但真不是一个东西。