操作系统中断处理机制详解

引言

中断处理是操作系统内核的核心功能之一,它使得操作系统能够响应硬件事件和异常情况。本文将详细分析一个32位操作系统中断处理机制的完整实现,包括GDT(全局描述符表)的作用、中断描述符表的构建、8259A中断控制器的配置,以及中断处理的完整流程。下文代码片段引自《操作系统真象还原》第七章第六节:编写中断处理程序

1. 系统启动与中断初始化

1.1 系统启动流程

操作系统从main.c开始执行,首先进行一系列初始化操作:

// main.c中的启动流程
int main() {
    put_str("I am kernel\n");
    init_all();  // 初始化所有模块
    while(1);
}

init_all()函数负责初始化系统的各个模块,其中中断系统的初始化是关键环节:

static void init_all() {
    idt_init();  // 初始化中断描述符表
}

1.2 中断初始化过程

idt_init()函数是中断系统的核心初始化函数,它完成了中断描述符表的构建和加载:

static void idt_init() {
    put_str("idt_init start\n");
    idt_desc_init();    // 初始化idt
    pic_init();         // 初始化8259A
    // 加载idt
    uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 32));
    asm volatile("lidt %0" : : "m" (idt_operand));
    put_str("idt_init done\n");
}

2. GDT表的核心作用

2.1 GDT表的结构与定义

GDT(全局描述符表)是x86保护模式下实现内存分段管理的核心数据结构。在这个操作系统中,GDT定义了4个关键段:

; GDT表定义(loader.S第18-29行)
gdt:
    .long 0x00000000     ;0个描述符:空描述符(必须存在)
    .long 0x00000000
    
    .long 0x0000ffff     ;1个描述符:代码段描述符
    .long DESC_CODE_HIGH4
    
    .long 0x0000ffff     ;2个描述符:数据段和栈段描述符
    .long DESC_DATA_HIGH4
    
    .long 0x80000007     ;3个描述符:视频段描述符(用于显存访问)
    .long DESC_VIDEO_HIGH4

2.2 段描述符的具体功能

代码段描述符(第1个)
基地址:0x00000000(整个内存空间)
段界限:0xfffff(4GB空间,配合4K粒度)
属性:可执行、32位、DPL=0特权级
作用:为内核代码提供执行环境
数据段描述符(第2个)
基地址:0x00000000(整个内存空间)
段界限:0xfffff(4GB空间)
属性:可读写、32位、DPL=0特权级
作用:为数据访问和栈操作提供空间
视频段描述符(第3个)
基地址:0x80000000(显存物理地址)
段界限:0x00007(32KB显存空间)
属性:可读写、32位、DPL=0特权级
作用:专门用于屏幕显示操作

2.3 选择子机制

; 选择子定义(loader.S第32-35行)
SELECTOR_CODE equ (0x01 << 3) + TI_GDT + RPL0  ; 0x08
SELECTOR_DATA equ (0x02 << 3) + TI_GDT + RPL0  ; 0x10
SELECTOR_VIDEO equ (0x03 << 3) + TI_GDT + RPL0 ; 0x18

每个选择子包含:
Index字段:指向GDT中的具体描述符
TI字段:指示使用GDT(0)还是LDT(1)
RPL字段:请求特权级(0为内核级)

2.4 保护模式转换的关键步骤

; 进入保护模式的核心代码(loader.S第95-100行)
mov eax, [gdt_ptr]      ; 加载GDT指针
lgdt [eax]              ; 将GDT地址加载到GDTR寄存器

mov eax, cr0
or eax, 0x00000001      ; 设置PE位,开启保护模式
mov cr0, eax

jmp dword SELECTOR_CODE:protect_mode_entry  ; 远跳转,加载CS选择子

2.5 GDT在中断处理中的作用

GDT在中断处理中发挥关键作用:

// interrupt.c第59行
p_gdesc->selector = SELECTOR_K_CODE;  // 中断门使用内核代码段选择子

当中断发生时,CPU通过中断门描述符中的选择子(SELECTOR_K_CODE)查找GDT,加载相应的代码段描述符到CS寄存器,确保中断处理程序在内核态正确执行。

3. 中断描述符表的构建

3.1 中断门描述符结构体

中断描述符表(IDT)是中断处理的核心数据结构。每个中断门描述符占用8字节,其结构定义如下:

// interrupt.c第5-14行
struct gate_desc {
    uint16_t offset_low;    // 处理程序地址低16位
    uint16_t selector;      // 段选择子
    uint8_t  dcount;        // 占用字节数
    uint8_t  attribute;     // 属性
    uint16_t offset_high;   // 处理程序地址高16位
};

