10.从开始写LINUX内核——时钟中断
Linux 0.12 内核时钟中断实现:从初始化到中断响应
时钟中断是操作系统中最基础且最重要的中断之一,它为系统提供时间基准,支持进程调度、定时器等核心功能。本文将基于 Linux 0.12 内核的 setup 程序框架,详细介绍时钟中断的完整实现,包括 8253 定时器初始化、中断向量绑定及中断处理程序编写,确保代码可直接用于实验验证。
一、时钟中断实现基础
1. 硬件基础
- 8253 可编程定时器:产生周期性时钟信号,默认频率 18.2Hz(约 55ms 一次中断)
- 8259A 中断控制器:时钟中断默认映射到 IRQ0,对应中断向量 0x20
- 中断描述符表(IDT):需在向量 0x20 处注册时钟中断处理程序
2. 开发环境与工具链
沿用 Linux 0.12 开发环境,核心工具包括:
as 2.34
:汇编器(支持 AT&T 语法)ld 2.34
:链接器(生成二进制镜像)qemu-system-i386
:模拟器(验证中断响应)
二、完整代码实现
以下是基于 setup 程序扩展的时钟中断实现代码,包含定时器初始化、中断处理程序及 IDT 配置:
asm
/* setup.s —— 扩展时钟中断支持(2048字节) */
.code16
.text
.global _start_setup/* 段地址定义 */
INITSEG = 0x9000 /* 硬件信息存储段 */
SETUPSEG = 0x9020 /* setup程序段地址 */
IDT_BASE = 0x0000 /* 中断描述符表基地址 */
IDT_LIMIT = 0x7FFF /* IDT长度(8192字节) */_start_setup:/* 初始化段寄存器 */movw %cs, %axmovw %ax, %dsmovw %ax, %es/* 收集硬件信息(光标、内存等) */movb $0x03, %alxor %bh, %bhint $0x10movw %dx, (0) /* 存储光标位置到INITSEG:0000 */movb $0x88, %ahint $0x15movw %ax, (2) /* 存储内存大小到INITSEG:0002 *//* 显示启动信息 */movw $setup_msg, %axmovw %ax, %bpmovw $0x1301, %ax /* BIOS 10h/13h:显示字符串 */movw $0x000C, %bx /* 亮红色文字 */movw $16, %cx /* 字符串长度 */movb $3, %dh /* 行3 */movb $0, %dl /* 列0 */int $0x10/* 初始化8259A中断控制器(允许IRQ0时钟中断) */call init_8259A/* 初始化8253定时器(产生时钟中断) */call init_8253/* 准备进入保护模式 */cli /* 关闭中断 */movw $0x0000, %axcld /* 清除方向标志 */
do_move:movw %ax, %esaddw $0x1000, %axcmpw $0x9000, %axjz end_movemovw %ax, %dsxorw %di, %dixorw %si, %simovw $0x8000, %cx /* 复制64KB数据 */repmovswjmp do_move
end_move:/* 加载GDT并切换到保护模式 */movw $SETUPSEG, %axmovw %ax, %dslgdt gdt_48 /* 加载全局描述符表 *//* 加载IDT(包含时钟中断描述符) */call setup_idt /* 初始化中断描述符表 */lidt idt_48 /* 加载IDT寄存器 *//* 切换到保护模式 */movl %cr0, %eaxorl $1, %eaxmovl %eax, %cr0.byte 0x66, 0xea /* 远跳转到32位代码 */.long protected_mode.word 0x0008 /* 代码段选择子 *//* 32位保护模式代码 */
.code32
protected_mode:/* 初始化数据段寄存器 */movl $0x10, %eaxmovw %ax, %dsmovw %ax, %esmovw %ax, %fsmovw %ax, %gsmovw %ax, %ssmovl $0x90000, %esp /* 设置栈指针 *//* 开启中断 */sti/* 显示时钟中断就绪标志 */movl $0xb8000 + 2*80, %edi /* 第2行起始位置 */movb $'C', %al /* 'C'表示时钟就绪 */movb $0x0A, %ah /* 绿底黑字 */movw %ax, (%edi)loop:jmp loop /* 等待时钟中断 *//* 初始化8259A中断控制器:允许IRQ0时钟中断 */
init_8259A:/* 主8259A初始化 */movb $0x11, %al /* ICW1:边沿触发,多片 */outb %al, $0x20.word 0x00eb, 0x00eb /* 短延迟 */movb $0x20, %al /* ICW2:IRQ0映射到向量0x20 */outb %al, $0x21.word 0x00eb, 0x00ebmovb $0x04, %al /* ICW3:主片级联 */outb %al, $0x21.word 0x00eb, 0x00ebmovb $0x01, %al /* ICW4:8086模式 */outb %al, $0x21.word 0x00eb, 0x00ebmovb $0xFE, %al /* OCW1:仅允许IRQ0(时钟)中断 */outb %al, $0x21ret/* 初始化8253定时器:产生18.2Hz时钟信号 */
init_8253:movb $0x36, %al /* 控制字:计数器0,模式3,二进制 */outb %al, $0x43.word 0x00eb, 0x00ebmovb $0x00, %al /* 计数器0低8位(初值0xFFFF) */outb %al, $0x40.word 0x00eb, 0x00ebmovb $0xFF, %al /* 计数器0高8位 */outb %al, $0x40.word 0x00eb, 0x00ebret/* 初始化IDT:注册时钟中断处理程序(向量0x20) */
setup_idt:leal idt, %edi /* EDI = IDT基地址 */movl $256, %ecx /* 初始化256个中断描述符 */movl $ignore_int, %edx /* 默认处理程序地址 */movl $0x00080000, %eax /* 高16位=0,低16位=处理程序偏移 */movw %dx, %ax /* AX = 处理程序偏移低16位 */movw $0x8E00, %dx /* 中断门属性(P=1,DPL=0,32位) */rp_idt:movl %eax, (%edi) /* 偏移低32位 */movl %edx, 4(%edi) /* 选择子+属性 */addl $8, %edi /* 下一个描述符 */decl %ecxjne rp_idt/* 单独设置时钟中断描述符(向量0x20) */leal 0x20*8(%edi - 256*8), %edi /* 定位到向量0x20 */leal clock_int, %edx /* 时钟处理程序地址 */movw %dx, %ax /* 更新偏移低16位 */movl %eax, (%edi)movl $0x8E00 + 0x0008, 4(%edi) /* 选择子=0x08(内核代码段) */ret/* 时钟中断处理程序 */
clock_int:pushal /* 保存所有通用寄存器 *//* 更新屏幕显示(第3行显示中断计数) */movl $0xb8000 + 3*80*2, %edi /* 显示位置:第3行第0列 */incl (%edi) /* 计数+1(初始值0) */movb $0x0C, %ah /* 红底黑字 */movb (%edi), %al /* 计数数值 */addb $'0', %al /* 转换为ASCII */movw %ax, (%edi)/* 发送EOI信号给8259A */movb $0x20, %aloutb %al, $0x20 /* 主控制器EOI */popal /* 恢复寄存器 */iret /* 中断返回 *//* 默认中断处理程序 */
ignore_int:pushalmovl $0xb8000 + 4*80*2, %edi /* 第4行显示错误 */movb $'!', %almovb $0x0F, %ah /* 白字黑底 */movw %ax, (%edi)movb $0x20, %aloutb %al, $0x20popaliret/* 全局描述符表(GDT) */
gdt:.word 0, 0, 0, 0 /* 空描述符 */.word 0x07ff, 0x0000, 0x9A00, 0x00C0 /* 代码段:0-32MB */.word 0x07ff, 0x0000, 0x9200, 0x00C0 /* 数据段:0-32MB */.word 0xffff, 0x8000, 0x920b, 0x00C0 /* 视频段:0xB8000 */gdt_48:.word 0x800 /* GDT长度 */.word 512 + gdt, 0x9 /* GDT基地址(0x9xxxx) *//* 中断描述符表(IDT) */
idt:.fill 256, 8, 0 /* 256个中断描述符 */idt_48:.word IDT_LIMIT /* IDT长度 */.word IDT_BASE + idt, 0x0 /* IDT基地址 *//* 字符串与填充 */
setup_msg:.ascii "setup is running".fill 2048 - (.-_start_setup), 1, 0 /* 填充到2048字节 */
三、编译与实验验证
1. 编译命令
bash
# 汇编生成目标文件
as -32 -o setup.o setup.s# 链接生成2048字节二进制
ld -m elf_i386 -Ttext 0x0 -s --oformat binary -e _start_setup -o setup setup.o# 验证文件大小
ls -l setup | awk '{print $5 " 字节(预期2048字节)"}'
2. 制作镜像与运行
bash
# 拼接引导扇区和setup程序(假设引导扇区为bootsect)
cat bootsect setup > linux.img# 使用QEMU运行
qemu-system-i386 -fda linux.img -boot a -vga std -no-reboot
3. 预期实验现象
- QEMU 窗口第 2 行显示
C
(时钟就绪标志) - 第 3 行字符随时间递增(每 55ms+1),表明时钟中断正常响应
- 无其他错误字符(如第 4 行无
!
),说明中断向量配置正确
四、关键代码解析
1. 8253 定时器初始化
asm
movb $0x36, %al ; 控制字:计数器0,模式3(方波)
outb %al, $0x43
movb $0x00, %al ; 初值低8位(0xFFFF)
outb %al, $0x40
movb $0xFF, %al ; 初值高8位
outb %al, $0x40
- 定时器 0 工作在模式 3(方波输出),初值 0xFFFF,产生约 18.2Hz 的周期性中断
2. 时钟中断向量绑定
asm
leal 0x20*8(%edi), %edi ; 定位到IDT的0x20号向量
leal clock_int, %edx ; 绑定时钟处理程序
movw %dx, %ax ; 存储处理程序偏移
movl %eax, (%edi)
movl $0x8E00 + 0x08, 4(%edi) ; 内核代码段选择子(0x08)
- 中断门属性
0x8E00
表示 32 位中断门,特权级 0 - 选择子
0x08
对应 GDT 中的内核代码段
3. 中断处理程序
asm
clock_int:pushal ; 保存寄存器incl (%edi) ; 更新计数movb $0x20, %aloutb %al, $0x20 ; 发送EOIpopaliret ; 中断返回
- 必须发送 EOI 信号(
0x20
),否则 8259A 会屏蔽后续中断 iret
指令自动恢复 CS、EIP、EFLAGS 寄存器
五、常见问题解决
时钟中断无响应
- 检查 8259A 初始化:
movb $0xFE, %al
确保仅开启 IRQ0 - 验证 IDT 加载:
lidt idt_48
指令是否正确执行 - 确认 GDT 代码段选择子:中断门选择子必须为内核代码段(0x08)
- 检查 8259A 初始化:
中断后系统崩溃
- 检查堆栈设置:保护模式下
%esp
需指向有效内存(如 0x90000) - 确保
pushal
与popal
配对,避免寄存器状态混乱
- 检查堆栈设置:保护模式下
QEMU 显示异常
- 验证 VGA 内存地址:文本模式内存基地址为 0xB8000
- 检查字符 ASCII 转换:计数需加
'0'
才能正确显示数字
总结
本文实现了 Linux 0.12 内核时钟中断的完整流程,从 8253 定时器初始化到 IDT 向量绑定,再到中断处理程序编写,所有代码严格遵循 AT&T 语法及 as 汇编器规范。通过 QEMU 运行可观察到周期性的中断计数更新,直观验证时钟中断的响应机制。这一实现为后续进程调度、时间管理等内核功能奠定了基础。