3、中断处理流程:中断响应、上下文保存、中断分发、中断处理、上下文恢复

好,咱们接着往下聊。上一节我讲了中断向量表和中断控制器,算是把中断的“硬件骨架”搭起来了。这一节,咱们要真正走一遍中断处理的完整流程。

说白了,就是CPU收到中断信号后,从“正在干活”到“去处理中断”,再到“回来继续干活”的整个过程。我把它拆成五个阶段:中断响应、上下文保存、中断分发、中断处理、上下文恢复。

这五个阶段,缺一不可。任何一个环节出问题,系统就可能崩溃。我在做嵌入式项目时,就遇到过上下文保存不完整导致系统跑飞的情况,那叫一个头疼。

3.1 中断响应——CPU的“本能反应”

中断响应是硬件自动完成的。CPU检测到中断信号后,会做以下几件事:

  • 关中断:防止新的中断打扰当前的中断处理。
  • 保存当前PC值:把正在执行的指令地址压栈,这样中断返回时才知道从哪继续。
  • 跳转到中断向量表:根据中断号,找到对应的中断服务程序入口地址。

嗯,这里要注意:不同架构的CPU,中断响应的细节略有不同。比如ARM Cortex-M系列,硬件会自动保存一部分寄存器到栈中,而x86架构则需要软件手动保存。

我的经验: 在微内核中,中断响应阶段要尽量快。我一般会把中断响应时间控制在几十个CPU周期内。如果硬件支持“尾链中断”(Tail Chaining),一定要用上,能省不少时间。

3.2 上下文保存——把“现场”保护好

中断响应后,CPU就跳到了中断服务程序。但这时候,CPU的寄存器里还存着被中断任务的“现场数据”。如果不保存,等中断处理完回来,任务就乱套了。

上下文保存要保存哪些东西?我列个清单:

保存内容 说明
通用寄存器 R0-R12(ARM)或EAX、EBX等(x86)
程序状态寄存器 CPSR(ARM)或EFLAGS(x86)
栈指针 SP,指向当前栈顶
链接寄存器 LR(ARM)或返回地址

在微内核中,我习惯用内核栈来保存上下文。每个线程都有自己的内核栈,这样中断嵌套时不会互相干扰。

// 伪代码:上下文保存
void interrupt_entry(void) {
    // 保存通用寄存器到栈中
    push_all_registers();
    
    // 保存程序状态寄存器
    push_psr();
    
    // 保存栈指针
    push_sp();
    
    // 现在可以安全地调用中断处理函数了
    handle_interrupt();
}
避坑指南: 我曾经在保存上下文时漏掉了浮点寄存器,结果中断返回后,浮点运算结果全错了。如果你用到了FPU,记得把FPU寄存器也保存下来。别问我怎么知道的...

3.3 中断分发——找到“谁该干活”

上下文保存完后,就要决定这个中断到底该由谁处理。在微内核中,中断分发通常由中断管理器负责。

分发策略一般有两种:

  • 静态分发:每个中断号固定对应一个处理函数。简单,但不灵活。
  • 动态分发:根据中断优先级、当前CPU负载等因素,动态选择处理函数。微内核常用这种方式。

我个人比较推荐动态分发。你想想看,如果高优先级中断来了,但当前CPU正在处理一个不太紧急的任务,动态分发可以把中断交给另一个空闲的CPU核心去处理。

分发过程大致是这样的:

  1. 中断管理器读取中断号。
  2. 查找中断号对应的处理函数表。
  3. 如果该中断有注册的处理函数,就调用它。
  4. 如果没有,就调用默认的“未处理中断”函数(通常是打印错误信息)。

3.4 中断处理——真正的“干活”阶段

到了这一步,才真正开始处理中断。中断处理函数(ISR)要完成具体的业务逻辑。

在微内核中,中断处理有个重要原则:快进快出。ISR里不要做耗时操作,比如文件读写、网络通信等。这些应该交给内核线程去完成。

我一般把中断处理分成两部分:

  • 上半部(Top Half):在ISR中执行,只做最紧急的事,比如读取硬件寄存器、清除中断标志。
  • 下半部(Bottom Half):推迟到内核线程中执行,处理耗时的逻辑。
// 伪代码:中断处理函数
void timer_isr(void) {
    // 上半部:清除定时器中断标志
    clear_timer_interrupt_flag();
    
    // 记录中断发生时间
    current_time = read_timer_counter();
    
    // 下半部:通知内核线程处理时间片轮转
    schedule_bottom_half(timer_tick_handler);
}
核心要点: 中断处理函数中,绝对不要调用可能引起阻塞的函数(比如获取信号量、等待消息队列)。否则,整个系统可能死锁。我在项目中见过太多这样的bug了。

3.5 上下文恢复——“回到现场”

中断处理完了,该回去了。上下文恢复就是上下文保存的逆操作。

恢复过程:

  1. 从栈中弹出之前保存的寄存器值。
  2. 恢复程序状态寄存器。
  3. 恢复PC值(也就是返回地址)。
  4. 开中断(如果之前关了的话)。

这里有个细节:如果中断处理过程中发生了任务切换(比如时间片到了),那恢复的就不是原来的任务上下文了,而是新任务的上下文。这就是操作系统的调度机制在起作用。

// 伪代码:上下文恢复
void interrupt_exit(void) {
    // 检查是否需要任务切换
    if (need_reschedule) {
        // 切换到新任务的上下文
        switch_to_next_task();
    }
    
    // 恢复通用寄存器
    pop_all_registers();
    
    // 恢复程序状态寄存器
    pop_psr();
    
    // 恢复PC,返回被中断的任务
    return_from_interrupt();
}

3.6 完整流程图

我把整个流程画成了SVG图,方便你理解:

微内核中断处理完整流程 1. 中断响应 2. 上下文保存 3. 中断分发 4. 中断处理 5. 上下文恢复 硬件自动完成 软件手动完成 可能触发任务切换

3.7 总结

好了,中断处理流程就讲完了。我再用几句话帮你捋一捋:

  • 中断响应:硬件自动完成,保存PC、关中断、跳转。
  • 上下文保存:保存CPU寄存器,保护现场。
  • 中断分发:找到对应的处理函数。
  • 中断处理:执行具体的业务逻辑,要快进快出。
  • 上下文恢复:恢复现场,继续执行被中断的任务。

下一节,我会讲中断嵌套和优先级管理。到时候咱们再聊聊,多个中断同时来了怎么办。嗯,那才是真正考验微内核设计能力的地方。