news 2026/9/15 7:09:54

Linux系统篇33——信号(五):信号从不“立刻处理“,内核只在回用户态的路上顺路办了它

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux系统篇33——信号(五):信号从不“立刻处理“,内核只在回用户态的路上顺路办了它


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

🐧 Linux系统⚙️ C++
📊 数据结构与算法🐍 Python
🔗 LangChain & LangGraph🗄️ MySQL 数据库
🌿 Git 工具🌐 计算机网络
🤖 AI💯 大厂面试、八股
📚 学习筑基专栏

🏠 博客主页:流浪 | 📝 原创首发于 CSDN


前情:信号(一)讲了信号"是什么、从哪来";信号(二)讲了 task_struct 三张表怎么收;信号(三)讲了阻塞:屏蔽不等于丢弃;信号(四)讲了 kill -9 与 core。
本篇:悬了四篇的问题——“信号到底什么时候被处理”——在本篇落地。主线:捕捉信号 → 用户态与内核态 → 硬件中断与时钟 → 软件中断与系统调用,层层递进,一气呵成。


一、捕捉信号:信号的处理,从不"立即"

先纠正一个直觉:信号的处理,不是立即处理,而是等一会儿再处理——在"合适的时候",才进行信号处理。

你早就见过证据:一个死循环进程收到 SIGINT,并不是"当场死亡";你注册的 handler 打印,也总是"晚半拍"才出现。信号从产生到被处理,中间隔着一段"挂号等待期"(pending),本篇就讲清楚这期间发生了什么。

1.1 信号的处理方式有三种:忽略 / 默认 / 自定义

信号真正被处理(递达)的那一刻,动作只有三种,没有第四种:

方式含义谁执行
忽略收到当没收到,销账了事内核置位即可,最省事
默认按 OS 为该信号内置的约定执行内核照章办事
自定义执行你注册的信号处理函数用户,内核只负责修路
1 方式一:忽略

把信号设置为忽略(SIG_IGN),递达时内核直接把它当空气。注意一个细节:"默认动作就是忽略"和"你主动设置忽略"是两回事——比如 SIGCHLD 默认忽略,和你显式SIG_IGN,在某些语义(如僵尸进程回收)上并不等价,这个坑后面系列单独聊。

2 方式二:默认

OS 给每个信号都内置了"出厂约定"。默认动作拢共四大类:

默认动作含义典型信号
Term终止SIGTERM、SIGINT
Term+Core终止并落盘 core 文件(呼应信号(四))SIGQUIT、SIGSEGV
Stop停止进程SIGSTOP、SIGTSTP(Ctrl+Z)
Cont继续进程SIGCONT
3 方式三:自定义

signal()sigaction()注册一个函数,信号递达时执行它:

voidhandler(intsigno);// 你写的函数signal(SIGINT,handler);// 注册:SIGINT 递达时,执行 handler

自定义是三种方式里唯一需要"折腾 CPU 身份"的,复杂到要穿越四次"身份墙"——这是本篇主角。

1.2 合适的时候,是什么时候?

1 先想清楚:进程凭什么能"看到"信号?

反问一句:pending 位图躺在哪里?——内核的task_struct。用户态代码根本摸不到这本账。

所以结论只有一个:查账这件事,必须"有人在内核里"才能干

信号处理的时机 = 进程从内核态返回用户态的时候。

2 do_signal():内核的"查账员"

每次从内核态回用户态,内核都会路过一个检查点,由do_signal()出场查账(课程口径;新内核里这套逻辑演化为exit_to_user_mode_loop,思想不变):

// 伪代码,基于 arch/x86/kernel/signal.c 思路简化do_signal(structpt_regs*regs){while((signr=get_signal())){// 1. 查 pending:取 pending & ~block 中最小的信号if(sig_ignored(signr)){// 2. 忽略:账本上直接销账清除 pending 对应位;// 不打扰任何人,继续返回用户态continue;}if(handler 表[signr]==SIG_DFL)// 3. 默认:按约定办事执行默认动作;// 终止/停止/core……多半不再返回else// 4. 自定义:最麻烦的一种偷改返回用户态的上下文;// 把"回去后的第一条指令"换成 handler 入口}// 没有信号要处理 → 原路返回用户态,继续 main 主流程}

查账顺序严格是三步:先查 pending 表——为 0,直接返回;不为 0——再看 block 表,判断该信号有没有被阻塞(被阻塞?当没看见,信号继续挂着,呼应信号(三));最后查 handler 表,决定动作。

