内核里的中断

简介: 内核里的中断

引言

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

中断相关的准备工作

  • 前面我们已经学习过了保护模式下的中断,并做了中断相关实验,现在我们就先做一下中断相关的准备工作吧
  • 首先在 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;
}
目录
相关文章
|
1天前
|
云安全 数据采集 人工智能
古茗联名引爆全网,阿里云三层防护助力对抗黑产
阿里云三层校验+风险识别,为古茗每一杯奶茶保驾护航!
古茗联名引爆全网,阿里云三层防护助力对抗黑产
|
5天前
|
Kubernetes 算法 Go
Kubeflow-Katib-架构学习指南
本指南带你深入 Kubeflow 核心组件 Katib,一个 Kubernetes 原生的自动化机器学习系统。从架构解析、代码结构到技能清单与学习路径,助你由浅入深掌握超参数调优与神经架构搜索,实现从使用到贡献的进阶之旅。
278 139
|
5天前
|
人工智能 中间件 API
AutoGen for .NET - 架构学习指南
《AutoGen for .NET 架构学习指南》系统解析微软多智能体框架,涵盖新旧双架构、核心设计、技术栈与实战路径,助你从入门到精通,构建分布式AI协同系统。
296 142
|
16天前
|
存储 关系型数据库 分布式数据库
PostgreSQL 18 发布,快来 PolarDB 尝鲜!
PostgreSQL 18 发布,PolarDB for PostgreSQL 全面兼容。新版本支持异步I/O、UUIDv7、虚拟生成列、逻辑复制增强及OAuth认证,显著提升性能与安全。PolarDB-PG 18 支持存算分离架构,融合海量弹性存储与极致计算性能,搭配丰富插件生态,为企业提供高效、稳定、灵活的云数据库解决方案,助力企业数字化转型如虎添翼!
|
11天前
|
缓存 并行计算 PyTorch
144_推理时延优化:Profiling与瓶颈分析 - 使用PyTorch Profiler诊断推理延迟,优化矩阵运算的独特瓶颈
在2025年的大模型时代,推理时延优化已经成为部署LLM服务的关键挑战之一。随着模型规模的不断扩大(从数亿参数到数千亿甚至万亿参数),即使在最先进的硬件上,推理延迟也常常成为用户体验和系统吞吐量的主要瓶颈。
358 147
|
5天前
|
人工智能 移动开发 自然语言处理
阿里云百炼产品月刊【2025年9月】
本月通义千问模型大升级,新增多模态、语音、视频生成等高性能模型,支持图文理解、端到端视频生成。官网改版上线全新体验中心,推出高代码应用与智能体多模态知识融合,RAG能力增强,助力企业高效部署AI应用。
298 1
|
11天前
|
机器学习/深度学习 存储 缓存
92_自我反思提示:输出迭代优化
在大型语言模型(LLM)应用日益普及的今天,如何持续提升模型输出质量成为了业界关注的核心问题。传统的提示工程方法往往依赖一次性输入输出,难以应对复杂任务中的多轮优化需求。2025年,自我反思提示技术(Self-Reflection Prompting)作为提示工程的前沿方向,正在改变我们与LLM交互的方式。这项技术通过模拟人类的自我反思认知过程,让模型能够对自身输出进行评估、反馈和优化,从而实现输出质量的持续提升。
438 136
|
15天前
|
存储 人工智能 搜索推荐
终身学习型智能体
当前人工智能前沿研究的一个重要方向:构建能够自主学习、调用工具、积累经验的小型智能体(Agent)。 我们可以称这种系统为“终身学习型智能体”或“自适应认知代理”。它的设计理念就是: 不靠庞大的内置知识取胜,而是依靠高效的推理能力 + 动态获取知识的能力 + 经验积累机制。
414 135
|
15天前
|
存储 人工智能 Java
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
本文讲解 Prompt 基本概念与 10 个优化技巧,结合学术分析 AI 应用的需求分析、设计方案,介绍 Spring AI 中 ChatClient 及 Advisors 的使用。
545 133
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话