ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用C语言模拟中断机制:从寄存器到嵌套的完整实现

用C语言模拟中断机制:从寄存器到嵌套的完整实现 做过计组实验的同学应该都有这种感受教材上把中断机制画得明明白白可一旦真要靠硬件实验或者仿真软件把它跑出来立刻就卡住了。中断向量表怎么跳现场怎么保护优先级怎么判断嵌套又该怎么实现我这次把中断机制用纯C语言的模拟程序完整实现了一遍从零开始构建寄存器、中断控制器、指令执行循环和中断服务程序代码跑通以后很多之前靠死记硬背的概念自动就通了。文章适合正在上计算机组成原理实验课、需要写中断实验报告或者单纯想搞清楚中断内部流程的人代码我放在第4章可以直接抄走跑一遍。1. 先把抽象变具体中断机制到底在干什么1.1 用一个课堂场景解释中断的生命周期你可以把CPU想象成一个正在专心打游戏的人耳机里放着音乐、画面正在推塔。这时候突然电话来了他必须暂停游戏把当前的血量、位置、装备状态记清楚然后切出去接电话。接完电话再切回游戏从刚才断掉的地方继续打。这个场景里几乎包含了中断机制的全部要素游戏程序是主程序电话铃声是中断请求放下手机记下当前状态是保存现场接通电话是进入中断服务程序挂掉电话是执行完ISR切回游戏是恢复现场继续推塔是返回主程序的下一条指令继续执行。唯一不同的是CPU平时会被更高优先级的事件打断很多次比如键盘输入、磁盘读写、定时器到点、网卡收到数据包。在计算机组成原理课程里中断机制的价值在于它让CPU不用一直傻等慢速的外设。没有中断的时候CPU只能用轮询的方式不停查看外设状态浪费大量时间有了中断外设完成一件事只需要发一个电信号CPU处理完手头这一条指令后立刻响应速度就完全不一样了。我在模拟程序里并不需要真的构造硬件电路而是把这一套流程翻译成数据结构用一个结构体保存CPU寄存器和内存用一个数组保存中断源的状态用函数指针或者分支语句模拟中断服务程序再让主循环在每执行完一条指令之后“顺便看一眼”有没有中断请求。1.2 实验中必须复现的四个核心要素中断机制虽然概念很多但落到代码层面你们实验报告里真正需要讲清楚的就四个核心要素。把它们单独列出来后面的代码理解起来会轻松很多。核心要素硬件中的角色本模拟程序中的实现中断请求外设通过信号线向CPU发出请求结构体里的pending标志位置1中断屏蔽CPU决定哪些中断不允许响应psw寄存器最低位作为总中断允许标志IF中断向量每个中断源对应一个服务程序入口编号用vector字段标记配合服务函数中断服务程序真正处理外设业务的代码isr_clock、isr_printer、isr_io三个函数另外还有一个常被忽略但实验里会考的东西中断优先级。多个中断请求同时到达时CPU到底先响应谁这靠priority字段表示数字越小优先级越高。如果低优先级中断在处理过程当中收到了更高优先级请求还会发生中断嵌套也就是被打断的中断、新来的中断、再回到原来的中断这个顺序一定不能乱。1.3 为什么“模拟”比看PPT有效计算机组成原理的中断实验不同学校提供的条件差别很大。有的实验室用数字逻辑实验箱靠拨码开关和LED灯模拟中断请求和响应这种做法的缺点是中间状态看不见脉冲过去就没了有的学校用Logisim或者其它仿真软件搭电路能观察信号但搭一个完整的中断控制器很费劲还有的学校直接要求在开发板上写嵌入式程序但那样又偏重于代码而不是CPU内部机制。用模拟程序做实验最大的优势是“透明”。CPU内部的每个寄存器变化、每次入栈出栈、每次中断请求的置位和清除都可以用printf打印出来。你能亲眼看到PC在执行完第几条指令后被中断打断现场被压进栈里ISR执行完又原封不动地弹回来。这种可视化的效果比对着逻辑电路图脑补要直观得多。我当时选择纯C语言而不是写Verilog或者用硬件仿真软件是因为实验报告要求分析中断响应过程C代码的每一步骤跟理论模型几乎一一对应写起实验总结来非常顺。下面我就详细说说这个模拟程序是怎么设计出来的。2. 模拟程序的结构设计寄存器、向量表与控制器2.1 CPU核心状态与指令集定义先把CPU抽象成最简结构。这个模拟程序不需要仿真完整的MIPS或者RISCV流水线只需要一个能执行几条简单指令的模型重点是展示中断机制而不是指令系统。typedef struct { int pc; // 程序计数器 int acc; // 累加器 int sp; // 栈指针 int psw; // 程序状态字最低位表示中断允许标志IF int memory[64]; // 简单内存模型 } CPU;指令集我设计了四条。LOAD把立即数加载到ACCADD做累加STORE把ACC写入内存HALT结束程序。看起来非常简单但它足以构成一个“一步一步执行”的主程序循环而中断机制恰恰就需要这种可以一步步观察的过程。为什么要用这么简化的指令集因为实验的核心目标是理解中断响应流程指令本身反而不重要。如果我把指令集搞成几十条代码量翻倍读者注意力全被指令解码吸引走了反而冲淡了主线。指令编码我采用“操作码立即数”的固定格式比如内存中第0号单元是1第1号单元是10就表示一条“LOAD 10”指令。CPU取指时读memory[pc]得到操作码再看下一条内存单元得到操作数执行完后PC加2。2.2 中断控制器IRR、IMR、ISR怎么协同实验里经常提到的8259A可编程中断控制器内部有三个核心寄存器组IRR记录哪些外设提出了中断请求IMR记录哪些中断被屏蔽ISR记录当前CPU正在服务哪个中断。我这套模拟程序没有严格区分IRR和IMR寄存器而是把它们合并成了更直观的C结构体数组typedef struct { int vector; // 中断向量号 int priority; // 优先级数值越小优先级越高 char name[20]; // 中断源名称 int pending; // 是否有中断请求相当于IRR位 int active; // 是否正在服务相当于ISR位 } IntSource;三个中断源的初始配置如下下标namevectorpriority场景0时钟中断00定时器到时最高优先级1打印机中断11打印机完成一次输出2IO设备中断22外设数据传输完毕最低优先级这里有个细节值得注意优先级数字越小级别越高这是我故意设置的跟很多教材一致。如果你们的实验文档里定义相反只需要改比较方向就行不影响原理。2.3 中断响应流程的阶段划分我把一次完整的中断过程拆成四个阶段这也是实验报告里最好用的分析框架第一阶段是中断请求。外设在某个时刻把对应中断源的pending置成1。注意这个请求只是“记录”下来CPU不会立刻放下手头的事而是要等当前正在执行的这条指令结束。第二阶段是中断响应发生在主循环的“检查中断”环节。CPU先通过psw最低位判断中断是否被允许再在多个pending的中断源里找出优先级最高的那个。选中之后做三件事把当前PC入栈保存把当前PSW入栈保存把请求状态清掉并标记active。第三阶段是中断处理。CPU跳到中断服务程序执行ISR里做的事情就是对外设做出具体响应比如读取设备数据、清除设备标志位。第四阶段是中断返回。把之前压栈的PSW和PC依次弹出恢复CPU原来的状态。注意弹出顺序和压栈顺序严格相反这是现场保护里最容易写错的地方。代码的主循环设计成两个动作交替进行执行一条主程序指令、检查一次中断请求。这个“每条指令结束后的检查”就是整个中断机制最容易出问题的地方我会在第3章重点讲。3. C语言模拟实现核心代码是如何一步步跑起来的3.1 数据结构和全局变量下面这段是程序的“零件清单”包括CPU、三个中断源和模拟栈。我把栈设计成单独的int数组栈指针初始化为-1表示空栈。#include stdio.h #define MEM_SIZE 64 #define INT_CLOCK 0 #define INT_PRINTER 1 #define INT_IO 2 typedef struct { int pc; int acc; int sp; int psw; int memory[MEM_SIZE]; } CPU; typedef struct { int vector; int priority; char name[20]; int pending; int active; } IntSource; CPU cpu; IntSource sources[3]; int stack[16]; int isr_depth 0; int step_count 0; int running 1; void init_cpu(void) { cpu.pc 0; cpu.acc 0; cpu.sp -1; cpu.psw 1; // IF 1允许中断 for (int i 0; i MEM_SIZE; i) { cpu.memory[i] 0; } } void init_int_source(void) { sources[INT_CLOCK].vector 0; sources[INT_CLOCK].priority 0; snprintf(sources[INT_CLOCK].name, 20, 时钟中断); sources[INT_CLOCK].pending 0; sources[INT_CLOCK].active 0; sources[INT_PRINTER].vector 1; sources[INT_PRINTER].priority 1; snprintf(sources[INT_PRINTER].name, 20, 打印机中断); sources[INT_PRINTER].pending 0; sources[INT_PRINTER].active 0; sources[INT_IO].vector 2; sources[INT_IO].priority 2; snprintf(sources[INT_IO].name, 20, IO设备中断); sources[INT_IO].pending 0; sources[INT_IO].active 0; }我在最初实现的时候把active标记漏掉了结果出现了同一个中断被重复响应的bug。后来才意识到ISR寄存器就对应这个active只有标记了它才能防止相同优先级的中断反复抢占。3.2 主循环与取指执行主循环是整个模拟器的发动机。每轮先执行一条指令指令结束之后把step_count加一然后检查外部事件看看有没有中断请求该置位最后再寻找最高优先级的中断并响应。int main(void) { init_cpu(); init_int_source(); load_program(); while (running) { execute_one_instruction(); step_count; trigger_events_by_step(step_count); int req find_highest_pending(); if (req 0 (cpu.psw 1)) { handle_interrupt(req); } } return 0; }execute_one_instruction很简单解码并执行指令void execute_one_instruction(void) { int opcode cpu.memory[cpu.pc]; int operand cpu.memory[cpu.pc 1]; switch (opcode) { case 0: break; // NOP case 1: cpu.acc operand; printf([周期%2d] 主程序: LOAD ACC %d\n, step_count, operand); cpu.pc 2; break; case 2: cpu.acc operand; printf([周期%2d] 主程序: ADD ACC %d\n, step_count, cpu.acc); cpu.pc 2; break; case 3: cpu.memory[operand] cpu.acc; printf([周期%2d] 主程序: STORE mem[%d] %d\n, step_count, operand, cpu.acc); cpu.pc 2; break; case 4: printf([周期%2d] 主程序: HALT\n, step_count); cpu.pc 1; running 0; break; default: printf(未知指令 opcode%d\n, opcode); running 0; } }触发外部中断事件的动作由trigger_events_by_step完成。我给程序设置了两个硬编码触发点时钟中断在第3个周期到达IO中断在第7个周期到达。void trigger_events_by_step(int step) { if (step 3) { sources[INT_CLOCK].pending 1; printf(\n [周期%2d] 时钟中断请求到达IRR置位\n, step); } if (step 7) { sources[INT_IO].pending 1; printf( [周期%2d] IO设备中断请求到达IRR置位\n, step); } }这相当于实验台上装了两个按钮到特定时刻就向CPU发出脉冲信号。实际硬件中这些信号来自定时器、串口等外设这里用周期数触发已经足够说明问题。3.3 中断判别与响应优先级和嵌套的实现find_highest_pending函数遍历三个中断源找出当前有pending请求且没有被active标记的中断源中priority最小的那个。这里有一点必须明确如果某个中断源已经处于active状态即使它又置位了pending也不能再选中它否则就无限自打断。int find_highest_pending(void) { int best -1; for (int i 0; i 3; i) { if (sources[i].pending !sources[i].active) { if (best -1 || sources[i].priority sources[best].priority) { best i; } } } return best; }handle_interrupt是核心中的核心它负责保存现场、执行ISR、恢复现场。为了演示嵌套我把它设计成可递归调用的函数嵌套深度由C语言的函数调用栈天然地管理同时用一个显式的stack数组展示硬件压栈的思想。void push(int value) { cpu.sp; stack[cpu.sp] value; } int pop(void) { int value stack[cpu.sp]; cpu.sp--; return value; } void handle_interrupt(int idx) { push(cpu.pc); push(cpu.psw); sources[idx].pending 0; sources[idx].active 1; isr_depth; printf([中断%d] 响应%s现场保存完成SP%d嵌套深度%d\n, sources[idx].vector, sources[idx].name, cpu.sp, isr_depth); if (idx INT_CLOCK) { isr_clock(); } else if (idx INT_PRINTER) { isr_printer(); } else if (idx INT_IO) { isr_io(); } int saved_psw pop(); int saved_pc pop(); cpu.psw saved_psw; cpu.pc saved_pc; sources[idx].active 0; isr_depth--; printf([中断%d] 退出%s现场恢复完成SP%d嵌套深度%d\n\n, sources[idx].vector, sources[idx].name, cpu.sp, isr_depth); }很多同学一开始看这段代码会疑惑这里push的cpu.pc难道不是中断响应前主程序的下一条指令地址吗没错确实是的。我并没有在进入ISR时真正修改cpu.pc而是利用C语言的函数调用来代替“PC跳转到中断向量地址”这个过程。这样写的好处是递归嵌套时C语言的调用栈会自动保存函数返回地址配合显式的stack数组展示现场保存/恢复既直观又不会写出一堆指针跳转的骚操作。3.4 三个中断服务程序与嵌套场景设计接下来是三个ISR函数。时钟中断响应后简单打印两句表示处理完成打印机中断模拟打印缓冲区写入和打印头动作IO中断则设计了一个嵌套场景——它在处理数据的过程中产生了打印请求于是主动把打印机中断置为pending并马上响应更高优先级的打印机中断。void isr_clock(void) { printf( ISR[时钟]: 读取定时器计数值更新系统时间...\n); printf( ISR[时钟]: 清除定时器请求标志中断处理完毕\n); } void isr_printer(void) { printf( ISR[打印机]: 写入打印缓冲区控制打印头动作...\n); printf( ISR[打印机]: 打印完成清除忙状态\n); } void isr_io(void) { printf( ISR[IO]: 正在接收外部设备数据...\n); printf( ISR[IO]: 数据接收完成准备输出到打印机\n); sources[INT_PRINTER].pending 1; printf( ISR[IO]: 发出打印请求在IO服务期间产生打印机中断\n); int req find_highest_pending(); if (req 0 (cpu.psw 1)) { handle_interrupt(req); } printf( ISR[IO]: 嵌套返回继续处理剩余IO数据\n); }这个嵌套设计非常重要。程序执行流程会变成主程序执行到第7个周期时响应IO中断IO中断处理到一半又响应打印机中断打印机处理完后回到IO中断继续IO中断结束后回到主程序。这样一个案例就把“中断嵌套”“优先级抢占”“现场保护”三个知识点全部串起来了。注意handle_interrupt里的saved_pc和saved_psw保存的都是主程序的现场。嵌套发生时IO中断的现场并不通过显示的stack数组保存而是靠C函数递归调用栈保存的。为了在实验报告里解释清楚我在代码注释里写了“C函数调用栈承担了部分现场保存功能”这个说法你们的老师是认的。不过有一说一如果想完全模拟硬件行为应该把所有环境都压进显式stack数组我在第5章会再讨论这个问题。4. 完整代码与运行效果验证4.1 可直接编译运行的完整代码下面就是可以直接复制保存为interrupt_sim.c并运行的完整代码。在Linux或者Windows上看使用gcc interrupt_sim.c -o interrupt_sim编译执行./interrupt_sim即可。#include stdio.h #define MEM_SIZE 64 #define INT_CLOCK 0 #define INT_PRINTER 1 #define INT_IO 2 typedef struct { int pc; int acc; int sp; int psw; int memory[MEM_SIZE]; } CPU; typedef struct { int vector; int priority; char name[20]; int pending; int active; } IntSource; CPU cpu; IntSource sources[3]; int stack[16]; int isr_depth 0; int step_count 0; int running 1; void load_program(void) { cpu.memory[0] 1; cpu.memory[1] 10; // LOAD 10 cpu.memory[2] 2; cpu.memory[3] 5; // ADD 5 cpu.memory[4] 3; cpu.memory[5] 30; // STORE mem[30] cpu.memory[6] 1; cpu.memory[7] 0; // LOAD 0 cpu.memory[8] 2; cpu.memory[9] 3; // ADD 3 cpu.memory[10] 3; cpu.memory[11] 31; // STORE mem[31] cpu.memory[12] 1; cpu.memory[13] 8; // LOAD 8 cpu.memory[14] 2; cpu.memory[15] 7; // ADD 7 cpu.memory[16] 3; cpu.memory[17] 32; // STORE mem[32] cpu.memory[18] 4; // HALT } void init_cpu(void) { cpu.pc 0; cpu.acc 0; cpu.sp -1; cpu.psw 1; for (int i 0; i MEM_SIZE; i) { cpu.memory[i] 0; } } void init_int_source(void) { sources[INT_CLOCK].vector 0; sources[INT_CLOCK].priority 0; snprintf(sources[INT_CLOCK].name, 20, 时钟中断); sources[INT_CLOCK].pending 0; sources[INT_CLOCK].active 0; sources[INT_PRINTER].vector 1; sources[INT_PRINTER].priority 1; snprintf(sources[INT_PRINTER].name, 20, 打印机中断); sources[INT_PRINTER].pending 0; sources[INT_PRINTER].active 0; sources[INT_IO].vector 2; sources[INT_IO].priority 2; snprintf(sources[INT_IO].name, 20, IO设备中断); sources[INT_IO].pending 0; sources[INT_IO].active 0; } void execute_one_instruction(void) { int opcode cpu.memory[cpu.pc]; int operand cpu.memory[cpu.pc 1]; switch (opcode) { case 0: break; case 1: cpu.acc operand; printf([周期%2d] 主程序: LOAD ACC %d\n, step_count, operand); cpu.pc 2; break; case 2: cpu.acc operand; printf([周期%2d] 主程序: ADD ACC %d\n, step_count, cpu.acc); cpu.pc 2; break; case 3: cpu.memory[operand] cpu.acc; printf([周期%2d] 主程序: STORE mem[%d] %d\n, step_count, operand, cpu.acc); cpu.pc 2; break; case 4: printf([周期%2d] 主程序: HALT\n, step_count); cpu.pc 1; running 0; break; default: printf(未知指令 opcode%d\n, opcode); running 0; } } void trigger_events_by_step(int step) { if (step 3) { sources[INT_CLOCK].pending 1; printf(\n [周期%2d] 时钟中断请求到达IRR置位\n, step); } if (step 7) { sources[INT_IO].pending 1; printf( [周期%2d] IO设备中断请求到达IRR置位\n, step); } } int find_highest_pending(void) { int best -1; for (int i 0; i 3; i) { if (sources[i].pending !sources[i].active) { if (best -1 || sources[i].priority sources[best].priority) { best i; } } } return best; } void push(int value) { cpu.sp; stack[cpu.sp] value; } int pop(void) { int value stack[cpu.sp]; cpu.sp--; return value; } void isr_clock(void) { printf( ISR[时钟]: 读取定时器计数值更新系统时间...\n); printf( ISR[时钟]: 清除定时器请求标志中断处理完毕\n); } void isr_printer(void) { printf( ISR[打印机]: 写入打印缓冲区控制打印头动作...\n); printf( ISR[打印机]: 打印完成清除忙状态\n); } void isr_io(void) { printf( ISR[IO]: 正在接收外部设备数据...\n); printf( ISR[IO]: 数据接收完成准备输出到打印机\n); sources[INT_PRINTER].pending 1; printf( ISR[IO]: 发出打印请求在IO服务期间产生打印机中断\n); int req find_highest_pending(); if (req 0 (cpu.psw 1)) { handle_interrupt(req); } printf( ISR[IO]: 嵌套返回继续处理剩余IO数据\n); } void handle_interrupt(int idx) { push(cpu.pc); push(cpu.psw); sources[idx].pending 0; sources[idx].active 1; isr_depth; printf([中断%d] 响应%s现场保存完成SP%d嵌套深度%d\n, sources[idx].vector, sources[idx].name, cpu.sp, isr_depth); if (idx INT_CLOCK) { isr_clock(); } else if (idx INT_PRINTER) { isr_printer(); } else if (idx INT_IO) { isr_io(); } int saved_psw pop(); int saved_pc pop(); cpu.psw saved_psw; cpu.pc saved_pc; sources[idx].active 0; isr_depth--; printf([中断%d] 退出%s现场恢复完成SP%d嵌套深度%d\n\n, sources[idx].vector, sources[idx].name, cpu.sp, isr_depth); } int main(void) { init_cpu(); init_int_source(); load_program(); while (running) { execute_one_instruction(); step_count; trigger_events_by_step(step_count); int req find_highest_pending(); if (req 0 (cpu.psw 1)) { handle_interrupt(req); } } return 0; }有一点要提前说明snprintf在Windows的MSVC老版本中可能行为不同遇到编译警告时可以改成strcpy或者手动赋值不影响运行。4.2 运行输出与逐行分析程序编译运行后输出大概长这样。我截取前20行左右足够说明执行流程。[周期 0] 主程序: LOAD ACC 10 [周期 1] 主程序: ADD ACC 15 [周期 2] 主程序: STORE mem[30] 15 [周期 3] 时钟中断请求到达IRR置位 [中断0] 响应时钟中断现场保存完成SP1嵌套深度1 ISR[时钟]: 读取定时器计数值更新系统时间... ISR[时钟]: 清除定时器请求标志中断处理完毕 [中断0] 退出时钟中断现场恢复完成SP-1嵌套深度0 [周期 3] 主程序: LOAD ACC 0 [周期 4] 主程序: ADD ACC 3 [周期 5] 主程序: STORE mem[31] 3 [周期 6] 主程序: LOAD ACC 8 [周期 7] IO设备中断请求到达IRR置位 [中断2] 响应IO设备中断现场保存完成SP1嵌套深度1 ISR[IO]: 正在接收外部设备数据... ISR[IO]: 数据接收完成准备输出到打印机 ISR[IO]: 发出打印请求在IO服务期间产生打印机中断 [中断1] 响应打印机中断现场保存完成SP3嵌套深度2 ISR[打印机]: 写入打印缓冲区控制打印头动作... ISR[打印机]: 打印完成清除忙状态 [中断1] 退出打印机中断现场恢复完成SP1嵌套深度1 ISR[IO]: 嵌套返回继续处理剩余IO数据 [中断2] 退出IO设备中断现场恢复完成SP-1嵌套深度0 [周期 7] 主程序: ADD ACC 7 [周期 8] 主程序: STORE mem[32] 15 [周期 9] 主程序: HALT逐行分析就能看到规律主程序执行LOADACC10、ADD得到15、STORE写内存一路正常。第3周期结束时CPU检测到时钟中断请求于是在执行下一条指令之前先去响应中断SP变成1说明压入了PC和PSW两个数据。处理完时钟中断后SP恢复成-1说明现场完全恢复。第7周期结束时的输出是全程序最漂亮的部分。IO中断先被响应SP变成1然后IO处理过程中打印请求到了打印机中断以更高优先级抢占SP从1跳到3这是因为又压入了两个数据嵌套深度从1变成2。打印机中断会先退出SP回到1之后IO中断再退出SP回到-1。整个过程完美展示了高级中断打断低级中断、逐级返回的完整链路。4.3 修改参数看不同现象跑通基础版本后我建议你做几个小实验观察不同现象这比直接抄代码写完实验报告有价值得多。第一把trigger_events_by_step里的时钟中断触发周期从3改成10也就是在主程序HALT之后才触发。你会发现中断请求虽然置位了但主程序已经running0循环退出中断永远得不到响应。这个实验能说明一个道理中断响应的前提是CPU还有正在执行的指令流CPU停机后没有人去检测中断请求了。真实硬件中停机期间的中断一般通过DMA或者复位等机制处理这是另外一个话题但理解了这个边界你对中断机制的理解会更深。第二把IO中断的priority改成比时钟中断更高的值比如-1然后同时触发两个中断。现在的find_highest_pending逻辑会选中优先级高的那个先响应另一个继续pending等第一个处理完再自动在下一个指令周期被处理。这个现象对应教材上说的“多个中断请求同时到达时按照优先级依次响应”。第三把cpu.psw初值改成0也就是关中断。你会发现即使外设请求置位了主循环里的if条件不成立中断永远不会得到响应。这直接对应“中断允许标志”的作用。第四把push(cpu.psw)和pop()的顺序对调观察压栈和出栈不匹配会有什么后果。这是一个很经典的破坏性实验c语言程序不一定崩溃但打印出来的恢复值会错位正好用来理解“后进先出”的意义。我强烈建议这些变体实验每一个都跑一遍并把输出截图附在实验报告里。事实上有经验的老师一眼就能看出你是真的做了实验还是只抄了结论。5. 实验中会踩的坑与实验报告加分写法5.1 常见的逻辑误区第一是中断请求置位后没有清0。很多自己写的模拟程序第一次跑起来会出现“同一个中断被无限响应”的现象原因就是响应完成之后没有把sources[idx].pending清零。硬件里对应的概念叫做“中断请求信号未撤销”需要ISR写特殊的寄存器或者发送EOI命令来清除。我的代码里在handle_interrupt开头就做了pending0这个操作对应硬件的“中断应答”动作。第二是嵌套深浅和恢复顺序混乱。我在调试嵌套功能时一度在isr_io里又调用handle_interrupt(printer)可是打印机中断退出后我发现SP没有回到IO中断的现场位置排查半天才发现是handle_interrupt里恢复现场时先pop了PSW再pop了PC而压栈时是先压PC再压PSW顺序完全反了。后来我总结了一个口诀压栈顺序必须和pop相反第一次压的PC最后一次pop出来一定是PC。第三是误以为响应中断时CPU会立刻跳走。现实中CPU必须等当前指令完全结束才能响应因为指令执行到一半寄存器和内存的状态是不完整的强行打断会导致现场无法恢复。我的主循环结构就是“执行一条指令再检查中断”这种设计天然保证了这一点。如果你在实验报告里画时序图一定要把这个“指令边界”画清楚。第四个坑稍微隐蔽模拟程序里的中断源只有三个但实际的中断系统可能有几十上百个中断源。当优先级相同的两个中断同时pending时怎么选硬件上通常会比较向量号大小来决定。我代码里优先级相同的情况直接保留先找到的那个严格来说不够严谨。要做到完全严谨应该在优先级相同的情况下再比较vector越小越优先或者按照硬件设计的具体规则排序。改进代码很简单在find_highest_pending里加一个vector的次级比较条件就行。5.2 从“跑通代码”到“深入理解”能让程序跑起来不算完实验加分的关键在于能不能讲清楚代码和硬件模型的对应关系。这里我列几个可以写进实验报告的分析点每一个都能展示你确实理解了这个东西。现场保护为什么至少要压入PC和PSWPC是恢复运行必须的PSW里除了中断允许标志通常还有条件码、工作模式等重要状态。真实CPU保存现场往往还要保存通用寄存器比如SP或者ACC。我们这个模拟程序用C语言函数调用做了部分现场保护所以只显式压入PC和PSW这个简化要在报告里说明白。中断返回为什么不能用普通函数返回代替概念上普通函数返回是把返回地址从函数栈中弹出而中断返回除了弹出PC还要恢复PSW并且可能要执行特殊的特权指令。可以让学生对比ret和iret的区别这在很多教材里都有强调。屏蔽的意义在什么地方我的程序里psw最低位是总中断允许标志IF相当于一次屏蔽所有可屏蔽中断。硬件上还有Interrupt Mask Register可以单独屏蔽某一类中断。这个模拟程序没有实现IMR但思路是一样的。你可以在实验报告里提出“如果我想屏蔽IO中断但允许时钟中断应该在代码里加什么字段”这样的思考题老师会觉得你不是在应付。EOI最好也提一下。8259A的中断处理流程要求ISR在结束前发送End of Interrupt命令来清除ISR寄存器位。我们的代码在handle_interrupt的退出阶段统一做active0类似于自动EOI。很多同学不理解为什么硬件要专门设计EOI定时你可以延伸阅读一下在报告里用两三句话说清楚是有加分的。5.3 实验报告写作建议与心得方向实验报告最容易写成的样子是标题代码运行截图然后“通过本次实验我学会了中断机制”。这种写法太平庸。既然代码都跑通了你应该把分析深度拉起来。我建议报告里安排三块内容第一块是“中断响应时序分析”把运行输出按照“主程序指令、中断请求、中断响应、ISR执行、中断返回”五个类别标出来画一张时间轴或者表格。不要用再说流程直接让读者从数据里看到流程。比如可以用下面这种表格周期事件类型内容SP变化嵌套深度0主程序LOAD ACC10-102主程序STORE mem[30]15-103中断请求时钟中断IRR置位-103中断响应压入PC和PSW113之后ISR时钟处理113之后中断返回弹出PSW和PC-10第二块是“中断嵌套的分析”把IO中断被打印机中断抢占的局部状态变化单独展开说明为什么此时SP会从1变成3为什么嵌套返回后SP先回1再回-1以及active和pending各自的角色。第三块是“改进设想”列出模拟程序与真实CPU中断机制的差距哪里做了简化哪里完全等价。比如我们用了函数指针栈区处理嵌套真实硬件用的是硬件栈我们没实现向量表中的地址跳转而是用C函数调用模拟我们可以扩展多级优先级也可以增加按键交互。这个部分是展示学习深度的地方不要省略。另外提醒一下实验报告里的“收获”不要写得太空。与其写“通过本次实验我深刻理解了中断”不如写“我发现中断响应的时机在每条指令结束后因此修改了主循环结构在execute_one_instruction之后才检查request这让我真正理解了CPU不会在指令中间被打断”。具体到一个真实的调试经历比任何漂亮话都有说服力。最后分享一个我这几年做实验的小习惯拿到一个理论机制先用最少的代码把它跑出可视化输出再回头看书本概念。中断这个知识点我光看书看了三四遍每次都觉得懂了写代码的时候照样卡在优先级和嵌套上。现在代码跑通了再去看教材上那个中断处理流程图每一根箭头对应的是代码里的哪一行心里全都有数。你们如果也卡在计组实验可以试试这套“先模拟、后理论”的思路比翻来覆去看PPT有效率得多。
返回列表