3 如果收到的是默认、忽略呢?
  • 忽略:do_signal检查时直接将 pending 表置 0,直接返回用户态,完事;
  • 默认:OS 根据特定信号的既定约定,执行特定动作,也简单。
4 因此:OS 执行默认、忽略,比自定义简单得多

一句话对比:忽略是"销账",默认是"照章办事",都一步到位;唯独自定义,内核要"做局"——先偷改上下文、放用户回去执行函数、再等用户自己回来销假。这个"局"怎么做,见 1.4。

1.3 重谈捕捉过程:自定义 handler,由谁执行?

1 灵魂拷问:用户身份,还是 OS 身份?

我们在执行自定义方法时,是以用户身份执行,还是以 OS 身份执行?

2 答案:用户
3 为什么?——一笔安全账

反过来想就明白了:如果内核亲自执行用户写的函数,等于用户代码借到了内核的权力——万一这个函数里在做非法操作呢?比如删文件、改文件权限、绕过权限检查?内核的"铁面无私"就被用户代码污染了。

所以原则是:

内核只负责"把路修到 handler 门口",绝不替用户跑腿。执行 handler 前后,内核只做两件事:保存现场、恢复现场。

1.4 因此,一次捕捉 = 四次身份切换

1 一张 ∞ 形图

这张 ∞ 形图是信号捕捉的灵魂:两条曲线的交点,就是信号检测的时间点横线和曲线的交点是现场恢复点。建议自己动手画一遍,比背十遍都有用。

2 第①次切换:用户态 → 内核态

主线进程因为系统调用、中断或异常,陷入内核。(为什么会"恰好"在内核?——1.5 节回答。)

3 第②次切换:内核态 → 用户态

返回前do_signal()查账,发现是自定义 handler,于是修改返回用户态的上下文——把"回去后该执行的下一条指令"偷换成 handler 的入口地址。CPU 回到用户态,顺理成章地"先"执行了 handler。

4 第③次切换:用户态 → 内核态(sys_sigreturn)

handler 执行完,函数return并不会直接回main——而是通过一个专门的系统调用sys_sigreturn再次进入内核,把第 3 步里内核保存的原始现场取回来。

5 第④次切换:内核态 → 用户态

带着原上下文,回到 main 的断点,继续跑。主线全程无感,仿佛什么都没发生过。

6 结论,和一个伏笔

从内核态返回用户态时做信号处理;捕捉一个自定义信号,进程一共要完成四次身份切换。

伏笔:handler 是"插在 main 任意两条指令之间"执行的,它和 main 共享同一个地址空间——所以才有"可重入函数""async-signal-safe"这些讲究(下一篇细聊)。

1.5 进程为什么要进入内核?

1 因为要被调度,而调度由 OS 完成

进程没主动干啥,为什么老在内核里?——因为进程要被调度,而调度这件事只能由 OS 完成。一个正常运行着的进程,注定周期性地被拉进内核、再放回用户态。

2 "返回用户态"是信号检查的天然卡点

信号检查点选在"返回用户态"这里,不是巧合,是顺路:反正每次回用户态都要过内核的岗亭,顺便把信号账本查了,零成本。

那问题继续往下钻:是什么力量,把进程周期性地"拽"进内核的?答案是两个大字:中断。但在讲中断之前,先把"用户态/内核态"本身说清楚。


二、用户态与内核态:地址空间怎么分,内核怎么进

讲清这个问题,绕不开五样东西:地址空间、如何进入、硬件中断、时钟中断、缺页。第一样是地基,本章讲;后四样对应"进内核的四条路",散在第三、四、五章——这里先把框架立起来。

2.1 从地址空间看:一个进程的两个世界

1 用户空间与内核空间

以 32 位经典划分为例:每个进程的虚拟地址空间是 4G,其中0~3G 是用户空间,归进程自己的代码和数据;3~4G 是内核空间,映射的是操作系统(64 位的划分方式不同,思想一致)。内核空间是所有进程共享的同一份映射——因为 OS 只有一个。

2 用户态/内核态的本质:CPU 的两档权限

“态"描述的是CPU 当前正在执行的代码的权限等级:跑用户代码,处于用户态,只能摸用户空间;跑内核代码,进入内核态,才有资格摸内核空间。所谓"切换身份”,本质是 CPU 换挡。

2.2 什么时候换挡?进内核的四条路

1 路一:系统调用

进程主动要服务(读写文件、创建进程)——第五章细讲。

2 路二:硬件中断

外设来"敲门"——第三章的主角。

3 路三:时钟中断

OS 的心跳,驱动的正是 1.5 说的"调度"——第四章细讲。

4 路四:缺页与异常