3.2 中断门描述符的创建

make_idt_desc()函数负责创建中断门描述符:

// interrupt.c第16-24行
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
    p_gdesc->offset_low = (uint32_t)function & 0x0000FFFF;
    p_gdesc->selector = SELECTOR_K_CODE;
    p_gdesc->dcount = 0;
    p_gdesc->attribute = attr;
    p_gdesc->offset_high = ((uint32_t)function & 0xFFFF0000) >> 16;
}

3.3 IDT表的初始化

idt_desc_init()函数初始化整个IDT表:

// interrupt.c第26-36行
static void idt_desc_init() {
    int i;
    for (i = 0; i < IDT_DESC_CNT; i++) {
        make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
    }
    put_str("   idt_desc_init done\n");
}

IDT_DESC_CNT定义为0x21,支持0x00-0x20共33个中断向量号,覆盖了所有CPU异常和基本硬件中断。

3.4 SELECTOR_K_CODE选择子的作用

在make_idt_desc()函数中,p_gdesc->selector = SELECTOR_K_CODE;这一行代码至关重要。SELECTOR_K_CODE在global.h中定义为:

// global.h中的定义
#define SELECTOR_K_CODE  (1 << 3)  // 内核代码段选择子

这个选择子的结构分析:
Index字段:1(指向GDT第1个描述符,即内核代码段)
TI字段:0(使用GDT)
RPL字段:0(内核级特权)
使用SELECTOR_K_CODE的原因:
确保中断处理程序在内核态执行:中断处理程序需要最高特权级
提供正确的代码段上下文:确保中断处理程序能够访问内核资源
符合x86保护模式的设计要求:硬件通过选择子查找段描述符

4. 8259A中断控制器的配置

4.1 8259A初始化函数

pic_init()函数负责初始化8259A可编程中断控制器:

// interrupt.c第38-53行
static void pic_init() {
    // 初始化主片
    outb (PIC_M_CTRL, 0x11);   // ICW1: 边沿触发+级联
    outb (PIC_M_DATA, 0x20);   // ICW2: 起始中断向量号0x20
    outb (PIC_M_DATA, 0x04);   // ICW3: IR2接从片
    outb (PIC_M_DATA, 0x01);   // ICW4: 8086模式

    // 初始化从片
    outb (PIC_S_CTRL, 0x11);   // ICW1: 边沿触发+级联
    outb (PIC_S_DATA, 0x28);   // ICW2: 起始中断向量号0x28
    outb (PIC_S_DATA, 0x02);   // ICW3: 从片连接主片的IR2
    outb (PIC_S_DATA, 0x01);   // ICW4: 8086模式

    // 打开主片IR0(时钟中断),其他全部屏蔽
    outb (PIC_M_DATA, 0xfe);
    outb (PIC_S_DATA, 0xff);

    put_str("   pic_init done\n");
}

4.2 8259A配置详解

ICW1(初始化命令字1)
0x11:边沿触发模式、级联方式、需要ICW4
ICW2(初始化命令字2)
主片:0x20(中断向量号0x20-0x27)
从片:0x28(中断向量号0x28-0x2F)
ICW3(初始化命令字3)
主片:0x04(IR2连接从片)
从片:0x02(连接主片的IR2)
ICW4(初始化命令字4)
0x01:8086模式、正常EOI
中断屏蔽
主片:0xfe(仅开启IR0,即时钟中断)
从片:0xff(全部屏蔽)

5. 中断处理程序的生成

5.1 VECTOR宏机制

kernel.S使用宏批量生成中断处理程序:

%macro VECTOR 2
section .text
intr%1entry:		                        ; 中断处理程序入口
    %2                                      ; 处理错误码
    push intr_str
    call put_str
    add esp,4			                    ; 清理栈

    mov al,0x20                             ; EOI命令
    out 0xa0,al                             ; 向从片发送EOI
    out 0x20,al                             ; 向主片发送EOI

    add esp,4			                    ; 清理错误码或压入的0
    iret				                    ; 中断返回
section .data
    dd    intr%1entry	                    ; 存储中断入口地址
%endmacro

宏参数说明:
%1:中断向量号
%2:ZERO或ERROR_CODE,控制是否压入0

5.2 中断处理程序生成

通过调用VECTOR宏生成33个中断处理程序:

VECTOR 0x00,ZERO    ; 除法错误
VECTOR 0x01,ZERO    ; 调试异常
VECTOR 0x02,ZERO    ; NMI中断
// ... 其他异常
VECTOR 0x20,ZERO    ; 时钟中断

6. 中断响应流程

6.1 中断响应的完整步骤

中断发生:硬件设备(如8253定时器)产生中断信号
中断识别:8259A接收中断信号,向CPU发送INTR
中断响应:CPU响应中断,发送INTA信号
向量获取:8259A返回中断向量号(如时钟中断为0x20)
IDT查找:CPU根据向量号查找IDT表
门描述符检查:检查中断门描述符的权限和属性
上下文保存:自动保存EFLAGS、CS、EIP到栈中
特权级切换:如果需要,切换到内核特权级
段选择子加载:加载中断门中的选择子到CS寄存器
跳转执行:跳转到中断处理程序执行:调用put_str打印"interrupt occur!"消息,向8259A发送EOI(End Of Interrupt)信号,执行iret指令返回

6.2 选择子解析与GDT查找

当中断向量号为0x20时:
IDT索引:0x20 × 8 = 0x100(IDT表中的偏移)
读取中断门:获取offset和selector字段
选择子解析:SELECTOR_K_CODE = 0x08
Index = 0x08 >> 3 = 1
TI = (0x08 >> 2) & 0x1 = 0(使用GDT)
RPL = 0x08 & 0x3 = 0(内核级)
GDT查找:GDT基地址 + 1 × 8 = 内核代码段描述符地址
段描述符加载:将段基地址等信息加载到CS的隐藏部分

6.3 中断处理执行流程

; Intel风格的中断处理程序执行流程(基于kernel.S实际代码)
intr_entry:
    ; 1. 保存现场(程序执行)
    ; 根据中断类型压入错误码或0
    push DWORD [error_code_or_zero]    ; %2 宏展开的结果
    
    ; 2. 保存字符串地址(程序执行)
    push OFFSET intr_str               ; push intr_str
    
    ; 3. 执行中断处理逻辑(程序执行)
    call put_str                        ; 调用打印函数
    
    ; 4. 清理栈中的字符串地址(程序执行)
    add esp, 4                          ; add esp,4
    
    ; 5. 发送EOI信号(程序执行)
    mov al, 20h                         ; mov al,20h
    out 0A0h, al                        ; out 0xa0,al (从片EOI)
    out 020h, al                        ; out 0x20,al (主片EOI)
    
    ; 6. 清理栈中的错误码或0(程序执行)
    add esp, 4                          ; add esp,4
    
    ; 7. 中断返回(程序调用,硬件自动执行)
    iret                                ; iret

在中断处理程序的执行流程中,有些操作是由CPU硬件自动执行的,有些是由程序代码显式执行的。详细分析一下:
CPU硬件自动执行的操作

  1. 中断响应阶段(硬件自动)
    当中断发生时,CPU硬件会自动执行以下操作:
; 硬件自动执行,无需程序代码
1. CPU检测到中断信号
2. 如果IF=1,则响应中断
3. 自动保存当前现场到栈中:
   - push EFLAGS        ; 保存标志寄存器
   - push CS            ; 保存代码段选择子
   - push EIP           ; 保存指令指针
4. 如果是特权级变化,还会:
   - push SS            ; 保存栈段选择子
   - push ESP           ; 保存栈指针
5. 根据中断向量号查找IDT表
6. 自动加载新的CS和EIP(从中断门描述符中获取)
7. 跳转到中断处理程序执行
  1. 段选择子加载(硬件自动)
// 硬件自动执行,程序只需设置选择子
p_gdesc->selector = SELECTOR_K_CODE;  // 程序设置
// CPU硬件会自动:
// 1. 从选择子中提取Index=1
// 2. 计算GDT地址:GDT基地址 + 1*8
// 3. 读取段描述符
// 4. 加载段基地址到CS的隐藏部分
// 5. 加载段界限和属性到CS的隐藏部分
  1. iret指令执行(硬件自动)
; 硬件自动执行iret的完整流程
iret
; CPU硬件会自动:
; 1. 从栈中弹出EIP(恢复指令指针)
; 2. 从栈中弹出CS(恢复代码段选择子)
; 3. 从栈中弹出EFLAGS(恢复标志寄存器)
; 4. 如果是特权级变化,还会:
;    - 从栈中弹出ESP(恢复栈指针)
;    - 从栈中弹出SS(恢复栈段选择子)
; 5. 恢复到中断前的执行状态

程序代码显式执行的操作

  1. 保存现场(程序执行)
