中断与异常处理

加载出内核后,后续的代码基本就是对内核的设计,首先就是对中断和异常处理的设计。

创建GDT表及其表项

x86架构(IA32 模式 )支持两种存储模式,分段式存储和分页式存储。

  • 分段:先通过分段机制,把逻辑地址(由段选择器和偏移量组成 )转换为线性地址。利用 全局描述符表(GDT)、局部描述符表(LDT )等,依据段选择器找到对应的段描述符,算出线性地址 。这是 x86 内存管理的基础,用于划分内存段、设置访问权限等。

    • 分段机制:
      1. 将线性地址空间转变为多个段(segments)。
      2. 每个段带有相关的保护机制
      3. 有多种类型的段:数据、代码、门、tss
      4. 使用的地址为逻辑地址,即段选择子(指向GDT或LDT中的段描述符)+偏移
  • 分页:可选启用,若启用,线性地址会经分页机制转换为物理地址。通过页目录(Page Directory )、页表(Page Table )等结构,把线性地址按页(如 4KB 页 )拆分,映射到物理内存页,可实现虚拟内存、内存共享、内存保护细化等,还能隐藏物理内存细节,让程序使用连续虚拟地址,实际对应物理内存可离散。

    • 分页机制
      1. 将线性地址转换为物理地址
      2. 通过虚拟内存机制,用磁盘空间扩展物理内存的容量
      3. 按需加载等功能

这里主要关注分段式存储中的GDT表。

GDT是x86架构下(尤其是保护模式)用于内存管理和保护的数据结构,本质是一个数组,每个元素叫做段描述符,占八个字节,段描述符控制者个隔断的起始地址、大小和访问属性(当前指令具体访问哪个段则由段寄存器决定),包含:

段描述符内容

Seg. Desc.(通用段描述符)

  • 作用:描述最基础的内存段,比如 代码段(Code Segment)、数据段(Data Segment)、栈段(Stack Segment)
  • 内容:存了段的基地址(Base)、段界限(Limit)、访问权限(如只读 / 可写、特权级) 等。程序里的 CS(代码段寄存器)、DS(数据段寄存器)、SS(栈段寄存器),最终都会通过这类描述符找到实际内存位置。

TSS Desc.(任务状态段描述符)

  • TSS :TSS(Task State Segment)是一块内存区域,存着任务的上下文(比如寄存器值、栈指针、特权级),切换任务时,CPU 会自动从 TSS 恢复 / 保存现场。
  • TSS Desc. 作用:把 TSS 当普通内存段 “描述” 起来,让 CPU 能找到它的基地址、大小、权限。图里多个 TSS Desc.,对应多任务场景下不同任务的 TSS。

LDT Desc.(局部描述符表描述符)

  • **LDT :LDT(Local Descriptor Table)是 “局部版 GDT”,一个任务(进程)可以有自己的 LDT,存专属的段描述符(比如用户态程序的私有代码段、数据段 )。
  • LDT Desc. 作用:把 LDT 本身当一个特殊段,用 LDT Desc. 描述它的位置、大小、权限,让 CPU 能找到并访问这个任务的 LDT。

image0344

创建一个GDT表结构体:

1
2
3
4
5
6
7
8
9
typedef struct _segment_desc_t  /*定义GDT表结构体*/
{
uint16_t limit15_0; /*段界限*/
uint16_t base15_0; /*基地址*/
uint8_t base23_16;
uint16_t attr; /*属性*/
uint8_t base31_24;

}segment_desc_t;

初始化GDT表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*初始化GDT表中的固定表项*/
void segment_desc_set(int selector, uint32_t base, uint32_t limit, uint16_t attr) {
segment_desc_t * desc = gdt_table + selector / sizeof(segment_desc_set); /*左移三位,也就是除以8*/
desc->limit15_0 = limit & 0xFFFF;
desc->attr = attr | (((limit>>16) & 0xF) << 8);
desc->base15_0 = base & 0xFFFF;
desc->base23_16 = (base >> 16) & 0xFF;
desc->base15_0 = (base >> 24) & 0xFF;
}