除 0、野指针、缺页,进程自己撞上的——5.4 节细讲。


三、硬件中断:OS 是怎么运行的

3.1 起点:OS/进程怎么知道磁盘上有数据了?

1 答案:硬件中断!

磁盘数据到了,OS 不可能一直盯着——是硬件主动通知它。

2 为什么不轮询?

让 CPU 每隔一段时间挨个问外设"好了没",查询本身烧掉大量 CPU 时间,还引入无谓延迟。中断的本质是事件驱动:平时不打扰,事了自然知。

3.2 冯诺依曼体系:CPU 只和内存直接打交道

冯诺依曼结构体系决定了CPU 要从内存中读数据。外设(如键盘)连接内存,间接和 CPU 相连——"间接"二字,正是下面针脚与中断控制器的意义。

3.3 针脚:中断的物理通道

1 什么是针脚

针脚就是 CPU 芯片边缘那圈金属引脚,是 CPU 与外界唯一的物理接口。其中有些针脚专门用于接收中断信号:外部电平一变,就等于硬件对 CPU"说了句话"。

2 中断控制器(8259):外设与 CPU 的中间人

问题来了:外设成百上千,CPU 的中断针脚只有几个,怎么接?——需要一个中间人:中断控制器(经典型号8259)。所有外设的中断线汇到它这里,由它统一向 CPU 的中断针脚发信号。(现代机器已用 APIC 取代 8259,且时钟源也集成进了 CPU——思想不变,教材口径照旧。)

3 两级发信号的完整硬件链路
外部设备准备好数据 ↓ ①向中断控制器(8259)的特定针脚发送信号 中断控制器 ↓ ②再由中断控制器向 CPU 的特定针脚发送中断信号 CPU 被硬件"戳了一下",放下手头工作,处理中断

3.4 先硬件,再软件

1 CPU 只知道"有设备准备好了"

中断信号到了,故事只讲了一半:CPU 只知道"外部有设备准备好了",但到底是可以被读、还是可以被写,以及对数据的处理——它并不知道。

2 这些由软件来完成:中断向量表

怎么办?得有一张"中断号 → 处理代码"的对照表。于是就有了中断向量表——它本质上是一个函数指针数组

3 下标就是 CPU 的中断号

数组的下标就是 CPU 的中断号,数组的内容,是对应中断服务程序(ISR)的入口地址。和"数组 + 下标"打了这么多年交道,这是它最硬核的一次出场。

4 完整链路总结
外设就绪 → 中断控制器(8259) → CPU 针脚收到中断 → 以中断号为下标,查中断向量表 → 找到对应中断服务程序的入口地址 → 去执行处理代码

5 中断向量表本身就是 OS 的一部分

最后一块拼图:中断向量表是 OS 的一部分。所以 OS 从不主动追问外部设备"你好了没"——它准备好,会通知 OS。这就是事件驱动。


四、有没有一种熟悉感:信号就是"用户态的中断"

4.1 硬件中断 vs 进程信号:同一套设计思想

看到这里,你应该有一种莫名的熟悉感——没错,硬件中断和 Linux 进程信号,在设计思想上一模一样:

硬件中断Linux 进程信号
中断号信号编号(1~64)
设备就绪,控制器挂号pending 位图置 1
中断屏蔽字block 屏蔽位图
中断向量表 → ISRhandler 表(默认/忽略/自定义)
ISR 执行完,恢复现场返回handler 执行完,sigreturn恢复现场

先挂号,择机处理,处理完恢复现场——一个模子刻出来的。

4.2 没有中断到来时,OS 在做什么?

1 答案:什么都不做,它是暂停的

可能颠覆直觉:OS 不是"时刻管理者",没有中断,它就在原地停着。

啊???我没听错吧 他啥都不做进程是咋调度的?

