注:以下程序参考了Linux0.12, linux 2.4, linux 2.6, skelix,但不雷同。以下理论知识来自《Linux内核完全剖析 0.12》
先传一张baby的萌照~
中断和异常
中断(Interrupt)和异常(Exception)是指明系统、处理器或当前执行程序(或任务)的某处出现了一个事件需要处理器处理。通常这种事件会导致执行控制被强制从当前运行的程序转移到被称为中断处理程序(interrupt handler)或异常处理程序(exception handler)的特殊软件函数或任务中。处理器相应中断或异常所采取的行动被称为中断/异常服务(处理)
1.异常和中断向量
为了有助于处理异常和中断,每个需要被处理的处理器定义的异常和中断条件都被赋予了一个标识号,称为向量(vector)。处理器把赋予异常和中断的向量用作中断描述符表(IDT,interrupt descriptor table)中的一个索引号,来定位一个异常或中断的处理程序入口点位置。
允许的向量范围是0~255.0~31保留用作80x86处理器定义的异常和中断(不过目前并非都已经有定义)。
32~255的向量用于用户定义的中断。这些中断通常用作外部IO设备,使得这些设备可以通过外部硬件中断机制向处理器发送中断。
----------------------------------------------------------------------------------------------------------------------------
向量号 助记符 说明 类型 错误号 产生源
------------------------------------------------------------------------------------------------------------------------
0 #DE 除出错 故障 无 DIV或IDIV指令
1 #DB 调试 故障/陷阱 无 任何代码或数据引用,或是INT 1指令
2 -- NMI中断 中断 无 非屏蔽外部中断
3 #BP 断点 陷阱 无 INT 3指令
4 #OF 溢出 陷阱 无 INTO指令
5 #BR 边界范围超出 故障 无 BOUND指令
6 #UD 无效操作码(未定义操作码) 故障 无 UD2指令或保留的操作码。(Pentium Pro中加入的新指令)
7 #NM 设备不存在(无数学协处理器)故障 无 浮点或WAIT/FWAIT指令
8 #DF 双重错误 异常终止 有(0) 任何可产生异常、NMI或INTR的指令
9 -- 协处理器段超越(保留) 故障 无 浮点指令(386以后的CPU不产生该异常)
10 #TS 无效的任务状态段TSS 故障 有 任务交换或访问TSS
11 #NP 段不存在 故障 有 加载段寄存器或访问系统段
12 #SS 堆栈段错误 故障 有 堆栈操作和SS寄存器加载
13 #GP 一般保护错误 故障 有 任何内存引用和其他保护检查
14 #PF 页面错误 故障 有 任何内存引用
15 -- (Intel保留,请勿使用) 无
16 #MF x87 FPU浮点错误(数学错误) 故障 无 x87 FPU浮点或WAIT/FWAIT指令
17 #AC 对起检查 故障 有(0) 对内存中任何数据的引用
18 #MC 机器检查 异常终止 无 错误码(若有)和产生源与CPU类型有关(奔腾处理器引进)
19 #XF SIMD浮点异常 故障 无 SSE和SSE2浮点指令(PIII处理器引进)
20-31 -- (Intel保留,请勿使用)
32-255 -- 用户定义(非保留)中断 中断 外部中断或者INT n指令
----------------------------------------------------------------------------------------------------------------------------
2. 中断源和异常源
中断源:
1)外部(硬件产生)的中断
2)软件产生的中断
外部中断通过处理器芯片上的两个引脚(INTR和NMI)接收。
当引脚INTR(可屏蔽中断)接收到外部发生的中断信号时,处理器就会从系统总线上读取外部中断控制器(如8259A)提供的中断向量号
当引脚NMI(非可屏蔽中断)接收到信号时,就产生一个非屏蔽中断,它使用固定的中断向量号2.
标志寄存器EFLAGS中的IF标志可用来屏蔽所有可屏蔽中断。
通过在指令操作数中提供中断向量号,INT n指令可用于从软件中产生中断。
异常源:
1)处理器检测到的程序错误异常
2)软件产生的异常
3.异常分类
1)Fault(故障):可以被纠正的异常
2)Trap(陷阱):是一个一起陷阱的指令被执行后会被报告的异常
3)Abort(终止):用于报告严重的错误,不会总是报告异常精确位置,不允许导致异常的程序重新继续执行
4.程序或任务的重新执行
为了让程序或任务在一个异常或中断处理完成之后能重新恢复执行,除了终止之外的所有异常都能报告精确的指令位置,并且所有中断保证是在指令边界上发生
5.中断描述符表
IDT,将每个异常或中断向量分别与它们的处理过程联系起来。
IDT可以驻留在线性地址空间的任何地方,处理器使用IDTR寄存器来定位IDT表的位置。
6.IDT描述符
IDT中可以存放三种类型的门描述符:中断门、陷阱门、任务门。
7.异常与中断处理
处理器对异常和中断过程的调用操作方法与使用CALL指令调用过程和任务的方法类似。当相响应一个异常或中断时,处理器使用异常或中断的向量作为IDT表中的索引。
如果索引值指向中断门或陷阱门,则处理器使用与CALL操作调用门类似的方法调用异常或中断处理过程。如果索引值指向任务门,则处理器使用与CALL指令操作任务门相似的方法进行任务切换,执行异常或中断的处理任务。
8.错误码
当异常条件与一个特定的段相关时,处理器会把一个错误码压入异常处理过程的堆栈上。
中断操作原理
微型计算机处理器向输入输出设备提供服务的一种方法是使用轮询方式。处理器顺序地查询系统中的每个设备,“询问”他们是否需要服务。此法简单但低效。
另一种方法是在设备需要服务时自己向处理器提出请求,处理器只有在设备提出请求时才向其提供服务。
当设备向处理器提出服务请求时,处理器会在执行完当前的一条指令后立即应答设备的请求,并转而执行该设备的相关服务程序。当服务程序执行完后,处理器会接着去做刚才被中断的程序。
设备向处理器发出的服务请求称为中断服务请求(IRQ,interrupt request)。
处理器响应请求而执行的设备相关程序则被称为中断服务程序/中断服务过程(ISR,interrupt service routine)。
可编程中断控制器(PIC,programmable interrupt controller)是微机系统中管理设备中断请求的管理者。它通过连接到设备的中断请求引脚接受设备发出的中断服务请求信号。
80x86微机系统中采用了8259A可编程中断控制芯片。PC/AT系统兼容机中使用了两片8259A芯片,共可管理15级中断向量。
8259A中断控制器编程方法
1)工作原理
PC/AT系列兼容机中使用了级联的两片8259A 可编程控制器(PIC)芯片,共可管理15级中断向量。从片的INT引脚连接到主片的IR2引脚上。
主片端口地址0x20,从片0xA0.
如图1
中断请求寄存器(IRR,interrupt request register)用来保存中断请求输入引脚上所有请求服务的中断级,寄存器8个位对应引脚IR7~IR0.
中断屏蔽寄存器(IMR,interrupt mask register)用来保存被屏蔽的中断请求线对应的位。寄存器8个位对应8个中断级。
优先级解析器(PR,Priority resolver)用于确定IRR中所设置位的优先级。
正在服务寄存器(ISR,In-service Register)中保存着正在接受服务的中断请求。
在8259A可以正常操作之前,必须首先设置初始化命令字(ICW,initialization command words)寄存器组的内容。
工作工程中可使用写入操作命令字(OCW,operation command words)寄存器组来随时设置和管理8259A的工作方式。
来自各个设备的中断请求线分别连接到8259A的IR0~IR7中断请求引脚上。当这些引脚上有一个或多个中断请求信号到来时,IRR中相应位被锁存,若IMR中对应位被置位,则相应的中断请求不会送到PR。否则送到PR选出最高优先级的中断,8259A向CPU发送一个INT信号,CPU执行完当前的一条指令后向8259A发送一个INTA来响应中断信号。8259A收到响应信号把所选出的中断请求保存到ISR,同时IRR中相应位被复位,表示中断请求开始被处理。此后CPU向8259A发出第二个INTA脉冲信号,通知8259A送出中断号。
CPU中断周期结束后,若8259A使用自动结束中断(AEOI),第二个INTA信号结尾ISR服务中断位被置位,否则中断程序结束时需向8259A发送一个结束中断(EOI)命令以复位ISR中的位。
初始化命令字
8259A有4种工作方式:全嵌套方式、循环优先级方式、特殊屏蔽方式和程序查询方式。
选定工作方式:
1)8259A工作之前对每个8259A的4个初始化命令字(ICW1~ICW4)的写入编程
2)工作工程中随时对8259A的三个操作命令字(OCW1~OCW3)进行编程
代码:
1.原load.s更名为setup.s,因为它还有负责重新设置GDT、IDT等任务,不再仅仅是加载内核
#----------------------------------------------------------------------------
# 默认的中断处理函数:
# 暂时设为这个函数,后面会修改为正确的函数
#----------------------------------------------------------------------------
interrupt_msg:
.asciz "未知的中断"
.align 2
default_interrupt:
pushl %eax
pushl %ecx
pushl %edx
push %ds # ds,es,fs虽然是16位寄存器,但会以32位形式入栈
push %es
push %fs
movl $DATA_SELECTOR, %eax
movw %ax, %ds
movw %ax, %es
movw %ax, %fs
pushl $200 # 显示字符串的位置top
pushl $200 # 显示字符串的位置left
pushl $interrupt_msg # 显示的字符串
call draw_string # 调用显示字符串函数
popl %eax
pop %fs
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax
iret # 中断返回
#----------------------------------------------------------------------------
# 设置GDT:
# 前面boot.s中的GDT所在的位置并不是一个安全的区域,容易被覆盖,此处将设置
# 一个新的GDT
#----------------------------------------------------------------------------
setup_gdt:
lgdt gdt_ptr
ret
#----------------------------------------------------------------------------
# 设置IDT:
# 设置一个新的IDT,它的每一项有8个字节,0-1,6-7字节是偏移量,2-3字节是选择符
# 4-5字节是一些标志。暂时将256个项都指向一个default_int中断门。而真正的中断门
# 将在后面重新安装
# 入口点31...16 | 1000 1110 0000 0000
# 段选择符 | 入口点15...0
#----------------------------------------------------------------------------
setup_idt:
lea default_interrupt, %edx # edx为默认中断处理程序地址
movl $CODE_SELECTOR<<16, %eax # eax high 16位为代码段选择符
movw %dx, %ax # eax low 16位为中断处理程序地址低16位
movw $0x8e00, %dx # edx low 16位为中断门的一些标志
movl $256, %ecx
lea idt, %edi
loop_set_idt:
movl %eax, (%edi)
movl %edx, 4(%edi)
addl $8, %edi
dec %ecx
jne loop_set_idt
lidt idt_ptr # 加载中断描述符表寄存器值
ret
2.前32号IDT中暂时都放陷阱门
下面是IDT如何把中断和中断处理程序联系起来:
exception.s:
/*************************************************************************
> File: exception.s
> 描述: 异常处理,包括大部分硬件的故障或出错处理的低层代码,主要设计
Intel保留中断的int0-int16的处理
> Author: 孤舟钓客
> Mail: guzhoudiaoke@126.com
> Time: 2013年01月11日 星期五 22时23分39秒
************************************************************************/
.include "kernel.inc"
/*
----------------------------------------------------------------------------------------------------------------------
向量号 助记符 说明 类型 错误号 产生源
--------------------------------------------------------------------------------------------------------------------
0 #DE 除出错 故障 无 DIV或IDIV指令
1 #DB 调试 故障/陷阱 无 任何代码或数据引用,或是INT 1指令
2 -- NMI中断 中断 无 非屏蔽外部中断
3 #BP 断点 陷阱 无 INT 3指令
4 #OF 溢出 陷阱 无 INTO指令
5 #BR 边界范围超出 故障 无 BOUND指令
6 #UD 无效操作码(未定义操作码) 故障 无 UD2指令或保留的操作码。(Pentium Pro中加入的新指令)
7 #NM 设备不存在(无数学协处理器)故障 无 浮点或WAIT/FWAIT指令
8 #DF 双重错误 异常终止 有(0) 任何可产生异常、NMI或INTR的指令
9 -- 协处理器段超越(保留) 故障 无 浮点指令(386以后的CPU不产生该异常)
10 #TS 无效的任务状态段TSS 故障 有 任务交换或访问TSS
11 #NP 段不存在 故障 有 加载段寄存器或访问系统段
12 #SS 堆栈段错误 故障 有 堆栈操作和SS寄存器加载
13 #GP 一般保护错误 故障 有 任何内存引用和其他保护检查
14 #PF 页面错误 故障 有 任何内存引用
15 -- (Intel保留,请勿使用) 无
16 #MF x87 FPU浮点错误(数学错误) 故障 无 x87 FPU浮点或WAIT/FWAIT指令
17 #AC 对齐检查 故障 有(0) 对内存中任何数据的引用
18 #MC 机器检查 异常终止 无 错误码(若有)和产生源与CPU类型有关(奔腾处理器引进)
19 #XF SIMD浮点异常 故障 无 SSE和SSE2浮点指令(PIII处理器引进)
20-31 -- (Intel保留,请勿使用)
32-255 -- 用户定义(非保留)中断 中断 外部中断或者INT n指令
---------------------------------------------------------------------------------------------------------------------
*/
.global int_0_divide_error, int_1_debug, int_2_nmi, int_3_break_point, int_4_overflow
.global int_5_bounds_check, int_6_invalid_opcode, int_7_device_not_available
.global int_8_double_fault, int_9_coprocessor_seg_overrun, int_10_invalid_tss
.global int_11_segment_not_present, int_12_stack_segment, int_13_general_protection
.global int_14_page_fault, int_15_reserved, int_16_coprocessor_error
# 中断处理过程中,若优先级发生变化,会将原ss,esp压入栈中
# 用户程序(进程)将控制权交给中断处理程序之前CPU将至少12字节(EFLAGS、CS、EIP)
# 压入中断处理程序(而不是被中断代码)的堆栈中,即进程的内核态栈中,这种情况与远调用相似
# 有些异常引起中断时,CPU内部会产生一个出错码压入堆栈
# 然后将中断处理程序地址入栈,堆栈指针指向esp_isr处
# 然后所有32位寄存器入栈
# 然后ds,es,fs,gs入栈,此时堆栈指针指向esp_push_all_regs
#
# 堆栈内容:
# -------------------阶段1,优先级改变,保护原来的堆栈---------------------------
# 72 - 原ss
# 66 - 原esp
# -------------------阶段2,控制权交给中断处理程序之前,CPU自动压栈--------------
# 64 - 原eflags
# 60 - cs <- 代码段选择符
# 56 - eip <- 返回地址
# 52 - error_code/0 <- 错误码可能没有,若没有中断处理程序自己压入0,见下面
# -------------------阶段3-------------------------------------------------------
# 48 - 中断服务程序地址 <- 由下面各个处理自己压栈
# -------------------阶段4-------------------------------------------------------
# 44 - eax
# 40 - ecx
# 36 - edx
# 32 - ebx
# 28 - esp
# 24 - ebp
# 20 - esi
# 16 - edi <- pushad
# 12 - ds
# 08 - es
# 04 - fs
# 00 - gs
int_service_routine:
pushal
push %ds
push %es
push %fs
push %gs
movl $DATA_SELECTOR, %edx
movw %dx, %ds
movw %dx, %es
movw %dx, %fs
movw %dx, %gs
movl 48(%esp), %eax
call *%eax # '*'号表示调用操作数指定地址处的函数
pop %gs
pop %fs
pop %es
pop %ds
popal
addl $8, %esp # 跳过48,52处的中断处理程序地址和错误号
iret
# int0 - 处理被0除出错的情况,无出错号,自动压入0
# 当0用作除数时发生
# do_divide_error为C程序,在traps.c中实现
int_0_divide_error:
pushl $0
pushl $do_divide_error
jmp int_service_routine
# int1 - debug调试中断入口点,无错误号,压入0代替
# 当eflags中的TF标志位置位时引发(当发现硬件断点、开启了指令跟踪陷阱或任务交换陷阱
# 或者调试寄存器访问无效时,CPU会产生该异常
int_1_debug:
pushl $0
pushl $do_debug
jmp int_service_routine
# int2 - 非屏蔽中断调用入口点, 无错误号
# 仅有的被赋予固定中断向量的硬件中断。每当接收到NMI信号,CPU内部产生中断向量2并执行
# 标准中断应答周期。NMI通常保留为极为重要的硬件事件使用。
# 当CPU收到一个NMI信号并且开始执行其中断处理过程时,随后所有的硬件中断都将被忽略
int_2_nmi:
pushl $0
pushl $do_nmi
jmp int_service_routine
# int3 - 断点指令引起中断的入口点,无错误号
# 与硬件中断无关,通常由调试器插入被调试的程序的代码中,处理过程同debug
int_3_break_point:
pushl $0
pushl $do_debug
jmp int_service_routine
# int4 - 溢出出错处理中断的入口点,无错误号
# EFLAGS中OF标志位置位时CPU执行INTO指令就会引发该中断。通常用于编译器跟踪算数计算溢出
int_4_overflow:
pushl $0
pushl $do_overflow
jmp int_service_routine
# int5 - 边界检查出错中断的入口点,无错误号
# 当操作数在有效范围以外时引发的中断。当BOUND指令测试失败就会产生该中断
# BOUND指令有三个操作数,若第一个不在另外两个之间,引发该异常
int_5_bounds_check:
pushl $0
pushl $do_bounds_check
jmp int_service_routine
# int6 - 无效操作指令出错中断的入口点,无错误号
# CPU执行机构检测到一个无效的操作码而引起的中断
int_6_invalid_opcode:
pushl $0
pushl $do_invalid_opcode
jmp int_service_routine
# int7 - 设备不存在引起中断的入口点,无错误号
int_7_device_not_available:
pushl $0
pushl $do_device_not_available
jmp int_service_routine
# int8 - 双出错故障,有错误号
# 通常CPU在调用前一个异常的处理程序而又检测到一个新的异常时,两个异常会被串行处理,
# 当CPU不能进行这样的串行处理时,会引发该中断
int_8_double_fault:
pushl $do_double_fault
jmp int_service_routine
# int9 - 协处理器段超出出错引起中断的入口点,无错误号
# 基本上等同于协处理器出错保护
int_9_coprocessor_seg_overrun:
pushl $0
pushl $do_coprocessor_seg_overrun
jmp int_service_routine
# int10 - 无效的任务状态段(TSS),有错误号
# 当CPU企图切换到一个进程,而该进程的TSS无效时引发该异常
int_10_invalid_tss:
pushl $do_invalid_tss
jmp int_service_routine
# int11 - 段不存在,有错误号
# 被引用的段不在内存中
int_11_segment_not_present:
pushl $do_segment_not_present
jmp int_service_routine
# int12 - 堆栈段错误,有错误号
int_12_stack_segment:
pushl $do_stack_segment
jmp int_service_routine
# int13 - 一般保护性错误,有错误号
int_13_general_protection:
pushl $do_general_protection
jmp int_service_routine
# int14 - 页错误,有错误号
int_14_page_fault:
pushl $do_page_fault
jmp int_service_routine
# int15 - 其他Intel保留的入口点引起中断的入口点,无错误号
int_15_reserved:
pushl $0
pushl $do_reserved
jmp int_service_routine
# int16 - 协处理器引起中断的入口点,无错误号
int_16_coprocessor_error:
pushl $0
pushl $do_coprocessor_error
jmp int_service_routine
traps.c:
/*************************************************************************
> File: traps.c
> Author: 孤舟钓客
> Mail: guzhoudiaoke@126.com
> Time: 2013年01月12日 星期六 03时15分22秒
************************************************************************/
#include <traps.h>
#include <graffiti.h>
#include <graphics.h>
#include <font.h>
#include <kernel.h>
extern u32 idt;
static char* exception_msg[] = {
"int0 #DE 除出错",
"int1 #DB 调试",
"int2 -- NMI中断",
"int3 #BP 断点",
"int4 #OF 溢出",
"int5 #BR 边界范围超出",
"int6 #UD 无效操作码",
"int7 #NM 设备不存在",
"int8 #DF 双重错误",
"int9 -- 协处理器段超越",
"int10 #TS 无效TSS",
"int11 #NP 段不存在",
"int12 #SS 堆栈段错误",
"int13 #GP 一般保护错误",
"int14 #PF 页面错误",
"int15 -- (Intel保留)",
"int16 #MF x87 FPU浮点错误",
"int17 #AC 对齐检查",
};
/* 声明一些函数原型,定义在exceptino.s中 */
void int_0_divide_error();
void int_1_debug();
void int_2_nmi();
void int_3_break_point();
void int_4_overflow();
void int_5_bounds_check();
void int_6_invalid_opcode();
void int_7_device_not_available();
void int_8_double_fault();
void int_9_coprocessor_seg_overrun();
void int_10_invalid_tss();
void int_11_segment_not_present();
void int_12_stack_segment();
void int_13_general_protection();
void int_14_page_fault();
void int_15_reserved();
void int_16_coprocessor_error();
static void disp_exception_info(char* str)
{
draw_window(300, 250, 200, 100);
set_color(RGB(0, 0, 0));
draw_string(str, 300+20, 250+50);
io_halt();
}
void do_divide_error()
{
disp_exception_info(exception_msg[0]);
}
void do_debug()
{
disp_exception_info(exception_msg[1]);
}
void do_nmi()
{
disp_exception_info(exception_msg[2]);
}
void do_overflow()
{
disp_exception_info(exception_msg[4]);
}
void do_bounds_check()
{
disp_exception_info(exception_msg[5]);
}
void do_invalid_opcode()
{
disp_exception_info(exception_msg[6]);
}
void do_device_not_available()
{
disp_exception_info(exception_msg[7]);
}
void do_double_fault()
{
disp_exception_info(exception_msg[8]);
}
void do_coprocessor_seg_overrun()
{
disp_exception_info(exception_msg[9]);
}
void do_invalid_tss()
{
disp_exception_info(exception_msg[10]);
}
void do_segment_not_present()
{
disp_exception_info(exception_msg[11]);
}
void do_stack_segment()
{
disp_exception_info(exception_msg[12]);
}
void do_general_protection()
{
disp_exception_info(exception_msg[13]);
}
void do_page_fault()
{
disp_exception_info(exception_msg[14]);
}
void do_reserved()
{
disp_exception_info(exception_msg[15]);
}
void do_coprocessor_error()
{
disp_exception_info(exception_msg[16]);
}
/*
* 使用陷阱门设置IDT表项:
* 入口点31-16位 | 陷阱门标志
* 代码段选择符 | 入口点15-0位
*/
static void set_trap_gate(u32 index, u32 addr)
{
u64 idt_item;
idt_item = 0x00008f0000000000ULL; /* 陷阱门的标志 */
idt_item |= (u64)CODE_SELECTOR << 16; /* 代码段选择子 */
idt_item |= ((u64)addr << 32) & 0xffff000000000000ULL; /* 地址high16位 */
idt_item |= ((u64)addr) & 0xffff; /* 地址low16位 */
*(&idt + index*8) = idt_item;
}
void init_trap()
{
int i;
set_trap_gate(0, (u32)&int_0_divide_error);
set_trap_gate(1, (u32)&int_1_debug);
set_trap_gate(2, (u32)&int_2_nmi);
set_trap_gate(3, (u32)&int_3_break_point);
set_trap_gate(4, (u32)&int_4_overflow);
set_trap_gate(5, (u32)&int_5_bounds_check);
set_trap_gate(6, (u32)&int_6_invalid_opcode);
set_trap_gate(7, (u32)&int_7_device_not_available);
set_trap_gate(8, (u32)&int_8_double_fault);
set_trap_gate(9, (u32)&int_9_coprocessor_seg_overrun);
set_trap_gate(10, (u32)&int_10_invalid_tss);
set_trap_gate(11, (u32)&int_11_segment_not_present);
set_trap_gate(12, (u32)&int_12_stack_segment);
set_trap_gate(13, (u32)&int_13_general_protection);
set_trap_gate(14, (u32)&int_14_page_fault);
set_trap_gate(15, (u32)&int_15_reserved);
set_trap_gate(16, (u32)&int_16_coprocessor_error);
for (i = 17; i < 31; i++)
set_trap_gate(i, (u32)&int_15_reserved);
}
i8259a.c:
/*************************************************************************
> File: i8259a.c
> 描述: 实现操作8259A芯片
> Author: 孤舟钓客
> Mail: guzhoudiaoke@126.com
> Time: 2013年01月12日 星期六 19时51分13秒
************************************************************************/
#include <i8259a.h>
void init_8259a(void)
{
/* ICW1:0x11, 表示边沿触发、多片级联、需要发送ICW4 */
io_outb(0x11, 0x20);
io_outb(0x11, 0xa0);
/* ICW2:
* 重新映射 IRQ0~IRQ7 到 0x20~0x27,重新映射 IRQ8~IRQ15 到 0x28~0x2F */
io_outb(0x20, 0x21);
io_outb(0x28, 0xa1);
/* ICW3:
* A0 D7 D6 D5 D4 D3 D2 D1 D0
* 主片 1 S7 S6 S5 S4 S3 S2 S1 S0
* 从片 1 0 0 0 0 0 ID2 ID1 ID0
* 主片:0x04,表示主芯片的IR2引脚连接一个从芯片
* 从片:0x02,表示从片连接到主片的IR2 引脚 */
io_outb(0x04, 0x21);
io_outb(0x02, 0xa1);
/* 从片连接到主片的 IRQ2 */
/* ICW4:0x01,表示8259A芯片被设置成普通嵌套、非中断方式、
* 用于8086 及其兼容模式 */
io_outb(0x01, 0x21);
io_outb(0x01, 0xa1);
/* 关闭 IRQ0~IRQ7 号中断 */
io_outb(0xff, 0x21);
/* 关闭 IRQ8~IRQ15 号中断 */
io_outb(0xff, 0xa1);
}