谈及操作系统,我们的第一印象往往是它始终很 "忙"------ 忙着管理硬件资源,忙着调度进程线程,忙着处理各类数据交互,仿佛是永不停歇的 "大管家"。但很少有人知道,这个 "大管家" 也会偷偷 "摸鱼":当系统中没有需要处理的任务时,它便会进入空闲状态,以一种极致简洁的方式等待下一次工作指令。而这背后,正是操作系统最底层的运行逻辑。
一、硬件中断
当我们在终端进行操作时,总能感受到操作系统的即时响应 ------ 无论是敲击键盘输入指令,还是移动鼠标进行操作,系统都能快速捕捉并处理。但操作系统究竟是如何感知到对应外设产生了数据的呢?按照常规思路,很容易想到 CPU 会定期对所有外设逐一轮询,检查是否有数据产生;可外设种类繁多,且多数外设产生数据的频率本就不高,这样的轮询方式,无疑是对 CPU 宝贵资源的巨大浪费。那么操作系统究竟是如何设计,才能让 CPU 在能耗控制与资源调度之间实现完美平衡的呢------硬件中断,就是为了解决这个痛点而生的 "聪明方案"。
既想要CPU保持对能耗的控制又要让其保证积极的资源调度,就需要让CPU该忙的时候忙,空闲的时候适当'摸摸鱼',做到'劳逸结合',但是如何让CPU摸鱼固然简单,需要CPU工作的时候如何唤醒它呢?其实当外设产生数据的时候,会向CPU发送信号,但是根据冯诺依曼体系:CPU不是只和内存打交道吗?怎么能直接和外设打交道?其实这里所谓的发信号并不是外设直接交给CPU,而是依赖中断控制器,中断控制器一边与CPU针脚相连接,另一边又和外设连接,当外设产生数据时,会产生中断请求信号 发送给中断控制器,中断控制器收到信号后会按照硬件优先级优先处理优先级高的硬件信号,随后将信号通过针脚发送给CPU,此时CPU会停下正在执行的程序,将其保存到寄存器,随后执行相应硬件中断执行函数。这种由外设发起的中断就叫做硬件中断。

比如:我们进行键盘操作产生数据,此时键盘就会向中断控制器发送一个中断请求(IRQ) ,中断控制器将其映射为对应的中断号(比如是 0),随后中断控制器通过针脚向CPU发送电信号,CPU收到信号就会中断系统进程,获取中断号,然后拿着中断号在中断向量表(一个函数指针数组,其中记录了不同中断信号的中断执行函数)中找到对应的执行方法进行函数调用。
通过这样一套系统方法,就能很好地让CPU保持'劳逸结合'的状态,保持能耗与调度之间的平衡。
二、时钟中断
通过上述对硬件中断概念的了解,我们知道硬件中断会随时提醒CPU进行数据获取工作,但是CPU同时也要进行软件层面的管理,那么在进行软件管理的时候又是如何合理进行资源调度的呢?其实类似于硬件中断,CPU进行软件层面管理的时候同样被一个定频的信号所提醒,进而定期地进行进程调度队列的检查并且刷新进程时间片,实现进程并发,兼顾系统时间戳的更新等等。这个信号称为时钟中断 。发起时钟中断的硬件叫做时钟源。 起初时钟源等同于键盘等外设,通过中断控制器向CPU进行信号发送,时钟源会定频向CPU发送时钟中断信号,让CPU执行进程调度函数,进行调度队列的更新,并且更新进程时间片,让时间片剩余0的进程被调度队列中优先级最高的进程轮切替换,从而达到并发的目的,与此同时更新系统时间戳等,但是基于其高频性,作为外设会大大降低其效率,因此后来时钟源被集成到CPU中,成为CPU的内置硬件,大大提高了效率。时钟中断的设计,让操作系统其实从未真正进入彻底的 "摸鱼" 状态,而是会被定期唤醒,持续完成系统资源的巡检与管理工作。

