Ⅰ.中断分类
1.外部中断
(1)外部中断类型
外部中断由两根中断线控制,INTR硬件中断和NMI异常中断,目的是为了区分中断紧急程度。INTR中断接收硬件中断请求,属于可屏蔽中断,即中断请求可以不做及时响应,通过 eflags寄存器的 IF 位将所有这些外部设备的中断屏蔽 。NMI中断来自于毁灭性异常,如内存崩溃、电源断开,中断一旦发生,意味着系统将崩溃,是无法屏蔽的。
并行和并发区别:并行指的是同时运行,只针对多核CPU而言;并行是指一个时间段内允许多个任务同时运行,针对单道处理系统而言。
(2)中断嵌套
任务a执行过程中,一个优先级比他更高的任务b向CPU发送中断请求,CPU执行中断处理程序,保存任务a的现场,切换到任务b,开始执行。若任务b执行过程中,又来了一个任务c,他的优先级比b还高,此时任务b依旧执行,执行结束后,比较任务队列中优先级大小,执行优先级最高的任务。
2.内部中断
(1)软中断
软中断来自于软件主动发起的中断。如int 8 位立即数 (执行中断号)、int3(调试断点指令 )、into(中断溢出指令 )、bound(数组越界中断)、ud2(未定义指令)。
(2)异常
CPU在执行指令是出现的指令语法错误引起的。不受标志寄存器 eflags 中的 E 位影响,无法向用户隐瞒。into、bound、ud2中断也属于异常中断。
对于中断是否无视 eflags 中的 IF 位,可以这么理解:
(1 )首先,只要是导致运行错误的中断类型都会无视 IF 位,不受 IF 位的管束,如 NMI 、异常。
(2 )其次,由于 int n 型的软中断用于实现系统调用功能,不能因为 IF位为 0 就不顾用户请求,所以
为了用户功能正常,软中断必须也无视 IF位。
总结:只要中断会影响到系统的“正常”执行,那么不受IF位影响。要么是错误异常,要么是软中断。
(3)中断调用过程
为了统一中断管理,把来自外部设备、内部指令的各种中断类型统统归结为一种管理方式,即为每个中断信号分配一个整数,用此整数作为中断的ID,而这个整数就是所谓的中断向量。内部中断的中断号在指令中,外部中断的终端号来自INTR和NMI,以中断号为索引检索中断向量表(实模式)/中断描述符表(保护模式),得到中断处理程序并执行。
异常和不可屏蔽中断的中断向量号是由 CPU 自动提供的,而来自外部设备的可屏蔽中断号是由中断代理提供的(咱们这里的中断代理是 8259A ),软中断是由软件提供的。
Ⅱ.中断描述符表
中断门包含了中断处理程序所在段的段选择子和段内偏移地址。当通过此方式进入中断后,标志寄存器 eflags 中的 IF 位自动置 0,也就是在进入中断后,自动把中断关闭,避免中断嵌套。 Linux 就是利用中断门实现的系统调用,就是那个著名的 int 0x80。
低端 1队在B 内存布局吗?位于地址0~0x3ff 的是中断向量表 IVT,它是实模式下用于存储中断处理程序入口的表。IVT 可容纳 256 个中断向量,每个中断向量用 4 字节描述 。
对比中断向量表,中断描述符表有两个区别 。
(1)中断描述符表地址不限制,在哪里都可以 。
(2)中断描述符表中的每个描述符用 8 字节描述。
IDT 的位置不固定,当中断发生时, CPU 是如何找到它的呢? 与全局段描述符表类似,中断描述符表通过IDTR寄存器找到IDT位置,存储中断个数由[15:0]中设定的表界限决定。
加载IDTR方式通过lidt命令:lidt 48位内存数据
1.中断执行过程
执行中断过程涉及到处理器根据中断号找到中断描述符,处理器优先级判断,执行中断处理程序
(1)处理器根据中断号找到中断描述符
一个中断门描述符占8个字节,中断向量号*8+中断描述符表基地址=该中断向量号的中断描述符
(2)处理器优先级判断
根据中断处理过程可看出,中断处理过程涉及两个优先级判断过程,访问中断描述符表中断判断,访问全局描述符表中断判断。需要保证当前特权级 CPL 必须在门描述符 DPL 和门中目标代码段 DPL 之间。
(3)执行中断处理程序
全局段描述符表的一个段描述符占8字节,GDTR基地址+选择子*8=中断处理代码段的起始地址,将该地址加载到CS中,将中断门描述符中高13位偏移地址加载到EIP中,开始执行。
2.中断发生时的压栈
中断执行过程中需要考虑的变量:首先,一定需要保存当前程序的CS、EIP,还需要保存当前程序是否允许嵌套的设置,即eflags寄存器,如果涉及到特权级变化,需要保存TSS描述符中的SS、ESP寄存器。那么他们的保存顺序呢?
(1)首先在找中断处理程序入口时,需要进行特权级判断,要求CPL在中断门描述符DPL和中断处理程序DPL之间。当涉及到特权级切换时,根据TSS找到切换的栈基地址和栈指针SS_new、ESP_new,那么就需要保存当前栈的SS_old和ESP_old。如图A。
(2)保存EFLAGS寄存器,是否支持中断嵌套。如图B。
(3)代码段切换。保存CS_old、EIP_old,如图C。
(4)保存错误码。某些异常会有错误码,此错误码用于报告异常是在哪个段上发生的,也就是异常发生的位置,所以错误码中包含选择子等信息。
**中断执行结束后,通过iret命令返回。**它是中断处理程序中最后一个指令 。
3.中断返回过程
处理器不保存之前的特权级切换过程,因此中断返回时,需要再次进行特权级判断。此外,依次从栈中恢复寄存器。以特权级切换的中断返回为例:
(1)特权级检查。从栈中取出CS_old的RPL与当前的CPL判断,要求DPL≤CPL。
(2)特权级满足要求后,恢复CS_old、EIP_old,CS为16bit,EIP为32bit。此处因为入栈的时候将16bit扩展为32bit,出栈只需要舍弃高16bit,取低16bit存入寄存器后即可。
(3)栈中的EFLAGS数据弹出到EFLAGS寄存器中。
(4)根据是否涉及特权级变换决定是否要保存SS、ESP。
无中断切换的返回与此雷同。
4.中断控制器8259A
中断控制器定义:中断代理,在众多中断请求中,选择最符合要求的一个,将中断请求提交给CPU。8259A 用于管理和控制可屏蔽中断,它表现在屏蔽外设中断,对它们实行优先级判决,向 CPU 提供中断向量号等功能。
Intel 处理器共支持 256 个中断,但 8259A 只可以管理 8 个中断 ,多个8259A可通过级联方式扩展,最多可级
联 9 个,也就是最多支持 64 个中断 。 n 片 8259A 通过级联可支持 7n+1个中断源,级联时只能有一片 8259A为主片 master,其余的均为从片 slave。来自从片的中断只能传递给主片,再由主片向上传递给 CPU,也就是说只有主片才会向 CPU 发送 INT 中断信号。
1.8259A处理中断请求过程:
(1)IMR寄存器含有8bit中断屏蔽标志位,依次对应IRQ0~7,根据位匹配判断当前请求中断是否被屏蔽,未屏蔽则放入IRR寄存器,PR选择优先级最高的中断后, 8259A向INT总线发送中断请求。
(2)CPU结束当前工作后,向INTA线发送确认信号。
(3)8259A接收到INTA信号后,立即将刚才选出来的优先级最大的中断在 ISR 寄存器中对应的 BIT 置1,此寄存器表示当前正在处理的中断,同时要将该中断从“待处理中断队列”寄存器 IRR 中去掉 。
(4)之后, CPU 将再次发送 INTA 信号给 8259A,这一次是想获取中断对应的中断向量号,即起始中断向量号+IRQ接口号。
2.8259A编程
8259A的设置主要包括两部分:
一部分是用 ICW 做初始化,用来确定是否需要级联,设置起始中断向量号,设置中断结束模式。某些设置之间是具有关联、依赖性的,因此必须依次写入 ICW1 、 ICW2、ICW3 、ICW4。
另一部分是用 OCW 来操作控制 8259A,前面所说的中断屏蔽和中断结束。
1.ICW初始化设置
(1)ICW1用来初始化 8259A 的连接方式和中断信号的触发方式。连接方式是指用单片工作,还是用多片
级联工作,触发方式是指中断请求信号是电平触发,还是边沿触发。
- IC4 表示是否要写入 ICW4 ,这表示,并不是所有的 ICW 初始化控制字都需要用到。
- SNGL表示单片还是级联,SNGL=1,单片;SNGL=0,级联
- ADI 表示 call address interval,用来设置 8085 的调用时间间隔,x86 不需要设置。
- LTIM 表示 level/edge triggered mode ,用来设置中断检测方式, LTIM 为 0 表示边沿触发, LTIM 为 1
表示电平触发。
(2)ICW2 用来设置起始中断向量号
只需要设置 IRQ0的中断向量号,IRQ1~IRQ7 的中断向量号是 IRQ0 的顺延 。咱们只负责填写高 5 位 T3~T7, ID0~ID2但这低 3 位不用咱们负责。由于咱们只填写高 5 位,所以任意数字都是8 的倍数,这个数字表示的便是设定的起始中断向量号。ID0~ID1对应IRQ0~IRQ7,高 5 位加低 3 位,便表示了任意一个 IRQ 接口实际分配的中断向量号。
(3)ICW3 仅在级联的方式下才需要(如果 ICW1 中的 SNGL 为 0 ),用来设置主片和从片用哪个 IRQ 接口互连。
对于主片, ICW3 中置 1 的那一位对应的 IRQ 接口用于连接从片,若为 0 则表示接外部设备。比如,若主片 IRQ2 和 IRQ5 接有从片,则主片的 ICW3 为 00100100
对于从片,要设置与主片 8259A 的连接方式,“不需要”指定用自己的哪个 IRQ 接口与主片连接
在中断响应时,主片会发送与从片做级联的 IRQ 接口号,所有从片用自己的 ICW3 的低 3 位和它对比,若一致则认为是发给自己的。比如主片用 IRQ2 接口连接从片 A,用 IRQ5 接口连接从片 B,从片 A 的 ICW3 的值就应该设为 00000010,从片 B 的 ICW3 的值应该设为 00000101 。所以,从片 ICW3中的低 3 位 IDO~ID2 就够了,高 5 位不需要,为 0 即可。
(4)ICW4 用于设置 8259A 的工作模式,当 ICW1 中的 IC4 为 1 时才需要 ICW4。
AEOI 表示自动结束中断( Auto End Of interrupt), 8259A 在收到中断结束信号时才能继续处理下一个中断,此项用来设置是否要让 8259A 自动把中断结束。若 AEOI 为 0,则表示非自动,即手动结束中断,咱们可以在中断处理程序中或主函数中手动向 8259A 的主、从片发送 EOI 信号。这种“操作”类命令,通过下面要介绍的 ocw 进行。若 AEOI 为 1 ,则表示自动结束中断。
2.OCW设置
(1)OCW1 用来屏蔽连接在 8259A 上的外部设备的中断信号,实际上就是把 OCW1 写入了即IMR 寄存器。这里的屏蔽是说是否把来自外部设备的中断信号转发给 CPU。
由于外部设备的中断都是可屏蔽中断,所以最终还是要受标志寄存器 eflags 中的 IF位的管束,若 IF 为 0,可屏蔽中断全部被屏蔽,也就是说,在IF为 0 的情况下,即使 8259A 把外部设备的中断向量号发过来, CPU 也置之不理。
M0~M7 对应 8259A 的 IRQ0~IRQ7 ,某位为 1 ,对应的 IRQ 上的中断信号就被屏蔽了。否则某位为0 的话,对应的 IRQ 中断信号则被放行。
(2)OCW2 用来设置中断结束方式和优先级模式。
OCW2 其中的一个作用就是发 EOI 信号结束中断。如果使 SL 为 1 ,可以用优W2 的低 3 位( L2~L0)来指定位于 ISR 寄存器中的哪一个中断被终止,也就是结束来自哪个 IRQ 接口的中断信号。如果 SL 位为 0, L2~L0 便不起作用了, 8259A 会自动将正在处理的中断结束,也就是把 ISR 寄存器中优先级最高的位清 0 。
OCW2 另 一个作用就是设置优先级控制方式,这是用 R 位(第 7 位)来设置的。
如果 R 为 0,表示固定优先级方式,即 IRQ 接口号越低,优先级越高 。
如果 R 为 1 ,表明用循环优先级方式,这样优先级会在 0~7 内循环 。
(3)OCW3 用来设定特殊屏蔽方式及查询方式
Ⅲ.中断处理程序
中断的具体实现过程包括:中断处理程序、中断描述符表IDT、中断控制器初始化、中断控制器配置
1.通过内联汇编构建最小中断处理程序
(1)汇编语言编写中断处理程序
;;;;-------用汇编实现最简单的中断处理程序,每个中断处理程序都打印字符串,然后向主片从片发送EOI信号-------;;;;
[bits 32]
%define ERROR_CODE nop
%define ZERO push 0
extern put_str ;声明外部函数
section .data
intr_str db "interrupt occur!", 0xa, 0 ;oxa为换行符,0为字符串终止符
global intr_entry_table ;定义全局的中断处理程序函数数组
intr_entry_table:
;;;;;;;;;;;;;;;以下都是intr_entry_table的内容;;;;;;;;;;;;;;;;;;;
;通过macro宏定义实现每个中断向量号对应的中断处理程序
;VECTOR 为宏名字叫,2表示2个参数
%macro VECTOR 2
section .text
; 每个中断处理程序都要压入中断向量号
; 所以一个中断向量号对应一个中断处理程序
; 自己知道自己的中断向量号是多少
intr%1entry:
%2
push intr_str
call put_str
add esp,4 ;删除栈中的intr_str
;如果是从片上进入的中断,除了往从片上发送 EOI 外,还要往主片上发送 EOI
mov al,0x20 ;中断结束命令 EOI
out 0xa0,al ; 向从片发送
out 0x20,al ; 向主片发送
add esp,4 ;跨过 error_code
iret ;从中断返回, 32 位下等同指令 iretd
section .data
dd intr%1entry ;存储各个中断入口程序的地址,形成 intr_entry_table 数组
%endmacro
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
...
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO
(2)编写IDT表,安装中断处理程序
本质是建立一个IDT数组,将数组地址以及长度存入idtr寄存器,每个数组内存储的是含有中断处理程序地址的中断门描述符
/* 根据中断处理程序建立中断门描述符,最终构建中断门描述符表 */
#include "interrupt.h"
#include "global.h"
#include "stdint.h"
#defint IDT_DESC_CNT 0x21
/* 中断门描述符结构体,根据中断门描述符结构定义 */
struct gate_desc{
uint16_t func_offset_low_word;
uint16_t selector;
uint8_t dcount;
uint8_t attribute;
uint16_t func_offset_high_word;
}
static struct gate_desc idt[IDT_DESC_CNT]; // 建立中断门描述符表,本质是数组
extern intr_handler intr_entry_table[IDT_DESC_CNT]; // 声明引用定义在 kernel.S的中断处理函数入口数组
/* 根据中断处理程序创建中断门描述符 */
void make_idt_desc(struct gate_desc *g_desc, uint8_t attr, intr_handler function){
g_desc->func_offset_low_word = (uint16_t)function & 0x0000FFFF;
g_desc->selector = SELECTOR_K_CODE;
g_desc->dcount = 0;
g_desc->attribute = attr;
g_desc->func_offset_high_word = ((uint16_t)function & 0xFFFF0000) >> 16;
}
/* 初始化中断门描述符表 */
static void init_idt_desc(void){
for(int i = 0; i < IDT_DESC_CNT; i++){
// intr_entry_table中存储的是中断处理程序的入口地址
make_idt_desc(idt[i], IDT_DESC_DPL0, intr_entry_table[i]);
}
put_str("idt_desc_init done ");
}
/*完成有关中断的所有初始化工作*/
void idt_init(void){
put_str("idt init start");
init_idt_desc();
pic_init(); // 初始化 8259A
/* 利用lidt命令加载idt,高 48 位是 IDT 地址,低 16 位是 IDT 长度 */
uint64_t idt_operand = ((sizeof(idt)-1)) | ( (uint64_t)((uint32_t)idt << 16));
asm volatile("lidt %0"::"m"(idt_operand))
put_str("idt init done");
}
(3)用内联汇编实现端口 I/O 函数
内联汇编规则:
asm [volatile]("assembly code": output : input : clobber/modify):
如何根据对汇编指令的寄存器添加寄存器约束
建立端口读写接口函数
/* 8259A的控制是通过端口读写,很麻烦,程序通过内联汇编把常用的端口读写功能封装成 C 函数 */
#ifndef _LIB_IO_H
#define _LIB_IO_H
/* 向端口port写入一个字节 */
static inline void outb(uint16_t port, uint8_t data){
// outb 是一个汇编指令,用于将数据字节写入指定的端口
// %b0 表示用寄存器 "a" 中的低字节作为数据字节,%w1 表示用寄存器 "Nd(port)" 的值作为端口
asm volatile("outb %b0, %w1"::"a"(data), Nd(port));
}
/* 将 addr 处起始的 word_cnt 个字写入端口 port */
static inline void outwd(uint16_t port, const void* addr, int word_cnt){
asm volatile("cld;\
rep outsw": "+S"(addr), "+c"(word_cnt):"d"(port));
}
/* 从端口 port 读入的一个字节返回 */
static inline uint8_t inb(uint16_t port, uint8_t data){
asm volatile("inb %w1, %b0":"=a"(data):"Nb"(port));
return data;
}
/* 将从端口 port 读入的 word_cnt 个字写入 addr */
static inline void inwd(void *addr, uint16_t port, uint32_t word_cnt){
asm volatile("cld;\
rep insw":"+D"(addr), "+c"(word_cnt):"+d"(port):"memory"
);
}
#endif
(4)设置8259A控制器
/* 初始化可编程中断控制器 8259A */
static void pic_init (void){
/*初始化主片*/
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模式,正常EOI
/*初始化从片*/
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模式,正常EOI
/*打开主片的IR0,也就是目前只接受时钟产生的中断*/
outb(PIC_M_DATA, 0xfe);
outb(PIC_S_DATA, 0xff);
put_str("pic init done\n");
}
(5)加载 IDT ,开启中断
把中断描述符表 IDT 的信息加载到 IDTR 寄存器。
static struct gate_desc idt[IDT_DESC_CNT]; // idt 是中断描述符表,本质上就是个中断门描述符数组
.....
{
/* 利用lidt命令加载idt,高 48 位是 IDT 地址,低 16 位是 IDT 长度 */
uint64_t idt_operand = ((sizeof(idt)-1)) | ( (uint64_t)((uint32_t)idt << 16));
asm volatile("lidt %0"::"m"(idt_operand)) // m表示内存约束
}
(6)编译链接过程
编译、链接、写入磁盘的步骤如下。
gcc -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/main.o kernel/main.c ;-c 只进行编译而不进行链接; -fno-builtin表示禁止编译器使用内联函数
nasm -f elf -o build/print.o kernel/print.S ;-f elf 用于指定输出文件的格式
nasm -f elf -o build/kernel.o kernel/kernel.S
gcc -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/interrupt.o kernel/interrupt.c
gcc -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/init.o kernel/init.c
ld -Ttext 0xc0001500 -e main -o build/kernel.bin build/kernel.o build/interrupt.o \ build/init.o build/main.o build/print.o ;使用ld链接所有的.o文件,-e表示入口为main
dd if=build/kernel.bin of=/home/joy/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc // 刻录到img文件中
2.改进中断处理程序
方案:在汇编版本的 intrXXentry 中调用 C 语言版本的中断处理函数。在 C 语言中建立个目标中断处理函数数组 idt_table,数组元素是 C 版本的中断处理函数地址,供汇编语言中的 intrXXentry 调用。
总结:
**(1)**外部设备的中断信号并非直接交由CPU进行处理,而是现在中断控制器(如8359A)内进行中断屏蔽、中断优先级等过程[见下图7-12]判断后选出要执行的中断,8359A向CPU的INTR总线发送数据通知CPU,当CPU结束当前执行任务后,向8359A 发送INRA信号,然后8359A向INTR总线写入中断向量号。CPU根据中断向量号检索中断描述符表找到中断处理程序入口地址并执行[见图7-7]。
Q:中断描述符表的中断向量号是如何确定的?如何从8259A的中断接口号找到请求中断向量号?
ans:外设只负责发送中断信号,不给自己分配中断向量号。因此中断描述符表的中断向量号是8259A分配的。中断向量号不一定和8259A的IRQ接口号一致,因此支持用户自定义起始中断向量号,所以是起始中断向量号+IRQ接口号
(2)中断处理程序编写
中断处理函数数组(通过section .data构建中断处理函数数组,中断号为数组下标)
中断描述符表(构建中断门描述符结构体,建立结构体数组,每个结构体赋值)
初始化中断控制器PIC的主从片,按照ICW1 ICW2 ICW3 ICW4的顺序依次设置对应的数据线和控制线
Q:分页机制当一个页执行完了,如果缺页,找到它后面该执行的页呢?段描述符表中哪些和这个相关,除了段基地址还有什么和程序执行密切相关的?
**ANS:**当发生缺页,CPU会触发中断机制,从硬盘加载对应的页到内存