本课属于 第一阶段 · 走进操作系统(第 1~5 课)

上一课我们说,从按下电源键到 shell 提示符,是一场四棒接力。现在系统已经跑起来了,接下来要回答一个更本质的问题:一个"睡着"的操作系统,是怎么在你敲下键盘的那一刹那,瞬间"醒来"去干活的?答案,就藏在这一课的两个字里——中断


从"你敲了一下键盘"说起

你现在敲一下键盘上的任意一个键。

从你的手指落下,到屏幕上蹦出那个字符,中间发生了什么?你可能会以为:CPU 一直在盯着键盘,等着你敲。完全不是。

真相是:CPU 刚才可能正在算别的东西、或者在发呆,它根本没空、也没必要一直盯着键盘。是你敲键这个动作,主动"叫醒"了它——键盘通过电路发出一记信号,这个信号叫中断(Interrupt)

一句话理解:中断就是"有人按门铃"。CPU 平时专心做自己的事,门铃一响,它立刻放下手里的活,跑去开门——也就是去处理那个打断它的事件。

这一课,我们要搞清楚三件事:

  1. 中断和异常,这两个词到底怎么区分;
  2. CPU 是怎么"按号找人"去处理的(中断向量表);
  3. 为什么说,没有中断,操作系统就瘫痪了

3.1 先分清两个词:中断 vs 异常

初学者最容易把"中断"和"异常"混在一起说。它们确实很像——都会打断 CPU 当前正在跑的代码,让 CPU 转而去执行一段"处理程序"。但它们的来源不一样。

类型谁触发典型例子性质
硬中断(中断)外部设备键盘敲击、网卡收包、磁盘读完异步(随时来,猝不及防)
软中断程序主动执行 int 0x80 发起系统调用同步(代码走到这步才发生)
异常CPU 自身除零、访问非法地址、缺页同步(执行指令时当场发生)
陷阱程序主动系统调用、调试断点同步

这里最关键的一条分界线是:来源在 CPU 外面,还是里面。

  • 中断(interrupt):来自 CPU 外部的设备。键盘、网卡、硬盘……这些"外围"发出的信号,叫中断。它的特点是异步——你没法预测它哪一刻会来。
  • 异常(exception):来自 CPU 内部,是 CPU 执行某条指令时"出了问题"。比如你让程序除以零,CPU 一算,发现"这没法算啊",当场触发一个异常。它是同步的——只要执行到那条坏指令,就一定会发生。

还有一个词叫陷阱(trap),你可以把它理解成"程序主动要求 CPU 帮个忙"——最常见的场景就是系统调用。第 1 课我们提过,程序想读写文件、分配内存,都得通过系统调用进入内核。这个"主动敲门"的动作,本质就是触发一次陷阱。

很多教材把"陷阱"也算作一种异常(统称"程序主动触发的异常"),你心里知道它是"主动的"就行。抓住一个核心就够:外部来的叫中断,内部或程序自身引起的叫异常。

3.2 中断向量表——"按号找人"

现在关键问题来了:CPU 收到一个中断,怎么知道该去执行哪段处理代码?

答案是给每个中断/异常编个号,再用一张表把"编号"和"处理函数"对应起来。这张表,就叫中断向量表(Interrupt Vector Table)

一句话理解:中断向量表就是一本"电话簿"。CPU 拿到一个中断号,翻开电话簿查到对应的处理函数(handler),然后打过去——也就是跳去执行。

比如在 x86 架构里,这些号是约定俗成的:

  • 除零异常,固定是某个编号;
  • 缺页异常(访问了不存在的内存页),又一个固定编号;
  • 系统调用,也有专门的一个编号(经典的 Linux 32 位系统调用走 int 0x80,这里的 0x80 就是那个号码)。

一旦发生事件,CPU 的流程是固定的:

拿到中断号 N → 查向量表 → 找到第 N 个处理函数地址 → 跳过去执行

你完全可以自己想象成:"门铃编号"和"该谁去开门"一一对应。厨房门铃响了,去的是厨子;大门门铃响了,去的是前台。CPU 就是那个"总调度",负责按铃的编号把对应的"人"喊来。


3.3 陷入机制:怎么安全地进内核

说到这,有一个非常关键、但容易被忽略的点:处理中断的代码,是跑在内核里的

也就是说,本来你程序在"用户态"好好跑着,突然一个中断来了,CPU 要从用户态切到内核态去执行处理代码。这个切换过程,叫陷入(trap into kernel)

那 CPU 是怎么保证这次切换"不出乱子"的?它有一套自动化的固定动作:

  1. 保存现场:把当前程序"跑到哪了、状态如何"(寄存器、程序计数器等)先存起来,就像暂停游戏前先存档;
  2. 切到内核态:获得更高权限,才能去碰硬件、改关键数据;
  3. 跳到 handler:去执行那个中断对应的处理函数;
  4. 处理完,恢复现场:把存档读回来,CPU 回到刚才被打断的地方,继续往下跑,好像什么都没发生过。
一句话理解:中断/异常发生时,CPU 自动"存档 → 升级权限 → 办事 → 读档 → 回到原处",整个过程对被打断的程序几乎是透明的。

这里和第 1 课的"系统调用"完美接上了。系统调用的本质,就是程序主动触发一次陷阱,让 CPU 走上面这套流程,替它进内核办事。你看,前面埋的伏笔,到这就全串起来了:

用户程序 → 主动触发陷阱(系统调用) → 陷入内核 → 内核代劳 → 返回结果 → 回到用户程序