所以容易想到,操作系统内的"时间"概念,实际大部分就是一个unsigned int类整数,当发生时钟中断时,由于时钟中断具有定频性,对时间这个整型相应地进行增减即可,此类方法就用于时间片计算以及时间戳计算。
三、操作系统常态------死循环
了解完两种中断,其实大体可以感受到,中断其实本质就是提醒操作系统你该忙起来了,那么不禁问道:难道操作系统一直都是处于被'驱使'的状态在进行工作吗------是的!如果说操作系统负责整个计算机的资源管理与调度,那么中断机制就是驱动操作系统自身有序调度的核心引擎 。基于这样的中断机制,操作系统完全可以 "躺平" 待机,无需持续轮询消耗资源,只需静默等待各类中断信号的触发;而系统要实现新的硬件响应功能,也仅需向中断向量表中添加对应的中断处理函数即可。有了以上概念,其实操作系统就相当于一个死循环的进程,对其底层代码进行伪代码生成大致如下:
cpp
void start_kernel(void) {
// 1. 系统初始化(仅执行一次)
init_system();
// 2. 操作系统核心死循环(整个系统的"主循环")
while (1) {
// 第一步:检查是否有可运行的进程
struct task_struct *next = pick_next_task();
if (next) {
// 有任务:切换到进程执行,执行完后回到这个循环
schedule(next);
} else {
// 无任务:CPU进入低功耗"躺平"态,仅响应中断唤醒
// 此时CPU几乎不耗电,区别于普通空转死循环
arch_cpu_idle();
}
// 关键点:任何中断(时钟/键盘/网卡)都会唤醒CPU,重新执行循环
// 时钟中断定期唤醒,保证进程调度和系统时间更新
}
}
四、软中断
前面两种中断机制都由硬件触发,那么有没有由软件可以触发的中断呢------答案是有的!其实当我们进行系统调用(例如open,write)的时候,并不是真正的系统调用,而是C语言对于系统调用的封装函数,真正的系统调用需要用汇编进行调取,因此如果对C语言系统调用函数进行一步步追踪可以看到最后是汇编语言,而系统调用本质就是一种软中断,我们通过open等库函数发起系统调用,底层执行int 0x80/syscall这些指令的行为,就是软件主动触发的软中断------ 区别于硬件触发的时钟、键盘中断,是用户态程序向内核态发起请求的软中断方式。其中int 0x80和syscall就是软中断的汇编指令:
int 0x80:本质是触发0x80 号软中断,内核提前在中断向量表中注册了 0x80 号软中断的处理函数(系统调用处理函数),因此执行这条指令后,CPU 会跳转到内核的系统调用处理逻辑;

syscall:x86_64 架构新增的专用系统调用指令,不再依赖软中断机制,直接通过 CPU 的快速系统调用门切换到内核态,效率比int 0x80高(少了中断控制器等环节)。
那么这两个软中断指令是如何将系统调用号(系统对系统调用进行的唯一数字标识)从用户层交给操作系统的呢?答案是寄存器,当执行这两个软中断指令后,对应的系统调用号以及其他数据会交给寄存器,CPU收到指令后通过寄存器获取系统调用号就可以直接进行相关函数调用。我们以
int 0x80举例:

其中movl 段落就是将SYS_ify进行宏定义展开并且写入寄存器eax。进程发起系统调用时会从用户态陷入内核态,由于用户态进程无法直接执行硬件操作和访问内核资源,这一陷入过程会为当前进程临时解锁 CPU 的最高硬件特权级 ,以此支撑系统调用的执行;内核会接管高权限执行流程,替进程完成对应的内核级操作,操作结束后特权级会被立即收回,进程切回用户态,在切换回用户态之前,内核会同时触发对信号的检查,并且检查信号状态,如果信号未处于阻塞(block)且未决(pending)状态就进行信号处理,这也是信号的处理机制以及时机。除了我们通过int 0x80/syscall发起的主动中断 (软中断),像进程故障(除零错误、野指针访问)以及硬件出错,也会触发 CPU 的中断机制。当发生这两种异常时,用户态程序没有权限和能力处理,操作系统会接管控制权,程序会从用户态自动陷入内核态,由内核来判断错误、尝试修复或终止进程。这个过程也涉及到了用户态和内核态的切换,故而在由内核态返回用户态的时候也会进行信号的检查,如果信号进行了自定义捕捉,就会先返回用户态进行自定义函数调用(对于默认动作或者忽略动作会直接在内核态执行),整个过程我们可以用以下图示进行描述:

- 用户态:进程触发故障(除 0 / 野指针),执行流正常运行被打断;
- 第一次陷入内核态 :CPU 硬件强制切换,内核判定错误不可修复,给进程标记待处理信号,完成异常基础处理;
- 内核态→用户态 :内核准备返回前检测到待处理信号,修改进程执行上下文(指向信号处理函数),切回用户态执行自定义信号处理函数;
- 用户态→第二次陷入内核态 :信号处理函数执行完毕后,会自动执行内核提前设置的系统调用指令(如 sigreturn),再次主动陷入内核态;
- 第二次内核态→用户态 :内核在这个阶段完成核心收尾------ 恢复进程触发故障前的原始执行上下文(指令地址、寄存器值、栈指针等),然后再次切回用户态,让进程从故障发生的下一条指令继续执行(若信号默认行为是终止,则内核直接终止进程,不返回)。
对于整个过程就形成了完整的闭环,我们对图片进行抽象

整个流程就变成了一个'无穷'符号,其中交点就是从内核态即将返回用户态时所进行的信号检查。这也就是信号所谓合适的执行时机。