/*对GDT表进行初始化*/
void init_gdt(void) {
for(int i =0; i < GDT_TABLE_SIZE; i++) { /*暂时清零*/
segment_desc_set(i * sizeof(segment_desc_set), 0, 0, 0);
}
}

分段模型

多段模型(Multi - Segment Model)

把内存拆成多个独立的段,每个段都有自己的基地址、长度、访问权限,不同类型的段分工明确:CS 指向代码段(存程序指令 )、SS 指向栈段(存函数调用的临时数据 )、DS/ES 等指向数据段(存变量 ) 。

3

平坦模型

他是多段模型的简化版,把整个内存(或线性地址空间)当成一整块连续的区域使用。

只使用了两个段:代码段和数据段。段基是地址均为0,limit大小为4GB(32位系统),这样逻辑地址的偏移量直接对应线性地址,段选择子也就失去实际意义。

逻辑地址转换到线性地址的过程

  1. 从段寄存器获取段选择子
  2. 根据选择子在GDT表中获取基地址
  3. 线性地址=基地址+偏移量,如果无分页机制,则直接为物理地址

5CC

重新加载GDT表

在loader_16中有设置过一个GDT表,没有解释每个值的意义,现在了解了GDT表项的含义,在kernel中重新加载GDT表,用到的模式就是平坦模式,只需要初始化两个段,数据段和代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void init_gdt(void) {
// 全部清空
for (int i = 0; i < GDT_TABLE_SIZE; i++) {
segment_desc_set(i << 3, 0, 0, 0);
}

//数据段
segment_desc_set(KERNEL_SELECTOR_DS, 0x00000000, 0xFFFFFFFF,
SEG_P_PRESENT | SEG_DPL0 | SEG_S_NORMAL | SEG_TYPE_DATA
| SEG_TYPE_RW | SEG_D | SEG_G);

// 只能用非一致代码段,以便通过调用门更改当前任务的CPL执行关键的资源访问操作
segment_desc_set(KERNEL_SELECTOR_CS, 0x00000000, 0xFFFFFFFF,
SEG_P_PRESENT | SEG_DPL0 | SEG_S_NORMAL | SEG_TYPE_CODE
| SEG_TYPE_RW | SEG_D | SEG_G);


// 加载gdt
lgdt((uint32_t)gdt_table, sizeof(gdt_table));

// 给出一些宏定义的意义
#define SEG_G (1 << 15) /* 粒度标志(Granularity):0=Limit单位为字节,1=Limit单位为4KB页 */
#define SEG_D (1 << 14) /* 默认操作数大小(Default Operation Size):
* 代码段:0=16位,1=32位(64位系统中为48位)
* 栈段:0=16位栈指针,1=32位栈指针(ESP/EBP) */
#define SEG_P_PRESENT (1 << 7) /* 段存在标志(Present):1=段在内存中,0=段不在内存(访问时触发异常) */

#define SEG_DPL0 (0 << 5) /* 描述符特权级(DPL):0=最高特权级(内核模式) */
#define SEG_DPL3 (3 << 5) /* 描述符特权级(DPL):3=最低特权级(用户模式) */

#define SEG_S_SYSTEM (0 << 4) /* 描述符类型标志(S):0=系统段(如TSS、LDT) */
#define SEG_S_NORMAL (1 << 4) /* 描述符类型标志(S):1=普通段(代码段/数据段) */

#define SEG_TYPE_CODE (1 << 3) /* 段类型标志(Type字段):C=1表示代码段(需配合S=1使用) */
#define SEG_TYPE_DATA (0 << 3) /* 段类型标志(Type字段):C=0表示数据段(需配合S=1使用) */

#define SEG_TYPE_RW (1 << 1) /* 读写权限标志(Type字段):
* 数据段:R/W=1表示可写(W标志)
* 代码段:R/W=1表示可读(R标志) */

后面可以在GDT表中增加一些特殊的描述符:门描述符(用于控制程序流程的跳转),包括中断门、陷阱门、任务门。

异常与中断

在程序运行的过程中,有可能会发生各种异常事件,CPU需要跳转到相应的程序对这些事件进行处理。门描述符就是处理异常和中断的中间层。

异常(Exception)

  • 定义:由 CPU 内部事件触发的同步事件,与当前执行的指令直接相关。
  • 触发原因:
    • 错误(Fault):如缺页异常(Page Fault)、除零错误。
    • 陷阱(Trap):如系统调用(通过int指令主动触发)。
    • 终止(Abort):如硬件故障、无法恢复的错误。
  • 特点:
    • 同步性:与指令执行严格同步,==发生在指令执行期间==。
    • 可预测性:通常由程序逻辑或硬件状态引发(如访问非法内存)。

中断(Interrupt)

  • 定义:由 CPU 外部设备(如键盘、网卡)或内部定时器触发的异步事件。
  • 触发原因:
    • 硬件中断:外设(如鼠标、硬盘)通过中断控制器(如 8259A、IOAPIC)向 CPU 发送信号。
    • 软件中断:通过特定指令(如 x86 的INT n)触发,常用于系统调用。
  • 特点:
    • 异步性:==与当前指令执行无关==,可能随时发生,由外部事件引起。
    • 突发性:由外部设备状态变化引发(如按键按下、数据到达)。

门描述符

门描述符是异常 / 中断处理的中间层,存储在IDT(中断描述符表)中负责:

  1. 地址映射:将中断向量号映射到具体的处理程序地址。
  2. 权限控制:限制哪些特权级的代码可以触发该异常 / 中断。
  3. 上下文切换:自动处理特权级切换(如从用户态到内核态)和堆栈切换。

当 CPU 接收到异常或中断信号时,执行以下步骤:

  1. 获取向量号
    • 异常:由 CPU 自动生成(如除零错误对应向量号 0)。
    • 中断:通过中断控制器(如 IOAPIC)获取向量号(如键盘中断对应 0x21)。
  2. 查找门描述符
    • 向量号作为索引,从 IDT 中找到对应的门描述符(例如,向量号 0 对应 IDT [0])。
  3. 验证权限
    • 比较当前特权级(CPL)与门描述符的 DPL(描述符特权级):
      • CPL ≤ DPL,允许访问;否则触发一般保护故障(#GP)。
  4. 执行处理程序
    • 根据门描述符中的段选择子偏移量,跳转到处理程序。
    • 对于中断门和陷阱门,自动保存当前上下文(如 EFLAGS、CS、EIP)。

中断门描述符与IDT表

IDT表配置类似于GDT表,由一个寄存器指向:IDTR寄存器,内部存放门描述符。

门描述符只存放了基地址和界限信息。

6

本项目主要实现中断门描述符的功能,其中interrupt gate中segment selector指定了代码段的选择子,offset指定了偏移。即二者结合,指定了各表项对应的异常/中断的处理程序的首地址。

7

地址生成如下:

  1. 根据向量号取IDT中的对应表项,
  2. 从IDT表项取选择子
  3. 用选择子从GDT表中查找段的首地址,
  4. 将段首地址+IDT表项中的偏移量,生成处理程序的首地址
  5. 跳转至首地址运行。

首先与GDT表类似,将IDT表初始化为全零,后续会将具体的异常中断处理程序与IDT表项进行关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 初始化为全0
void irq_init (void) {
for (int i = 0;i < IDE_TABLE_NR; i++) {
gate_desc_set(idt_table + i,0, 0, 0);
}

lidt((uint32_t)idt_table, sizeof(idt_table)); //重新加载
}

static inline void lidt(uint32_t start, uint32_t size) { //与lgdt十分相似
struct {
uint16_t limit;
uint16_t start15_0;
uint16_t start31_16;
} idt;

idt.start31_16 = start >> 16;
idt.start15_0 = start & 0xFFFF;
idt.limit = size - 1;

__asm__ __volatile__("lidt %[g]"::[g]"m"(idt));
}

初始化并加载成功后,IDT表的首地址与qemu显示的寄存器信息一致:

idt加载后,首地址能对上

下面就是为所有异常配置缺省的处理程序,采用平坦模型,所有段的起始地址都为0,通过offset来找到对应处理程序的地址:

8

发生除零异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void init_main(void) {
int a = 3 / 0; // 除零触发异常
for (;;) {}
}

static void do_default_handler (const char * message) {
for (;;) {} //死机处理
}

void do_handler_unknown (void) {
do_default_handler("Unknown exception.");
};

/**
* @brief 中断和异常初始化
*/
void irq_init(void) {
for (uint32_t i = 0; i < IDT_TABLE_NR; i++) {
gate_desc_set(idt_table + i, KERNEL_SELECTOR_CS, (uint32_t) exception_handler_unknown, /* 这个函数使用汇编语言编写,因为部分关键操作必须使用特定汇编指令(如cli关中断、sti开中断、iret中断返回),这些指令无对应的 C 语言关键字,需通过汇编内嵌或独立汇编文件实现。 */
GATE_P_PRESENT | GATE_DPL0 | GATE_TYPE_IDT);
}
lidt((uint32_t)idt_table, sizeof(idt_table));
}

exception_handler_unknown:
// 保存所有寄存器
pusha
push %ds
push %es
push %fs
push %gs

// 调用中断处理函数
call do_handler_unknown

// 恢复保存的寄存器
pop %gs
pop %fs
pop %es
pop %ds
popa
iret

解析异常栈信息

这里主要实现具体在哪个位置触发了异常,异常发生时,会有一部分信息被自动压入栈中,包括EFLAGS、CS、EIP和错误码

9

并且在发生异常后我们通过代码也主动保存了一部分信息,最终我们保存的信息如下:

10

但是可以看出来如果将这些信息全部作为参数传入函数中处理,参数会变得非常长,因此可以看作为结构体,将该结构体的指针也就是ESP寄存器,将结构体指针作为参数传入会更加简洁。

利用宏重用异常处理代码

x86架构保护模式下的异常与中断表定义了非常多的宏,如果针对这些异常分别添加处理函数,就需要复制多次类似的汇编代码编写的处理程序,而这些处理程序的区别只在调用的对应c代码,因此利用gcc工具链中汇编代码中的宏功能,实现类似C语言中的define功能,从而达到重用代码的目的。

如果要定义宏,需要使用.macro和.endm伪指令。其基本示例如下:

.macro 宏的名称 参数0, 参数1….
汇编代码
.endm

其中参数是可选的,也可给参数一个缺省值,例如:

  • .macro comm — 定义一个comm宏,不需要参数
  • .macro plus1 p, p1 — 定义一个plus1宏,带参数p和p1
  • .macro plus1 p p1 — 定义一个plus1宏,带 参数p和p1(用空格分隔)
  • .macro reserve_str p1=0 p2 — 定义一个reserve宏,带p1和p2参数,其中p1的缺省值为0

在宏的内部,==可以通过\参数名的方式去引用参数== ,例如:

   .macro  sum from=0, to=5
    .long   \from
    .if     \to-\from
    sum     "(\from+1)",\to
    .endif
    .endm

具体实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
.macro exception_handler name num with_error_code
.extern do_handler_\name
.global exception_handler_\name
exception_handler_\name:
// 如果没有错误码,压入一个缺省值
// 这样堆栈就和有错误码的情形一样了
.if \with_error_code == 0
push $0
.endif

// 压入异常号
push $\num

// 保存所有寄存器
pushal
push %ds
push %es
push %fs
push %gs

// 调用中断处理函数
push %esp
call do_handler_\name
add $(1*4), %esp // 丢掉esp

// 恢复保存的寄存器
pop %gs
pop %fs
pop %es
pop %ds
popal

// 跳过压入的异常号和错误码
add $(2*4), %esp
iret
.endm

//调用
exception_handler unknown, -1, 0
exception_handler divider, 0, 0
exception_handler Debug, 1, 0
exception_handler NMI, 2, 0
exception_handler breakpoint, 3, 0
exception_handler overflow, 4, 0
exception_handler bound_range, 5, 0
exception_handler invalid_opcode, 6, 0
exception_handler device_unavailable, 7, 0
exception_handler double_fault, 8, 1
exception_handler invalid_tss, 10, 1
exception_handler segment_not_present, 11, 1
exception_handler stack_segment_fault, 12, 1
exception_handler general_protection, 13, 1
exception_handler page_fault, 14, 1
exception_handler fpu_error, 16, 0
exception_handler alignment_check, 17, 1
exception_handler machine_check, 18, 0
exception_handler smd_exception, 19, 0
exception_handler virtual_exception, 20, 0

// 硬件中断
exception_handler timer, 0x20, 0

发生异常后,会根据错误码等跳转到相应的handler函数,并且使CPU暂停执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
static void do_default_handler (exception_frame_t * frame, const char * message) {
for (;;) {hlt();} //暂停,使用内联函数实现
}

void do_handler_unknown (exception_frame_t * frame) {
do_default_handler(frame, "Unknown exception.");
}

void do_handler_divider(exception_frame_t * frame) {
do_default_handler(frame, "Device Error.");
}

void do_handler_Debug(exception_frame_t * frame) {
do_default_handler(frame, "Debug Exception");
}

void do_handler_NMI(exception_frame_t * frame) {
do_default_handler(frame, "NMI Interrupt.");
}

void do_handler_breakpoint(exception_frame_t * frame) {
do_default_handler(frame, "Breakpoint.");
}

void do_handler_overflow(exception_frame_t * frame) {
do_default_handler(frame, "Overflow.");
}

void do_handler_bound_range(exception_frame_t * frame) {
do_default_handler(frame, "BOUND Range Exceeded.");
}

void do_handler_invalid_opcode(exception_frame_t * frame) {
do_default_handler(frame, "Invalid Opcode.");
}

void do_handler_device_unavailable(exception_frame_t * frame) {
do_default_handler(frame, "Device Not Available.");
}

void do_handler_double_fault(exception_frame_t * frame) {
do_default_handler(frame, "Double Fault.");
}

void do_handler_invalid_tss(exception_frame_t * frame) {
do_default_handler(frame, "Invalid TSS");
}

void do_handler_segment_not_present(exception_frame_t * frame) {
do_default_handler(frame, "Segment Not Present.");
}

void do_handler_stack_segment_fault(exception_frame_t * frame) {
do_default_handler(frame, "Stack-Segment Fault.");
}

void do_handler_general_protection(exception_frame_t * frame) {
do_default_handler(frame, "General Protection.");
}

void do_handler_page_fault(exception_frame_t * frame) {
do_default_handler(frame, "Page Fault.");
}

void do_handler_fpu_error(exception_frame_t * frame) {
do_default_handler(frame, "X87 FPU Floating Point Error.");
}

void do_handler_alignment_check(exception_frame_t * frame) {
do_default_handler(frame, "Alignment Check.");
}

void do_handler_machine_check(exception_frame_t * frame) {
do_default_handler(frame, "Machine Check.");
}

void do_handler_smd_exception(exception_frame_t * frame) {
do_default_handler(frame, "SIMD Floating Point Exception.");
}

void do_handler_virtual_exception(exception_frame_t * frame) {
do_default_handler(frame, "Virtualization Exception.");
}

初始化中断控制器

早期x86使用8259芯片来管理终端,使用两块8259级联来支持15种中断,随着多核处理器的发展,8259被APIC所取代

这里具体的硬件实现不需要关注,只需要关注相应的寄存器的配置。在对寄存器初始化时,需要分配对上述中的master和slave进行初始化。其中master对应的端口起始地址为0x20,slave对应的端口起始地址为0xA0。芯片的手册中给出了初始化流程和相应的寄存器格式说明。

8259的工作模式较为复杂,这里只做了非常简单的配置,不考虑中断嵌套、优先级等问题。具体配置如下:

  • 主片:边缘触发,级联、起始中断序号为0x20,IRQ2上有从片,普通全嵌套、非缓冲、非自动结束、8086模式
  • 从片:边缘触发,级联、起始中断序号为0x28,连接到主片的IRQ2上,普通全嵌套、非缓冲、非自动结束、8086模式

后续要使用定时器,因此需要做中断的开关函数。

启动定时器并开中断

中断的打开与关闭

中断的打开和关闭受制于两种配置,一个是8259A内部的IMR寄存器,还有一个EFLAGS的IF标志位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void irq_disable_global(void) { //关闭全局中断位
cli();
}
void irq_enable_global(void) { //开启全局中断位
sti();
}

void irq_enable(int irq_num) {
if(irq_num < IRQ_PIC_START) {
return;
}

// 将IRQ编号转换为相对于PIC芯片的内部偏移量
// IRQ_PIC_START通常为0x20(32),是8259 PIC在x86中断向量表中的起始位置
irq_num -= IRQ_PIC_START;

// 判断IRQ属于第一块还是第二块8259 PIC
if (irq_num < 8) { // 第一块8259 PIC(主控制器),负责IRQ0-7
// 1. 读取当前主PIC的中断屏蔽寄存器(IMR)
// 2. 清除对应IRQ位(使用位运算 ~(1 << irq_num))以启用该IRQ
// 3. 将修改后的屏蔽字写回主PIC的IMR端口(0x21)
uint8_t mask = inb(PIC0_IMR) & ~(1 << irq_num);
outb(PIC0_IMR, mask);
} else { // 第二块8259 PIC(从控制器),负责IRQ8-15
// 1. 计算IRQ在从PIC中的相对编号(0-7)
// 2. 读取当前从PIC的中断屏蔽寄存器(IMR)
// 3. 清除对应IRQ位以启用该IRQ
// 4. 将修改后的屏蔽字写回从PIC的IMR端口(0xA1)
irq_num -= 8; // 转换为从PIC的内部偏移量
uint8_t mask = inb(PIC1_IMR) & ~(1 << irq_num);
outb(PIC1_IMR, mask);
}

}

void irq_disable(int irq_num) {
if(irq_num < IRQ_PIC_START) {
return;
}

irq_num -= IRQ_PIC_START;


if (irq_num < 8) {
uint8_t mask = inb(PIC0_IMR) | ~(1 << irq_num);
outb(PIC0_IMR, mask);
} else {
uint8_t mask = inb(PIC1_IMR) | ~(1 << irq_num);
outb(PIC1_IMR, mask);
}
}

定时器

定时器负责提供精准的时间基准和周期性中断,用于维护系统时间、计算进程执行时间片、实现睡眠等延时函数。x86系统中使用的定时器芯片是8253。

8253是一颗带有3个内部计数器的定时器的芯片,用于为计算机提供相关的定时和计数功能。其中,我们主要关心定时器/计数器0,因其余两个一般用于其它用途。
定时器0可单独计数,其输入的时钟频率为1.193182 MHz。在每个时钟节拍的作用下,进行递减计数。当减至0时,通过8259的IRQ0向CPU发出中断请求,CPU将进入中断服务程序运行。

13

为实现对8253进行配置,可通过如下端口进行设置。

端口地址 名称
0x40 定时器0数据端口
0x41 定时器1数据端口
0x42 定时器2数据端口
0x43 模式和命令端口

具体实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 计算PIT的重装载值:将毫秒转换为PIT计数值
// PIT_OSC_FREQ = 1193180Hz (PIT的基准时钟频率)
// OS_TICK_MS = 期望的中断周期(毫秒),例如10ms = 100Hz
// 公式:reload_count = (1193180 * 10ms) / 1000 = 11932 (约等于10ms)
uint32_t reload_count = (PIT_OSC_FREQ * OS_TICK_MS) / 1000;

/* 配置PIT通道0为模式3(方波发生器) */
// PIT_CHANNEL = 0x00 (选择通道0)
// PIT_LOAD_LOHI = 0x30 (先写低字节,再写高字节)
// PIT_MODE3 = 0x06 (模式3:方波输出,用于周期性中断)
outb(PIT_COMMAND_MODE_PORT, PIT_CHANNEL | PIT_LOAD_LOHI | PIT_MODE3);

// 写入16位计数值的低8位
outb(PIT_CHANNEL_DATA_PORT, reload_count & 0xFF);
// 写入16位计数值的高8位
outb(PIT_CHANNEL_DATA_PORT, (reload_count >> 8) & 0xFF);

// 注册定时器中断处理函数
// IRQ0_TIMER = 0x20 (PIT通道0对应的中断向量号)
// exception_handler_time = 自定义的中断处理函数
irq_install(IRQ0_TIMER, (irq_handler_t)exception_handler_time);

// 启用PIT通道0的中断
// 通过清除8259A PIC的IMR(中断屏蔽寄存器)对应位来启用IRQ0
irq_enable(IRQ0_TIMER);

定时器的中断会在以后一些延时函数、进程调度等地方用到。

以上就是中断与异常处理部分的记录。