; 程序显式执行(根据kernel.S实际代码)
intr%1entry:		                        ; 中断处理程序入口
    %2                                      ; 程序执行:根据中断类型压入错误码或0
    push intr_str                          ; 程序执行:保存字符串地址到栈中
    call put_str                           ; 程序执行:调用打印函数
    add esp,4                              ; 程序执行:清理栈中的字符串地址参数
  1. 中断处理逻辑(程序执行)
; 程序显式执行(根据kernel.S实际代码)
    ; 在call put_str中执行打印"interrupt occur!"
    ; 这里没有段寄存器切换操作,直接使用当前段环境
  1. EOI信号发送(程序执行)
; 程序显式执行(根据kernel.S实际代码)
    mov al,20h                             ; 程序设置:EOI命令码
    out 0a0h,al                            ; 程序执行:向从片8259A发送EOI
    out 020h,al                            ; 程序执行:向主片8259A发送EOI
  1. 恢复现场(程序执行)
; 程序显式执行(根据kernel.S实际代码)
    add esp,4                              ; 程序执行:清理栈中的错误码或0
    iret                                   ; 程序调用:触发硬件自动恢复

完整的执行时序对比
硬件自动执行时序:

中断发生 → CPU检测中断 → 保存EFLAGS/CS/EIP → 查找IDT → 加载CS/EIP → 跳转处理程序
                                                                    ↓
程序执行中断处理 ← iret恢复现场 ← 程序发送EOI ← 程序处理逻辑 ← 程序保存现场

程序执行时序:

进入intr%1entry → %2(压入错误码/0) → push intr_str → call put_str → add esp,4 → mov al,20h → out 0a0h,al → out 020h,al → add esp,4 → iret

在这里插入图片描述
设计优势
这种硬件与软件分工的设计具有以下优势:
硬件保证原子性:关键的状态保存和恢复由硬件自动完成,确保原子性
软件灵活性:具体的中断处理逻辑由软件实现,具有灵活性
性能优化:硬件自动执行的关键路径性能最优
安全性:硬件自动进行的权限检查和状态切换更加安全
理解硬件与软件的分工对于深入理解中断机制至关重要,这也是x86架构设计的精髓所在。
代码特点
简化的现场管理:只管理必要的栈数据,不保存通用寄存器
无段寄存器切换:直接使用进入中断时的段环境
最小化开销:只保存和恢复中断处理必需的数据
硬件优化:充分利用CPU硬件的自动保存/恢复机制

7. "interrupt occur!"重复输出现象分析

7.1 现象描述

在系统运行过程中,屏幕会持续输出"interrupt occur!"字符串,这是时钟中断持续触发的正常现象。

7.2 原因分析

时钟中断配置:8259A仅开启了主片的IR0(时钟中断)
中断处理程序:所有中断处理程序都调用put_str输出信息
周期性触发:8253定时器以固定频率产生时钟中断
循环执行:内核主循环中持续响应中断

7.3 执行流程分析

内核启动
    ↓
初始化中断系统
    ↓
执行sti指令开启中断
    ↓
进入主循环while(1)
    ↓
时钟中断触发(约每秒多次)
    ↓
执行intr20_entry处理程序
    ↓
调用put_str输出"interrupt occur!"
    ↓
发送EOI信号
    ↓
iret返回主循环
    ↓
等待下一次时钟中断
    ↓
重复上述过程

8. 中断门属性与权限控制

8.1 中断门属性定义

// global.h中的属性定义
#define IDT_DESC_P         1
#define IDT_DESC_DPL0      0
#define IDT_DESC_32_TYPE   0xE

#define IDT_DESC_ATTR_DPL0 (IDT_DESC_P | (IDT_DESC_DPL0 << 5) | IDT_DESC_32_TYPE)

8.2 权限检查机制

x86 CPU在处理中断时会进行严格的权限检查:
CPL(当前特权级):当前执行代码的特权级
RPL(请求特权级):选择子中的请求特权级
DPL(描述符特权级):段描述符或门描述符的特权级
中断门的权限规则:
CPL ≤ 门描述符的DPL:允许通过中断门
CPL ≤ 目标代码段的DPL:允许跳转到目标代码段
内核中断门的特权级:
门描述符DPL:0(只有内核可以访问)
目标代码段DPL:0(内核代码段)
SELECTOR_K_CODE的RPL:0(请求内核级特权)
这种设计确保:
用户程序不能直接访问中断门
中断处理程序在内核态执行
系统调用需要通过门机制进入内核

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lcreek

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值