内核里的中断

简介: 内核里的中断

引言

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

中断相关的准备工作

  • 前面我们已经学习过了保护模式下的中断,并做了中断相关实验,现在我们就先做一下中断相关的准备工作吧
  • 首先在 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;
}
目录
相关文章
|
5月前
|
6月前
|
Linux 虚拟化
minos 2.4 中断虚拟化——中断子系统
前面讲述了 minos 对 GICv2 的一些配置和管理,这一节再往上走一走,看看 minos 的中断子系统
66 3
|
7月前
|
安全 Linux
【Linux】详解用户态和内核态&&内核中信号被处理的时机&&sigaction信号自定义处理方法
【Linux】详解用户态和内核态&&内核中信号被处理的时机&&sigaction信号自定义处理方法
|
7月前
|
资源调度 调度 UED
CPU执行系统调用时发生中断,操作系统还能切回中断前的系统调用继续执行吗?
系统调用服务例程在执行过程中,通常不会被中断。系统调用服务例程的执行是一个原子操作,即在执行期间不会被中断。这是为了确保在系统调用服务例程执行期间对内核数据结构的一致性和完整性。
|
Linux 索引
RISC-V SiFive U54内核——CLINT中断控制器
RISC-V SiFive U54内核——CLINT中断控制器
|
传感器 调度
什么是中断系统?
一、什么是中断系统 中断系统是计算机系统中的一种机制,它允许外部设备和程序请求处理器的注意力,以便进行特定的操作。当一个中断请求被触发时,处理器会暂停当前正在执行的程序,转而执行与中断相关的程序或服务例程。中断系统可以提高计算机系统的效率和响应速度,因为它允许处理器在等待某些事件的同时执行其他任务。常见的中断包括硬件中断(例如键盘输入、鼠标移动、网络数据传输等)和软件中断(例如操作系统调度、系统调用等)。 二、中断系统的特点 中断系统具有以下特点: 1. 实时性:中断系统能够及时响应外部设备的请求,提高计算机系统的响应速度和效率。 2. 可靠性:中断系统能够保证中断请求的可靠性,确保外部设备的
293 0
|
存储 缓存 Linux
RISC-V SiFive U54内核——中断和异常详解
RISC-V SiFive U54内核——中断和异常详解
|
存储 Linux 程序员
1.1.5操作系统(中断和异常,系统调用)
中断 1.中断的作用 2.中断的分类 3.外中断的处理过程 系统调用 1.什么是系统调用,有何作用? 2.系统调用与库函数的区别 3.系统调用过程
1.1.5操作系统(中断和异常,系统调用)
|
Java 机器人 Linux
【2. 操作系统—中断、异常、系统调用】
🌗1. 启动 作用解析 Disk : 存放OS和Bootloader BIOS : 基于I/O处理系统(主要是计算机开机后,能够检查各种外设,然后加载软件执行) Bootloader : 加载OS,将OS从磁盘放入内存 注意:os最开始不是放到内存中的,而是放到disk(硬盘)中,由bios提供支持 开机流程 BIOS 开机后,寻找显卡和执行BIOS (此时, CS : IP = 0xF000 : 0xFFF0, CS/IP 两个寄存器) 将Bootloader从磁盘的引导扇区加载到0x7C00 (Bootloader一共占用512M字节的内存) 跳转到 CS : IP = 0
216 0
【2. 操作系统—中断、异常、系统调用】
详解中断系统
本文针对地详解了中断系统
276 0