3.4 为什么说中断是"发动机"

现在可以回答那个大问题了:为什么把中断叫做操作系统的发动机

因为操作系统的核心工作模式,不是"一直盯着所有事情",而是:

平时睡觉,有事叫醒。
  • 你敲键盘,键盘发中断,CPU 被叫醒,去处理这个按键;
  • 网卡收到一个数据包,发中断,CPU 被叫醒,去处理网络;
  • 硬盘读完数据,发中断,CPU 被叫醒,把数据交给等待的程序;
  • 甚至CPU 调度(什么时候该换一个进程跑)本身,也靠一个叫"时钟中断"的定时铃声来驱动。

想象一下,如果没有中断会怎样?操作系统只能靠"轮询"——不停地、一遍遍地去问键盘"你按键了吗?没按?我再问一遍"。这样它会把几乎所有力气都花在"反复问"上,真正干正事的时间所剩无几,而且响应还慢。整个系统会又笨又卡,等于瘫痪

所以,中断是操作系统对外界做出反应的根本机制。没有它,操作系统就是一台听不见、看不见、不会被打断的机器,什么都做不了。

一句话理解:OS 不是"盯梢的保安",而是"打盹的门卫"——中断就是那声门铃,是它醒过来干活的唯一信号。

动手实验:故意触发异常,看信号怎么处理

说了这么多,我们亲手触发一次异常看看。在 Linux 里,进程触发了异常(比如除零、访问非法内存),内核会向这个进程发一个信号(signal),而你的程序可以注册一个信号处理函数(signal handler)来"接住"它。

先写一段 C 程序,故意除零:

// lesson03.c —— 触发异常并观察信号处理
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

void handler(int sig) {
    printf("捕获到信号 %d:发生异常啦!\n", sig);
    exit(1);
}

int main(void) {
    // 除零异常 → SIGFPE 信号;非法内存访问 → SIGSEGV 信号
    signal(SIGFPE, handler);
    signal(SIGSEGV, handler);

    // 故意除零,触发异常
    int a = 1, b = 0;
    int c = a / b;             // 这一行会触发 SIGFPE
    return c;
}

编译并运行:

gcc lesson03.c -o lesson03 && ./lesson03

预期输出:

捕获到信号 8:发生异常啦!

这里发生了什么事?除以零 → CPU 执行 a / b 时发现除数为 0,当场触发异常 → 陷入内核 → 内核认定这是"非法运算",向进程发送 SIGFPE 信号(编号 8)→ 你注册的 handler 被调用 → 打印出那句"发生异常啦"。

你再试试把代码里的 int c = a / b; 换成一段非法内存访问,比如:

int *p = (int *)0;   // 指向地址 0,这是非法地址
*p = 42;             // 尝试写非法地址,触发 SIGSEGV

重新编译运行,你会看到它捕获的是 SIGSEGV(编号 11)——非法内存访问异常。同一个机制,不同的"门铃编号"。

这个实验妙就妙在:它把"异常 → 信号 → 你的 handler"这条链路,用一段几十行的程序完整走了一遍。抽象的"陷入机制",一下子就具体了。

深入点:两个进阶概念

① 软中断与"下半部"(bottom half)

硬中断有个铁律:要快进快出。因为 CPU 在处理中断时,别的中断往往被暂时挡住,如果你在里面磨蹭,系统就会卡顿。

那网卡一下收到一大堆包、磁盘要处理大块数据,这些"繁重活"怎么办?答案是:硬中断只干最紧急的一点点,剩下的繁重工作,丢给"软中断 / 下半部"延迟处理。

打个比方:门铃响了,前台(硬中断)立刻应一声"来了",但真正搬货、登记这些累活,交给后面的人(软中断/下半部)慢慢干。这样门铃能尽快空出来,继续响下一声。

这就是 Linux 里"上半部 + 下半部"的设计哲学:上半部快速响应,下半部慢慢善后

② 中断优先级与屏蔽

有些中断可以被"屏蔽"(关中断),也就是暂时不响应。这用在临界区——一段不能被中途打断的关键代码里。

但这里有个度:关中断太久,会丢事件、甚至把系统卡死。所以内核里关中断的时间,都是以"微秒"来精打细算的,能短则短。这背后是工程师对"安全"和"响应速度"的长期权衡。


小结与思考题

这一课,我们把操作系统真正的"发动机"拆开了:

  • 中断来自外部设备,异常来自 CPU/程序自身,陷阱是程序主动触发(最常见就是系统调用);
  • CPU 靠中断向量表"按号找人",找到对应的 handler;
  • 处理时,CPU 自动走一遍保存现场 → 陷入内核 → 办事 → 恢复现场,安全地完成用户态与内核态的切换;
  • 没有中断,操作系统就只能空转轮询,等于瘫痪——所以中断是它的发动机。

留三个问题给你:

  1. 键盘敲击是「中断」还是「异常」?除以零呢?说说你的判断依据。
  2. 系统调用和中断/陷阱之间,到底是什么关系?
  3. 用你自己的话,向一个没学过计算机的朋友解释:为什么"没有中断,操作系统就瘫痪了"?
下一课,我们终于要见到操作系统真正的主角了:进程。你双击一个图标、在命令行敲一个命令,背后"活过来"的那个东西,到底长什么样?它和躺在磁盘上的"程序",又有什么区别?

标签: 计算机基础, 操作系统, Linux

添加新评论