引言
- 在上一章节中,我们实现了单个任务,然而这个任务是个死循环,那么怎么才能跳出这个任务从而执行别的任务呢?
- 答案就是利用时钟中断,由于我们在内核中并没有实现中断相关功能,所以本章节我们先学习一下中断的使用,下一个章节我们再在中断的基础上实现多任务吧
中断相关的准备工作
- 前面我们已经学习过了保护模式下的中断,并做了中断相关实验,现在我们就先做一下中断相关的准备工作吧
- 首先在 loader.asm 中创建一个中断描述符表 IDT (我们先创建 IDT 表,后期再填充)
IDT_BASE: %rep 256 Gate CODE32_FLAT_SELECTOR, 0, 0, DA_INTR_GATE %endrep IDT_LEN equ $ - IDT_BASE
- 当然,还要加载 IDT
IDT_PTR : dw IDT_LEN - 1 dd IDT_BASE lidt [IDT_PTR]
问题
- 回顾当时我们把中断服务程序直接写在了 loader.asm 中,然而现在我们不可能把所有的中断服务程序都写在 loader.asm 中吧,我们也不知道所有的中断服务程序具体干了什么
- 合理的做法是在内核程序中,当需要用到某一个中断时,再实现对应的中断服务程序。只有需求方才知道中断中应该干什么
- 然而,问题又引出了问题,如果中断服务程序由内核 kernel 实现,那么该中断服务程序入口地址怎么填充到中断描述符表 IDT 中呢
- 于是我们得想办法把 IDT 位置信息告诉 kernel
共享内存
- 通过共享内存的方式将 IDT 信息告诉 kernel
- 共享内存一般用于实现不同程序间数据交互
- 比如,我们规划内存地址 0xA000 为共享内存的起始地址。目前内存中的使用情况如下:
- 共享内存中的数据位置必须双方提前定义好,虽然我们现在只需要将 IDT 位置信息传递到 kernel,但是未雨绸缪,我们把 GDT 和 LDT 信息都放到共享内存吧,说不定以后有需要
- loader.asm 中将数据放入共享内存
PutDataToShare:
; 将 GDT 描述符表基地址放到共享内存 GDT_ENTRY_ADDR 中 mov dword [GDT_ENTRY_ADDR], GDT_BASE ; 将 GDT 描述符个数放到共享内存 GDT_SIZE_ADDR 中 mov dword [GDT_SIZE_ADDR], GDT_LEN / 8 ... ret
- 在 kernel 中取数据
U32* p = (U32*)0xA000; printk("GDT Entry: %x\n", *p); p = (U32*)0xA004; printk("GDT Size: %d\n", *p); // ... 读取其它数据
- 创建 “share.h” 文件,将共享内存相关数据地址放入,如果想使用共享内存中的数据,则包含头该文件,通过指针解引用的方式就可获取到数据了
#define SHARE_START_ADDR 0xA000 #define GDT_ENTRY_ADDR (SHARE_START_ADDR + 0) #define GDT_SIZE_ADDR (SHARE_START_ADDR + 4) #define LDT_ENTRY_ADDR (SHARE_START_ADDR + 8) #define LDT_SIZE_ADDR (SHARE_START_ADDR + 12) #define IDT_ENTRY_ADDR (SHARE_START_ADDR + 16) #define IDT_SIZE_ADDR (SHARE_START_ADDR + 20) // ...
- 共享内存用法是不是很简单,接下来继续主线任务吧
注册中断服务程序
- 现在 IDT 位置已经通过共享内存的方式传递给了内核,而我们在 loader.asm 中创建的 IDT 表是不完整的,并没有绑定中断服务程序,接下来我们要实现一个接口函数,其作用就是根据中断号将终中断服务程序入口地址填充到 IDT 表中对应的中段描述符
- 函数名称: E_RET IrqRegister(E_IRQ_NUM irqNmu, F_ISR ifunc)
- 输入参数: E_IRQ_NUM irqNmu --中断号; F_ISR ifunc --中断服务程序
- 输出参数: 无
- 函数返回: E_OK:成功; E_ERR:失败
- 函数实现如下:
E_RET IrqRegister(E_IRQ_NUM irqNmu, F_ISR ifunc) { // 检查参数合法性 if(irqNmu >= IRQ_TOTAL || NULL == ifunc) return E_ERR; // 从共享内存中获取中段描述符表 IDT 地址 和大小 GATE* gate = (GATE*)(*(U32*)IDT_ENTRY_ADDR); U32 idtSize = *((U32*)(IDT_SIZE_ADDR)); // 合法性检查 if(NULL == gate || 0 == idtSize || irqNmu >= idtSize) return E_ERR; // 函数名就相当于函数入口地址 // 由于使用平坦模式,段基址为 0,那么函数名 func 地址就等同于段内偏移 (gate+irqNmu)->offset1 = (U16)((U32)ifunc); (gate+irqNmu)->offset2 = (U16)(((U32)ifunc)>>16); return E_OK; }
- 测试一下,实现一个中断服务程序
void int0x80_func(void) { printk("int0x80\n"); while (1); }
- 将其注册到中断描述符表中
IrqRegister(0x80, int0x80_func);
- 触发中断
asm volatile("int $0x80");
- 成功打印出字符串 “int 0x80”,这说明中断进入成功
中断返回
- 思考一下,为啥中断服务程序 int0x80_func 里有个 while(1) 死循环呢?
- 其实我是想强调一下中断返回的,中断返回指令是 iret,而函数默认使用的是 ret 指令,所以我们应该在函数的默认内嵌汇编 iret 指令
void int0x80_func(void) { printk("int0x80\n"); asm volatile("iret"); }
- 在触发中断后打印字符串 "After int 0x80\n",目前程序的执行逻辑是触发 int 0x80 中断,进入中断服务程序,将打印字符串 “int 0x80”,然后退出中断,再打印字符串 "After int 0x80\n"
IrqRegister(0x80, int0x80_func); // 注册 0x80 号中断服务程序 asm volatile("int $0x80"); // 触发 0x80 号中断 printk("After int 0x80\n");
- 编译运行,发现并没有打印出字符串 "After int 0x80\n",这是怎么回事呢?
- 回顾一下 C与汇编混合编程,根据约定,函数起始位置增加了两条指令 "push ebp" 和 "mov ebp, esp" 函数结束末尾应有两条指令,"pop ebp" 和 "ret",如果使用正常的函数返回,其实这 4 条指令是被编译器自动封装的,我们不需要关心,然而我们提前使用了 “iret” 中断返回指令,此时栈指针 esp 位置是错的,需要在 "iret" 前增加一条指令 "pop ebp",不过,这不是一种好方法,更好的方式是返回前使用 “leave” 指令,比如当中断函数内部又调用函数时,此时就不能用 "pop ebp" 指令了,而只能用 leave 指令
- 于是,中断服务程序就变成了:
void int0x80_func(void) { printk("int0x80\n"); asm volatile("leave;iret"); }
- 这回再编译运行,发现可以成功打印出字符串 "After int 0x80\n"
绑定默认中断服务程序
- 由于在创建 IDT 时未绑定默认中断服务程序,如果此时触发中断会发生未知的错误,所以必须给所有中断绑定一个默认中断服务程序
- 先实现一个默认中断服务函数
- 函数名称: static void DefaultHander(void)
- 输入参数: 无
- 输出参数: 无
- 函数返回: 无
- 函数实现如下:
static void DefaultHander(void) { asm volatile("leave;iret"); // 中断返回 }
- 接下来将该中断服务函数绑定到中断描述符表 IDT 中的每一项吧,这个没啥说的,就是利用上面已经实现的 IrqRegister 函数而已
- 总结一下,目前涉及到的文件有:irq.c、irq.h、share.h、main.c
8259A 驱动
- 接下来实现的完整代码见:8259A.asm、8259A.asm、main.c
- 因为后面的多进程需要借助时钟中断,时钟中断实现又要借助 8259A 芯片,所以这里先把 8259A 的驱动实现好
- 创建 “drivers” 文件夹,以后驱动代码都放到这个文件夹中,现在我们在这个文件夹中创建 “8259A.asm” 文件,驱动代码在 中断编程实验 中已经实现过了,现在只需要将代码从 loader.asm 中复制过来即可。由于增加了源文件,此时对应的 BUILD.json 配置文件也要更改
- 由于驱动代码是 nasm 汇编实现,需要被 C 语言调用,根据调用约定,那么汇编中的函数都需要改成如下格式:
asm_func: push ebp mov ebp, esp xxx ;pop ebp ; 推荐使用 leave leave ret
- 可以使用 [ebp + xxx] 来得到函数的传参,其实当前驱动代码里还是使用寄存器传参,只是我懒得改
- 想要被 C 调用,还需要使用 global 关键字
global pic_init global write_m_EOI global write_s_EOI global read_m_ISR global read_s_ISR global read_m_IRR global read_s_IRR global read_m_IMR global write_m_IMR global read_s_IMR global write_s_IMR global set_m_smm
- C 语言想要调用汇编中的函数,还差最后一点,创建 "8259A.h" 头文件放到 “include” 目录下,将如下内容写入
void pic_init(void); // 初始化可编程中断控制器 8259A - 级联 void write_m_EOI(void); // 手动结束主片中断 void write_s_EOI(void); // 手动结束从片中断 void read_m_ISR(void); // 读主片 ISR 寄存器的值,返回值存入 al 寄存器 void read_s_ISR(void); // 读从片 ISR 寄存器的值,返回值存入 al 寄存器 void read_m_IRR(void); // 读主片 IRR 寄存器的值,返回值存入 al 寄存器 void read_s_IRR(void); // 读从片 IRR 寄存器的值,返回值存入 al 寄存器 void read_m_IMR(void); // 读主片 IMR 寄存器的值,返回值存入 al 寄存器 void write_m_IMR(void); // 将 al 寄存器的值写入主片 IMR 中 void read_s_IMR(void); // 读从片 IMR 寄存器的值,返回值存入 al 寄存器 void write_s_IMR(void); // 将 al 寄存器的值写入从片 IMR 中 void set_m_smm(void); // 设置主片工作在特殊屏蔽模式
- 8259A 驱动实现完成之后,接下来我们就在 main 函数中测试一下吧
- 先准备一个中断服务程序,注意使用中断返回指令 "iret"
void TimerHandle(void) { static U32 count; SetCursorPos(0, 3); printk("TimerHandle: %d", count++); write_m_EOI(); // 手动结束主片中断 asm volatile("leave;iret"); // 中断返回 }
- 接下来只要把该中断服务程序注册到中断向量表中的对应位置
IrqRegister(IRQ32, TimerHandle); // 注册 0x20 号中断(时钟中断) • 最后别忘了初始化 8259A 和开启外部中断 pic_init(); ... asm volatile("sti"); // 开中断
- 编译运行,最终效果:
优化中断代码
- 使用 IrqRegister 函数确实很灵活性,但这种方式也是有一些弊端的
- 就比如绑定的中断服务程序最后必须加上中断返回 “asm volatile("leave;iret")”,不然程序就会崩溃
- 还有就是中断在各处注册,别人注册过的中断,有可能又被注册成你的中断服务程序,造成不必要的麻烦
- 后面在实现任务切换时需要保存上下文和恢复上下文工作,不建议跟逻辑部分写在一个函数里
- 针对以上的问题,我们把中断入口写在同一个文件中,并且拆分中断逻辑,具体做法如下
- 创建 “interrupt.asm” 文件,放到 “core” 文件夹下,其内容格式如下:
DefaultHandle: ret Int0x00_Entry: call DefaultHandle iret Int0x01_Entry: call DefaultHandle iret Int0x02_Entry: call DefaultHandle iret ; 下面是所有的中断服务程序入口,省略 ...
- 新加源文件一定要记得修改对应的 BUILD.json 文件
- 中断逻辑功能实现可以放在 “call xxx_func” 的 xxx_func 函数中,可以在调用前后做其它工作,比如用于任务切换的 0x20 号中断需要保存上下文和恢复上下文,就可以写成下面的形式
Int0x20_Entry: ; 保存上下文 ; ... call Int0x20Handle ; 中断逻辑功能 ; 恢复上下文 ; ... iret
- Int0x20Handle 函数就是我们上面实现的 TimerHandle 函数,只不过最后的中断返回要去掉,只保留逻辑功能代码
void Int0x20Handle(void) { static U32 count; SetCursorPos(0, 3); printk("TimerHandle: %d", count++); write_m_EOI(); // 手动结束主片中断 // asm volatile("leave;iret"); // 中断返回 }
- 中断入口不是你说在这就在这的,必须把入口地址放到中断描述符表 IDT 中才行,这就可以借助前面实现过的 IrqRegister 函数了
- 先把中断入口地址统一放到一起,方便遍历这些地址
IntVectorStart: dd Int0x00_Entry dd Int0x01_Entry dd Int0x02_Entry ... IntVectorLen: dd ($-IntVectorStart)/4
- 接下来利用 IrqRegister 函数在初始化的时候就把所有的中断入口注册到 IDT 中,不再重新写一个中断初始化函数了,就把 IrqInit 函数改了吧
E_RET IrqInit(void) { E_IRQ_NUM irqNum = IRQ0; E_RET ret = E_ERR; U32 idtSize = *((U32*)(IDT_SIZE_ADDR)); U32* isr = (U32*)&IntVectorStart; U32 isrSize = (U32)IntVectorLen; for(irqNum = IRQ0; irqNum < IRQ_TOTAL && irqNum < idtSize && irqNum < isrSize; irqNum++, isr++) { ret |= IrqRegister(irqNum, (F_ISR)(*isr)); } return ret; }
- 本次改动的代码:interrupt.asm、interrupt.h、irq.c、main.c