2 Star 5 Fork 4

稀风 / KOS

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
内核里的中断.md 13.75 KB
一键复制 编辑 原始数据 按行查看 历史
稀风 提交于 2023-01-28 10:58 . 代码优化

引言

  • 在上一章节中,我们实现了单个任务,然而这个任务是个死循环,那么怎么才能跳出这个任务从而执行别的任务呢?
  • 答案就是利用时钟中断,由于我们在内核中并没有实现中断相关功能,所以本章节我们先学习一下中断的使用,下一个章节我们再在中断的基础上实现多任务吧

中断相关的准备工作

  • 前面我们已经学习过了保护模式下的中断,并做了中断相关实验,现在我们就先做一下中断相关的准备工作吧
  • 首先在 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.cirq.hshare.hmain.c

8259A 驱动

  • 接下来实现的完整代码见:8259A.asm8259A.asmmain.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.asminterrupt.hirq.cmain.c
1
https://gitee.com/thin-wind/KOS.git
git@gitee.com:thin-wind/KOS.git
thin-wind
KOS
KOS
main

搜索帮助