4. 中断服务程序:ISR编写规范、保存与恢复现场、中断返回指令IRET

好,咱们进入中断机制里最核心、也最考验功底的部分——中断服务程序(ISR)的编写。说白了,ISR 就是 CPU 响应中断后,跑去执行的那段“临时任务”。写得好,系统稳如泰山;写得不好,嗯,我见过系统直接死在那儿的。

4.1 ISR 编写规范:别让中断变成灾难

我个人习惯,写 ISR 之前先问自己三个问题:这个中断能有多快?它要访问哪些共享资源?会不会被更高优先级的中断打断?想清楚这些,再动手。

核心规范就几条:

  • 短小精悍:ISR 不是主程序,别在里面做复杂运算。我见过有人把排序算法写在 ISR 里,结果系统响应慢得像蜗牛。ISR 的任务就是“快速响应,快速退出”。
  • 不可重入要小心:如果 ISR 和主程序共享同一个全局变量,记得加保护。我曾经踩过一个坑——两个中断同时修改一个标志位,结果逻辑全乱套了。
  • 关中断时间尽量短:在 ISR 里关中断,是为了保护临界区。但关太久,其他中断就响应不了。我建议:关中断只包住那几行关键代码,别一关就是一大片。
  • 避免调用不可重入函数:像 printf()malloc() 这类函数,很多实现是不可重入的。在 ISR 里调用它们,等于给自己埋雷。

重要原则:ISR 里只做“必须立即做的事”,比如读取硬件寄存器、清除中断标志、把数据放到队列里。剩下的处理,交给主循环或任务去干。

4.2 保存与恢复现场:CPU 的“记忆暂停术”

中断来了,CPU 正干着活呢,突然被叫走。等它回来,得知道自己刚才干到哪了。这就是“保存现场”和“恢复现场”的意义。

保存现场:把 CPU 当前的所有寄存器(通用寄存器、段寄存器、标志寄存器等)压入堆栈。这样,中断返回时就能原样恢复。

恢复现场:中断处理完,从堆栈里弹出之前保存的寄存器值,让 CPU 回到中断前的状态。

你想想看,如果忘了保存某个寄存器,中断返回后那个寄存器的值就变了,主程序跑着跑着就出错了。我早期调试时,就遇到过因为没保存 EAX 寄存器,导致循环变量被改,程序直接飞掉的情况。

典型的保存/恢复流程(x86 汇编示例):

; 中断服务程序入口
ISR_Handler:
    ; 保存现场
    push    ax
    push    bx
    push    cx
    push    dx
    push    si
    push    di
    push    ds
    push    es

    ; 这里写你的中断处理逻辑
    ; 比如读取键盘输入、处理定时器等等

    ; 恢复现场
    pop     es
    pop     ds
    pop     di
    pop     si
    pop     dx
    pop     cx
    pop     bx
    pop     ax

    ; 中断返回
    iret

我的小技巧:保存现场时,压栈顺序和出栈顺序要严格相反。我习惯用“先入后出”来记——先压进去的,最后才能弹出来。另外,有些 CPU 会自动保存 CSIP 和标志寄存器,这部分不用你操心。

4.3 中断返回指令 IRET:优雅地回到“现场”

ISR 的最后一条指令,通常是 IRET(Interrupt Return)。它和普通的 RET 不一样,IRET 会做三件事:

  • 从堆栈弹出 IP(指令指针)
  • 从堆栈弹出 CS(代码段寄存器)
  • 从堆栈弹出 FLAGS(标志寄存器)

说白了,IRET 就是告诉 CPU:“我干完了,你回到之前被打断的地方,继续执行。” 如果你误用了 RET 而不是 IRET,那标志寄存器就不会被恢复,程序状态就乱了。我曾经见过一个同事,调试了半天发现中断返回后系统总是不对劲,最后发现是用了 RET 而不是 IRET——嗯,这种错误,犯一次就记住了。

警告:在 64 位模式下,IRET 的行为略有不同,它会弹出更多的信息。如果你在写 64 位系统的 ISR,务必查阅对应 CPU 手册。别拿 32 位的经验直接套用。

4.4 本章知识体系图

下面这张图,帮你理清 ISR 编写的核心逻辑:

中断服务程序(ISR)核心流程 中断发生 保存现场(压栈) 执行ISR处理逻辑 恢复现场(出栈) IRET 中断返回 关键要点 • ISR 要短小精悍 • 保存/恢复顺序相反 • 关中断时间要短 • 避免不可重入函数 • IRET 恢复 CS/IP/FLAGS • 别用 RET 代替 IRET • 共享资源要加保护 • 64位下 IRET 行为不同

4.5 避坑指南:我踩过的那些雷

写 ISR 这么多年,我总结了几条“血泪教训”:

  • 别在 ISR 里开循环等待:比如用 while(flag); 等某个硬件状态。万一那个状态永远不来,系统就卡死了。我早期做嵌入式时,就因为这个原因,导致整个系统“假死”,查了两天才找到问题。
  • 中断嵌套要小心:高优先级中断可以打断低优先级中断。但嵌套层数太多,堆栈可能溢出。我建议:除非必要,否则尽量用“中断禁止”代替“嵌套”。
  • 清除中断标志要及时:很多硬件在中断响应后,不会自动清除中断标志。如果你忘了在 ISR 里手动清除,那中断会反复触发,系统就一直在处理同一个中断。嗯,这种“死循环”我见过不止一次。

我的习惯:写 ISR 时,我会在开头先关中断,然后保存现场,接着清除中断标志,再处理逻辑,最后恢复现场、开中断、执行 IRET。这个顺序,我用了十几年,基本没出过问题。

好了,ISR 的编写规范、现场保存与恢复、以及 IRET 指令,就聊到这儿。记住:中断处理是系统稳定性的基石,写的时候多一分小心,调试的时候就少一分痛苦。


专注资料整理