2 内核源码:for(;😉 pause();

内核的主循环朴素到令人发笑(经典示意):

for(;;)pause();// 睡过去,等中断来叫醒

4.3 时钟:OS 的心跳

1 时钟源:以固定频率向 CPU 发中断

中断控制器里有一个时钟源,以固定的频率向 CPU 发送特定的中断信号(时钟中断)——不管有没有外设干活,它雷打不动。

2 在中断向量表里,注册时钟中断的服务 = 进程调度

时钟中断的向量表表项,注册的就是进程调度函数。每次时钟中断到达,CPU 都会进内核跑一次调度逻辑。

3 时间片的真正来历

于是,OS 在硬件时钟中断的驱动下,进行调度——这就是"时间片"的真正来历:所谓时间片,就是用固定频率的中断"数"出来的。

4 进程调度的大致过程
set_intr_gate(0x20, &timer_interrupt); // 开机初始化时执行一次 _timer_interrupt: // 时钟中断服务程序(汇编)入口 ... call _do_timer; // 'do_timer(long CPL)' does everything ... ... do_timer: // C 函数:每次时钟节拍都会走到 ... if ((--current->counter) > 0) return; // 如果进程运行时间还没完,则退出 schedule(); // ← 图中标注"进行调度!" ... switch_to(next);
5 为了追求效率:时钟源集成进 CPU 内部

外置时钟源走针脚,又慢又占资源;后来干脆将时钟源集成在 CPU 内部(local APIC timer 的思想),发中断更准时、开销更低。

6 频率 → 时间戳:离线也知道几点

有了固定频率,就能"数中断"算出时间戳,再与历史时间对照校准——这样计算机哪怕离线,也知道现在几点了(实时时钟 RTC 的思路)。

4.4 结论:OS 就是基于中断进行工作的软件

OS 就是基于中断进行工作的软件。它平时"暂停"在死循环里,所有的主动性——调度、外设、异常——全部来自中断。

4.5 延伸一问:有没有软件触发的中断?

有。比如除 0、野指针、缺页中断——这些由软件"撞出来"的中断,让OS 得以基于中断知道硬件异常。它们和系统调用同属"软件中断"家族,下一章细讲。


五、软件中断:一条指令把 CPU"骗"进内核

5.1 CPU 内部的触发指令

1 x86(32 位):int 指令

经典如int 0x80

2 x86_64:syscall 指令

这两条指令的共同点:自动让 CPU 触发一次中断,陷入内核——不需要任何外设参与,软件自己就能"敲门"。

5.2 系统调用表与系统调用号

1 问题:CPU 只有一个,怎么知道你要调哪个服务?

当我们进行系统调用时,具体是如何进入 OS、完成系统调用的?毕竟 CPU 只有一个,内核怎么区分"你要的是 open 还是 write"?

2 答案:系统调用表,又一个"函数指针数组"

系统里有一张系统调用表——每一个系统调用都有一个唯一的下标,这个下标就是系统调用号。你把调用号放进寄存器,内核按下标查表,跳到对应服务入口。(和中断向量表如出一辙——第三处"熟悉感"。)

5.3 open 函数不就是系统调用吗?

1 不是:OS 不提供任何系统调用,只提供调用号

你天天调的open(),是glibc 封装的一层壳。OS 这边,只认"调用号"。

2 拆开 glibc 的 open
// glibc 的 open(32 位示意)intopen(constchar*path,intflags,...){mov eax,5;// 5 = __NR_open,系统调用号放进 eaxsyscall// 32 位写法:int 0x80 —— 触发软中断,陷入内核}// 内核按 eax 里的 5 查系统调用表 → 找到 sys_open 入口执行

5.4 软件中断的两兄弟:陷阱与异常

1 陷阱(trap):主动索取服务

int 0x80/syscall——主动、有意为之,为了获取内核服务。

2 异常(exception):被动撞上

除 0、野指针、缺页——进程自己撞上的错误或事件。注意:缺页不一定是错误,请求分页(懒加载)就是靠它实现的,这个坑系列后面填。OS 正是基于中断,才知道硬件出了异常。


六、收束:Ctrl+C 的一生

6.1 完整链路

现在,可以把第一篇埋下的线全部缝上了:

敲下 Ctrl+C → 键盘控制器向中断控制器发信号(硬件中断) → CPU 收到中断,执行键盘中断服务程序 → 驱动把 SIGINT 挂进前台进程的 pending 位图(挂号) → 该进程某次从内核态返回用户态时 → do_signal() 查账:pending=1,block=0 → 默认动作是终止 → 进程消亡

而如果你用sigaction注册了 handler:同一条路走到查账那一步,变成四次身份切换——内核改上下文,回用户态执行你的函数,sys_sigreturn回内核,再回 main。

一切的中介,都是中断。理解了中断,你才真正理解了信号;理解了信号,你才真正看到了 OS 运行的骨架。

6.2 最简实验(建议自己跑一遍)

#include<stdio.h>#include<signal.h>#include<unistd.h>voidhandler(intsigno){printf("get a signal: %d\n",signo);}intmain(void){signal(2,handler);// 给 SIGINT 注册自定义动作(sigaction 是更现代的写法)while(1){printf("I am a process, pid: %d\n",getpid());sleep(1);}return0;}

运行后kill -2 <pid>,观察打印——再想想:这条打印出现在 main 循环的哪两条指令之间?它背后,是刚刚那四次身份切换。


七、文末面试题

7.1 推导题(按本讲知识点,附答案)

1.【推导】信号在什么时候被处理?为什么必须是这个时刻?
答:从内核态返回用户态时。pending / block / handler 三张表都在内核task_struct里,用户态摸不到,只有"回用户态路上"的检查点(do_signal)能查账。

2.【推导】一次自定义信号的捕捉,一共发生几次用户态/内核态切换?
答:四次。陷入内核 → 回用户态执行 handler → handler 完经sys_sigreturn回内核 → 恢复现场回用户态。

3.【推导】自定义 handler 为什么由用户身份执行,内核不代劳?
答:安全。内核若亲自执行用户函数,等于用户代码借到内核权力,可借机删文件、改权限;内核只保存/恢复现场。

4.【推导】handler 执行期间,又收到同一个信号,会立刻嵌套执行吗?
答:默认不会。执行 handler 前,内核自动把该信号加入进程屏蔽字;相同信号此时只会挂号(pending 置 1),等 handler 返回、屏蔽字恢复后再递达。

5.【推导】没有任何中断到来时,CPU 在干什么?
答:暂停。内核停在for(;;) pause();上;一切主动性(调度、外设、异常)都由中断驱动。

6.【推导】中断向量表、系统调用表、信号的 handler 表,共同点是什么?
答:都是"函数指针数组 + 唯一下标"——下标分别是中断号、系统调用号、信号编号;查表跳转,一套思想三处复用。

7.【推导】open() 是系统调用吗?描述一次系统调用的完整过程。
答:不是,open 是 glibc 封装的壳。过程:调用号放 eax →int 0x80/syscall触发软中断陷入内核 → 内核按调用号查系统调用表 → 执行对应服务程序 → 返回。

7.2 真题(搜断面经,均已转述并核对原文主题)

1. 从用户态切换到内核态,有哪几种方式?
答:系统调用、程序异常(缺页/除零等)、外设中断——正是 2.2 节那"四条路"的归纳。
【真题 · 转述自 小林coding《操作系统面试题》 与 GitHub: 0voice/linux_kernel_wiki 面试题(题库型,无单一年份)】

2. 信号是怎样被捕捉的?处理过程中用户态/内核态如何切换?
答:即 1.4 节"四次身份切换"。
【真题 · 转述自 CSDN 博文《面试常问:Linux信号之信号的产生、信号的捕捉》 · 2018 年】


小结:本篇把信号从"挂号"讲到"递达",又顺手掀了 OS 的老底——它自己就是一个被中断驱动的软件。跑 6.2 实验时踩到的任何坑,评论区见。

系列回顾:信号(一) · 信号(二) · 信号(三) · 信号(四)

下一篇预告:信号(六)——可重入函数与 volatile:handler 插队执行的副作用,比你想的大。

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

SpringBoot+Vue构建高校毕业审核系统实战

1. 项目背景与核心需求高校毕业与学位资格审核是教务管理中的关键环节&#xff0c;传统人工审核方式存在效率低、易出错、流程不透明等问题。这个基于SpringBootVue的前后端分离系统&#xff0c;正是为了解决以下痛点&#xff1a;审核标准复杂&#xff1a;不同专业、培养方案存…

作者头像 李华
网站建设 2026/9/15 7:08:17

2026国内量化交易软件选择:研究平台与券商终端如何搭配

国内个人投资者选量化交易软件&#xff0c;不一定只选一款。聚宽和米筐更靠近研究项目&#xff0c;QMT和PTrade更靠近券商账户环节。只做Python研究可先比前两者&#xff1b;准备把成熟规则接到账户环境&#xff0c;再按本人券商条件核对后两者。这四款候选分别位于研究和账户环…

作者头像 李华
网站建设 2026/9/15 7:05:24

抖店OPC自动化运营系统:多店管控与策略模板下发架构

技术摘要抖店店群行业正从人力驱动转向人机协同的系统驱动。传统ERP强项是订单、财务、进销存&#xff0c;聚焦后端履约&#xff0c;不覆盖抖店前台大量日常运营动作&#xff1b;原生OPC系统构建"痛点挖掘-策略制定-系统执行-实时监控-溯源复盘"完整闭环。本文从多店…

作者头像 李华
网站建设 2026/9/15 7:04:54

大文件传输怎么选?场景拆解与工具推荐

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

作者头像 李华
网站建设 2026/9/15 7:01:28

ERP选型必读:预约前用性能评测筛掉不合适厂商

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

作者头像 李华