赞
踩
本篇对应书籍第七章的内容
本篇内容介绍了操作系统的中断处理机制,建立中断描述符表,填充门描述符,以及中断处理程序,初始化8259A中断控制器实现外部中断功能,控制8253定时计数器实现中断频率的提升
CPU 暂停正在运行的程序,转而去运行其他程序,处理完在回来执行刚才的程序,这个过程叫做中断处理,也叫中断。
操作系统是个死循环,在死循环的过程中,等待事情的发生,当有事情发生了,就会转而去处理这个事情,事情是通过中断来告知操作系统的。
操作系统是中断驱动的。
把中断按事件来源分类,来自 CPU 外部的中断就称为外部中断,来自 CPU 内部的中断称为内部中断。
外部中断按是否导致宕机来划分,可分为可屏蔽中断和不可屏蔽中断两种。
内部中断按是否正常来划分,可分为软中断和异常。
外部中断的中断源是硬件。外部硬件的中断通过两根信号线通知 CPU 的,分别是 INTR(INTeRrupt) 和 NMI(Non Maskable Interrupt):

在 Linux 中,可屏蔽中断分为上半部和下半部:
操作系统是中断驱动的,中断发生后会执行相应的中断处理程序,中断处理程序中需要立即执行的部分在上半部,完成中断应答或硬件复位等重要紧迫工作。中断处理程序中不紧急的部分则被推迟到下半部中去完成。上半部是在关中断的情况下执行,不可被打扰,下半部则不是。
中断发起时,相应的中断向量号通过 NMI 或 INTR 引脚被传入 CPU,中断向量号是中断向量表或中断描述符表里中断项的下标,CPU 根据此中断向量号在中断向量表或中断描述符表中检索对应的中断处理程序并去执行。
中断向量表、中断描述符表中存储的是中断号以及中断号对应的处理程序的位置
内部中断可分为软中断和异常。
软中断就是由硬件主动发起的中断,因为它来自于软件,所以称之为软中断。
发起中断的指令:
int 8位立即数,一般用于系统调用。int3,调试断点指令,其所触发的中断向量号是 3。我们用 gdb 或 bochs 调试程序时,实际上就是调试器 fork 了一个子进程,用于执行被调试的程序。调试器中经常要设置断点,其原理就是父进程修改了子进程的指令,将其用 int3 指令替换,从而子进程调用 int3 触发中断。into,中断溢出指令,中断向量号是 4。bound,检查数组索引越界指令,触发 5 号中断。ud2,未定义指令,触发 6 号中断。除了int 8位立即数以外的中断也可以算作是异常,异常是指令执行期间 CPU 内部产生的错误引起的,不受 eflags 里的 IF 位约束(只要中断关系到正常运行,就不受 IF 位影响)。
异常按照轻重程度可分为以下三种:
处理器所支持的 256 种中断:

最左边一列就是中断向量号,类似段选择子,不过中断向量号是从中断描述符表中索引中断描述符,其中没有RPL字段,中断号来源:
中断描述符表(Interrupt Descriptor Table,IDT)是保护模式下用于存储中断处理程序的表,实模式下是中断向量表,他们的区别在于:
中断描述符表不仅有中断描述符,还有陷阱门、中断门等描述符,中断描述符表中的每个描述符都可以叫做,门
段描述符描述的是一段内存区域,门描述符描述的是一段代码,描述符大小都是8字节,门描述符中也有属性,门描述符都属于系统段,S 都为 0,type 不一样,重新回顾一下这几个门的作用:
任务门
0101。
中断门
1110。
陷阱门
1111。
调用门
1100。
现代操作系统很少用到任务门和调用门,主要用到的是中断门,和陷阱门
IDT 同 GDT 一样,CPU 硬件上提供了存储其位置的寄存器,中断描述符表寄存器 IDTR:

前 16 位是界限,后 32 位是基址,理论上可以有 64KB/8KB=8192 个描述符,但 CPU 只支持 256个
加载指令:lidt 48位内存数据
完整的中断过程分为 CPU 外和 CPU 内两部分:
CPU 内的过程:
处理器根据中断向量号定位中断门描述符。
处理器进行特权级检查。
对于软件发起的软中断,当前特权级 CPL 必须位于 门描述符 DPL 和门中目标代码段描述符 DPL 之间(特权比门高,才能使用门,特权比处理程序低,才能使用门调用处理程序,特权转移只能从低到高进行)。
对于外部设备引起的中断和异常,则只检查 CPL 和目标代码段 DPL ,CPL 权限要小于 DPL 才行。
执行中断处理程序。

中断发生后,eflags 中的 NT 位和 TF 位会被置零,如果是中断门,则 IF 位也置零
TF 位:Trap Flag,陷阱标志位,用在调试环境中,TF 为 0 的时候,禁止单步执行
NT 位:Nest Task Flag,任务嵌套标志位,用来标记任务嵌套调用情况,用于在当前任务中中断进行新的任务,进行完之后再回来完成当前任务的场景
从中断返回的指令是 iret,从栈中弹出数据到寄存器 cs、eip、eflags 等,根据特权级是否改变决定是否要恢复旧栈
处理器提供了专门控制 IF 位的指令:
cli使 IF 位置 0sli使 IF 位置 1压栈操作如图所示,出栈则是按照压栈的反方向进行的
如果有中断错误码,处理器并不会主动跳过它的位置,必须手动将其跳过,在准备用 iret 指令返回时,当前栈指针 esp 必须指向栈中备份的 EIP_old 所在的位置。


处理器在中断结束后返回的过程中还要进行一次特权级检查:
从 CS_old 和 EIP_old 中检查 RPL 判断是否有特权级变化
如果检查通过,则更新 cs 和 eip
如果没涉及特权级变化,则当前栈指针还是 esp_old,用的还是旧栈
将 eflags 弹出到标志寄存器
如果需要改变特权级,则直接恢复旧栈
中断错误码用来指明中断发生在哪个段上。

通常能够压入错误码的中断属于中断向量号 0 ~ 32 之内的异常,而外部中断(32 ~ 255)和 int 软中断并不会产生错误码。
通常我们不用处理错误码。
8259A 用于管理和控制可屏蔽中断,它表现在屏蔽外设中断,对它们实行优先级判决,向 CPU 提供中断向量号等功能。
一片 8259A 只能管理 8 个中断,通过级联芯片可以支持 7n+1 个中断源。
8259A 芯片的内部结构:

8259A 芯片收到中断信号之后,芯片内部的中断屏蔽寄存器 IMR 会判断该信号是否屏蔽(编程控制),如果屏蔽就丢弃信号,否则送入中断请求寄存器 IRR,IRR 相当于待处理中断队列,时机成熟时,优先级判别器 PR 会从中选择优先级高的中断,通过控制电路 INT 接口向 CPU 发送 INTR 信号;
CPU 处理完成后,通过自己的 INTA 接口向 8259A 的 INTA 接口回复一个响应信号,8259A 收到信号之后,立即将中断服务寄存器 ISR 中对应刚才选择的中断的位设置为 1,表示当前正在处理该中断,同时将该中断从 IRR 队列中去掉,之后 CPU 再次发送 INTA 信号给 8259A,8259A 将中断向量号(编程控制)通过系统数据总线发给 CPU ,CPU 进行执行中断处理程序。
硬件程序是固定的,可编程指的是我们可以控制硬件程序提供的输入和输出,CPU 提供了中断处理的框架,我们只需要提供 CPU 所需要的输入即可让 CPU 自动完成工作,需要的数据是:
我们只需要在外部设备中设置好中断向量号,然后在中断描述符表中设置好对应的中断处理程序即可。
外部连接的硬件也是固定的:

8259A 内部有两组寄存器,一组是初始化命令寄存器组,用来保存初始化命令字(ICW),ICW 共 4 个,ICW1 ~ ICW4。另一组是操作命令寄存器,用来保存操作命令字(OCW),OCW 共 3 个,OCW1 ~ OCW3。
对 8259A 的编程,也分为初始化和操作两部分:
ICW1 用来初始化 8259A 的连接方式和中断信号的触发方式。ICW1 需要写入主片的 0x20 端口和从片的 0xA0 端口。

ICW2 用来设置起始中断向量号。ICW2 需要写入到主片的 0x21 端口和从片的 0xA1 端口。

只需要设置 IRQ0 的中断向量号,剩下的就会依次顺延
ICW3 仅在级联的方式下才需要,用来设置主片和从片用哪个 IRQ 接口互连。ICW3 需要写入主片的 0x21 端口及从片的 0xA1 端口。
主片和从片的 ICW3 结构不同。


ICW4 用于设置 8259A 的工作模式,ICW3 需要写入主片的 0x21 端口及从片的 0xA1 端口。

OCW1 用来屏蔽连接在 8259A 上的外部设备的中断信号,某位为 1,对应的 IRQ 上的中断信号就屏蔽了。OCW 要写入主片的 0x21 或从片的 0xA1 端口。

OCW2 用来设置中断结束方式和优先级模式。OCW2 写入主片的 0x20 及从片的 0xA0 端口。
OCW2 其中一个作用是发 EOI 信号结束中断。

OCW2 高位可以组合出多种不同的结束方式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nHt8PqoB-1674189947520)(E:\操作系统真象还原\note\picture\第七章\ICW2组合.png)]

OCW3:这里用不上
Intel 8259A 芯片位于主板的南桥芯片上,8259A 与外设的连接是内部电路实现了的,直接操作即可使用。
流程:
这里用到了汇编的宏 macro,宏是用来代替重复性输入的,格式如下:
%macro mul_add 3 ;宏声明 宏名称 宏参数
mov eax, %1 ;参数1
add eax, %2 ;参数2
add eax, %3 ;参数3
%endmacro
;调用如下:
mul_add 12, 23, 34
中断处理程序如下:
这一段代码通过宏创建了 intr_entry_table 数组(公开的成员),成员是33个中断处理程序的地址
[bits 32] %define ERROR_CODE nop ;为了栈中格式统一,如果 CPU 在异常中已经自动压入错误码,这里不做操作 %define ZERO push 0 ;为了栈中格式统一,如果 CPU 在异常中没有自动压入错误码,这里填充 0 extern put_str ;声明外部函数,告诉编译器在链接的时候可以找到 section .data intr_str db "interrupt occur!", 0xa, 0 global intr_entry_table intr_entry_table: %macro VECTOR 2 section .text intr%1entry: ;每个中断处理程序都要压入中断向量号,所以1个中断类型1个处理程序,自己知道自己的中断号是多少 %2 push intr_str call put_str add esp, 4 ;如果从片上进入中断,除了往片上发送 EOI 外,还要往主片上发送 EOI,因为后面要在 8259A 芯片上设置手动结束中断,所以这里手动发送 EOI mov al, 0x20 ;中断结束命令 EOI out 0xa0, al ;往从片发送 out 0x20, al ;往主片发送 add esp, 4 iret section .data ;这个 section .data 的作用就是让数组里全都是地址,编译器会将属性相同的 Section 合成一个大的 Segmengt,所以这里就是紧凑排列的数组了 dd intr%1entry ;存储各个中断入口程序的地址,形成 intr_entry_table 数组 %endmacro VECTOR 0x00, ZERO VECTOR 0x01, ZERO VECTOR 0x02, ZERO VECTOR 0x03, ZERO VECTOR 0x04, ZERO VECTOR 0x05, ZERO VECTOR 0x06, ZERO VECTOR 0x07, ZERO VECTOR 0x08, ZERO VECTOR 0x09, ZERO VECTOR 0x0a, ZERO VECTOR 0x0b, ZERO VECTOR 0x0c, ZERO VECTOR 0x0d, ZERO VECTOR 0x0e, ZERO VECTOR 0x0f, ZERO VECTOR 0x10, ZERO VECTOR 0x11, ZERO VECTOR 0x12, ZERO VECTOR 0x13, ZERO VECTOR 0x14, ZERO VECTOR 0x15, ZERO VECTOR 0x16, ZERO VECTOR 0x17, ZERO VECTOR 0x18, ZERO VECTOR 0x19, ZERO VECTOR 0x1a, ZERO VECTOR 0x1b, ZERO VECTOR 0x1c, ZERO VECTOR 0x1d, ERROR_CODE VECTOR 0x1f, ZERO VECTOR 0x20, ZERO VECTOR 0x21, ZERO

代码中略的部分(书上也写了略),会在后面小节中补充上来
这一段代码创建了中断描述符结构体,使用了一个函数来填充这个结构体,用了另一个函数来将各个中断描述符填充到中断描述符表中去,最后通过idt_init()来进行调用:
#include "interrupt.h" #include "global.h" #include "stdint.h" #include "io.h" #include "print.h" /*略*/ #define IDT_DESC_CNT 0x21 // 目前总共支持的中断数 /*中断门描述符结构体*/ struct gate_desc { uint16_t func_offset_low_word; // 中断处理程序偏移量低16位 uint16_t selector; // 中断处理程序目标代码段选择子 uint8_t dcount; // 此项位双字计数字段,是门描述符第四字节,是固定值 uint8_t attribute; // type属性 + S + DPL + P uint16_t func_offset_high_word; // 中断处理程序偏移量高16位 }; // 静态函数声明,非必须 // intr_handler 实际上是 void* 在 interrupt.h 里定义的 static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function); static struct gate_desc idt[IDT_DESC_CNT]; // idt 本质上就是个中断门描述符数组 extern intr_handler intr_entry_table[IDT_DESC_CNT]; // 声明引用在 kernel.asm 中的中断处理函数入口数组 /*创建中断门描述符*/ // 参数:中断描述符,属性,中断处理函数地址 // 功能:向中断描述符填充属性和地址 static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) { p_gdesc->func_offset_low_word = (uint32_t) function & 0x0000FFFF; // 0000FFFF = 1111 1111 1111 1111,即将前面全置0 p_gdesc->selector = SELECTOR_K_CODE; p_gdesc->dcount = 0; p_gdesc->attribute = attr; p_gdesc->func_offset_high_word = ((uint32_t) function & 0xFFFF0000) >> 16; } /*初始化中断描述符表*/ static void idt_desc_init(void) { int i; for (i = 0; i < IDT_DESC_CNT; i++) { make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]); // IDT_DESC_DPL0在global.h定义的 } put_str(" idt_desc_init done\n"); } /*完成有关中断的所有初始化工作*/ void idt_init() { put_str("idt_init_start\n"); idt_desc_init(); // 初始化中断描述符表 pic_init(); // 初始化 8259A /* 加载 idt, idt = 32 位表基址 + 16位表界限*/ 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\n"); }
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;
void idt_init(void);
#endif
#ifndef _KERNEL_GLOBAL_H #define _KERNEL_GLOBAL_H #include "stdint.h" #define RPL0 0 #define RPLl 1 #define RPL2 2 #define RPL3 3 #define TI_GDT 0 #define TI_LDT 1 #define SELECTOR_K_CODE ((1 << 3) + (TI_GDT << 2) + RPL0) #define SELECTOR_K_DATA ((2 << 3) + (TI_GDT << 2) + RPL0) #define SELECTOR_K_STACK SELECTOR_K_DATA #define SELECTOR_K_GS ((3 << 3) + (TI_GDT << 2) + RPL0) /*-------------- IDT描述符属性 ------------*/ #define IDT_DESC_P 1 #define IDT_DESC_DPL0 0 #define IDT_DESC_DPL3 3 #define IDT_DESC_32_TYPE 0xE //32位的门 #define IDT_DESC_16_TYPE 0x6 //16位的门,用不到 #define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P << 7) + (IDT_DESC_DPL0 << 5) + IDT_DESC_32_TYPE) #define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE) #endif
到此,和中断相关的数据(中断描述符表,中断向量号)都准备好了,接下来只要把 8259A 设置好即可
这里先把常用的端口IO操作封装成函数,方便以后调用
这里封装了 4 个函数:
void outb(uint16_t port, uint8_t data)void outsw(uint16_t port, const void* addr, uint32_t word_cnt)uint8_t inb(uint16_t port)void insw(uint16_t port, void* addr, uint32_t word_cnt)static 表示作用域在本文件内,需要调用需要把本文件包含进入,会导致文件体积增大
加了 inline 关键字,函数会在调用处原地展开,编译后的代码不包含call,也就是不属于函数调用了,减少了函数调用相关 的工作,提升了工作效率
牺牲体积来增加运行速度还是不错的。
/************** 机器模式 *************** b -- 输出寄存器QImode名称,即寄存器中的最低8位:[a-d]l。 w -- 输出寄存器HImode名称,即寄存器中2个字节的部分,如[a-d]x。 HImode “Half-Integer”模式,表示一个两字节的整数。 QImode “Quarter-Integer”模式,表示一个一字节的整数。 *******************************************/ #ifndef __LIB_IO_H #define __LIB_IO_H #include "stdint.h" /* 向端口port写入一个字节 */ static inline void outb(uint16_t port, uint8_t data) { /********************************************************* 对端口指定 N 表示0-255, d表示用dx存储端口号, %b0表示对应al,%w1表示对应dx */ asm volatile ("outb %b0, %w1" : : "a"(data), "Nd"(port)); /******************************************************/ // 这里是 AT&T 语法的汇编语言,相当于: mov al. data // mov dx, port // out dx, al } /* 将addr处起始的word_cnt个字写入端口port */ static inline void outsw(uint16_t port, const void* addr, uint32_t word_cnt) { /********************************************************* + 表示此限制既做输入,又做输出, outsw 是把 ds:esi 处的 16 位的内容写入 port 端口, 我们在设置段描述符时,已经将ds,es,ss段的选择子都设置为相同的值了, 此时不用担心数据错乱 */ asm volatile ("cld; rep outsw" : "+S"(addr), "+c"(word_cnt) : "d"(port)); /*********************************************************/ // 这里是 AT&T 语法的汇编语言,相当于: cld // mov esi, addr // mov ecx, word_cnt // mov edx, port } /* 将从端口 port 读入一个字节返回 */ static inline uint8_t inb(uint16_t port) { uint8_t data; asm volatile ("inb %w1, %b0" : "=a"(data) : "Nd"(port)); return data; } /* 将从端口 port 读入的 word_cnt 个字写入 addr */ static inline void insw(uint16_t port, void* addr, uint32_t word_cnt) { // insw 是将从端口 port 处读入的 16 位内容写入 es:edi 指向的内存 asm volatile ("cld; rep insw" : "+D"(addr), "+c"(word_cnt) : "d"(port) : "memory"); } #endif
8259A 的编程就是写入 ICW 和 OCW,其中
因为硬盘是接在了从片的引脚上,将来实现文件系统是离不开硬盘的,所以我们这里使用的8259A要采用主、从片级联的方式。
在x86系统中,对于初始化级联8259A, 4个ICW都需要,必须严格按照ICW1~4顺序写入。
写入端口:
0x20(主片)或0xA0(从片)写入。0x21(主片)或0xA1(从片)写入。这里的内容就是上次写的这个文件中,“略”的部分,这里把对 8259A 芯片的初始化操作加了进去。
//第一个“略”处 #include "io.h" #define PIC_M_CTRL 0x20 //主片 #define PIC_M_DATA 0x21 #define PIC_S_CTRL 0xA0 //从片 #define PIC_S_DATA 0xA1 //第二个“略”处 /* 初始化可编程中断控制器 8259A */ static void pic_init(void){ //初始化主片 outb(PIC_M_CTRL, 0x11); // ICW1: 0001 0001 ,边沿触发,级联 8259,需要ICW4 outb(PIC_M_DATA, 0x20); // ICW2: 0010 0000 ,起始中断向量号为 0x20(0x20-0x27) outb(PIC_M_DATA, 0x04); // ICW3: 0000 0100 ,IR2 接从片 outb(PIC_M_DATA, 0x01); // ICW4: 0000 0001 ,8086 模式,正常EOI //初始化从片 outb(PIC_S_CTRL, 0x11); // ICW1: 0001 0001 ,边沿触发,级联 8259,需要ICW4 outb(PIC_S_DATA, 0x28); // ICW2: 0010 1000 ,起始中断向量号为 0x28(0x28-0x2f) outb(PIC_S_DATA, 0x02); // ICW3: 0000 0010 ,设置连接到主片的 IR2 引脚 outb(PIC_S_DATA, 0x01); // ICW4: 0000 0001 ,8086 模式,正常EOI //打开主片上的 IR0 也就是目前只接受时钟产生的中断 //eflags 里的 IF 位对所有外部中断有效,但不能屏蔽某个外设的中断了 outb (PIC_M_DATA, 0xfe); outb (PIC_S_DATA, 0xff); put_str(" pic init done\n"); }
pic_init()函数在最后的idt_init中调用
这一段代码之前写了,现在完善了相关函数,再来回顾一下:
初始化中断描述符表和 8259A 芯片之后,通过lidt命令加载 IDT,开启中断机制。
/*完成有关中断的所有初始化工作*/
void idt_init(){
put_str("idt_init start\n");
idt_desc_init(); //初始化中断描述符表
pic_init(); //初始化 8259A
/*加载 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\n");
}
用一个函数专门来启动模块,以后添加新的模块了也添加到这里来启动:
#include "init.h"
#include "print.h"
#include "interrupt.h"
/* 负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); // 初始化中断
}
为了让 main.c 调用 init_all 函数,所以建立一个 init.h
#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all(void);
#endif
#include "print.h"
#include "init.h"
int main() {
put_str("I am kernel\n");
init_all();
asm volatile("sti"); // 为演示中断处理, 在此临时开中断
while (1);
return 0;
}
使用sti指令开启中断(sti指令的作用是将 IF 位 set 为 1)
完整代码:


为了文件目录的整洁,将所有的目标文件和编译后的内核文件都放在 build 目录下:
nasm -f elf -o build/print.o lib/kernel/print.asm
nasm -f elf -o build/kernel.o kernel/kernel.asm
gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/main.o kernel/main.c
gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/interrupt.o kernel/interrupt.c
gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/init.o kernel/init.c
ld -m elf_i386 -Ttext=0xc0001500 -e main -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o
gcc 里用到的新的参数:
-fno-builtin 处理内建函数
-I 参数要把所有相关文件的目录都选上
运行 Bochs:

在 Bochs 里使用 info idt 查看当前 IDT:

这里先来回顾一下到此为止所学的内容:
中断处理机制是怎么一个流程:
首先,中断分为软件中断和硬件中断:
得到中断向量号之后,CPU 通过向 IDTR 寄存器查询中断描述符表 IDT 的地址
通过中断向量号索引当前中断在 IDT 中的位置,也就是门描述符,从中获取该中断响应的中断处理程序的地址
跳转到中断处理程序去执行,通过 iret 返回
代码文件分别是做什么的:
interrupt.c 是中断的主要初始化文件,初始化了 PIC 和 IDT
global.h 定义了门描述符和段选择子
io.h 封装了对端口的读写函数
init.c 将 interrupt.c 封装好的初始化程序再次封装,供 main.c 调用
中断是如何开启的:
开启中断准备工作分两部分:
准备工作结束后,通过设置 IF 位来开中断
之前中断处理程序都是汇编写的,写起来太麻烦,可以选择用 C 来编写
在 C 语言中建立中断处理函数数组 idt_table,数组元素是 C 版本的中断处理函数地址,供汇编中的 intrXXentry 使用
这就只需要在中断入口程序中,让中断向量号*4,加上 C 语言数组 idt_table 地址索引到对应的中断处理函数,就可以调用C语言的中断处理函数了。

在这里添加如下代码:
//添加两个声明 char* intr_name[IDT_DESC_CNT]; // 用于保存异常的名字 intr_handler idt_table[IDT_DESC_CNT]; // 用于保存处理程序地址 ;;; //在idt_init()前添加: /*通用的中断处理请求*/ static void general_intr_handler(uint8_t vec_nr){ if(vec_nr == 0x27 || vec_nr == 0x2f){ // IRQ7 IRQ15 会产生伪中断,无需处理 // 0x2f 是从片 8259A 上的最后一个 IRQ 引脚,保留项 return ; } put_str("int vector : 0x"); put_int(vec_nr); put_char('\n'); } /*完成一般中断处理函数注册及异常名称注册*/ static void exception_init(void){ int i; for(i = 0;i < IDT_DESC_CNT; i++){ // idt_table 数组中的函数是在进入中断后根据中断向量号调用的 // 见 kernel.S 的 call [idt_table = %1*4] idt_table[i] = general_intr_handler; // 以后用register_handler 来注册具体的处理函数 intr_name[i] = "unknown"; } intr_name[0] = "#DE Divide Error"; intr_name[1] = "#DB Debug Exception"; intr_name[2] = "NMI Interrupt"; intr_name[3] = "#BP Breakpoint Exception"; intr_name[4] = "#OF Overflow Exception"; intr_name[5] = "#BR BOUND Range Exceeded Exception"; intr_name[6] = "#UD Invalid Opcode Exception"; intr_name[7] = "#NM Device No七 Available Exception"; intr_name[8] = "JIDF Double Fault Exception"; intr_name[9] = "Coprocessor Segment Overrun"; intr_name[10] = "#TS Invalid TSS Exception"; intr_name[11] = "#NP Segment Not Present"; intr_name[12] = "#SS Stack Fault Exception"; intr_name[13] = "#GP General Protection Exception"; intr_name[14] = "#PF Page-Fault Exception"; // intr_name[l5]第15项是intel保留项,未使用 intr_name[16] = "#MF x87 FPU F'loating-Point Error"; intr_name[17] = "#AC Alignment Check Exception"; intr_name[18] = "#MC Machine-Check Exception"; intr_name[19] = "#XF SIMD Floating-Point Exception"; } /*完成有关中断的所有初始化工作*/ void idt_init(){ put_str("idt_init start\n"); idt_desc_init(); //初始化中断描述符表 exception_init(); //初始化异常名称并注册通用处理程序 pic_init(); //初始化 8259A /*加载 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\n"); }
这里是创建了通用的中断处理请求函数,初始化中断处理函数为通用函数,然后初始化函数名称。
接下来只需要让 kernel.S 里的中断描述符中的地址指向 idt_table 中的地址即可
[bits 32] %define ERROR_CODE nop ; 若在相关的异常中cpu已经自动压入了错误码, 为保持栈中格式统一, 这里不做操作 %define ZERO push 0 ; 为了栈中格式统一, 如果 CPU 在异常中没有自动压入错误码, 这里填充 0 extern idt_table ; 声明 c 注册的中断处理函数数组 section .data ; intr_entry_table位于data段, 之后会和宏中的data段组合在一起(注意: 宏中的text段与intr_entry_table不是同一个段) global intr_entry_table intr_entry_table: ;--------------- 宏 VECTOR 开始, 参数数目为2, 第一个参数为中断号, 第二个参数为该中断对 ERROR_CODE 的操作 --------------- %macro VECTOR 2 section .text intr%1entry: ; 每个中断处理程序都要压入中断向量号, 所以1个中断类型1个处理程序, 自己知道自己的中断号是多少, %1: 调用宏时的第一个参数 %2 ; %2: 调用宏时的第二个参数, 有错误码时什么都不做, 没有时压入 0 使格式统一 ; 保存上下文环境 push ds push es push fs push gs pushad ; 如果从片上进入中断, 除了往从片上发送 EOI 外,还要往主片上发送 EOI, 因为后面要在 8259A 芯片上设置手动结束中断, 所以这里手动发送 EOI mov al, 0x20 ; 中断结束命令 EOI, 0x20 = 0010 0000, 第五位为EOI位 out 0xa0, al ; 往从片发送 out 0x20, al ; 往主片发送 push %1 ; 不管中断处理程序是否需要, 一律压入中断向量号 call [idt_table + %1*4] ; 调用中断处理程序 jmp intr_exit section .data ; 这个 section .data 的作用就是让数组里全都是地址, 编译器会将属性相同的 Section 合成一个大的 Segmengt, 所以这里就是紧凑排列的数组了 dd intr%1entry ; 存储各个中断入口程序的地址, 形成 intr_entry_table 数组 %endmacro ;---------------宏 VECTOR 结束--------------- section .text global intr_exit intr_exit: ; 恢复上下文环境 add esp, 4 ; 跳过参数中断号 popad pop gs pop fs pop es pop ds add esp, 4 ; 手动跳过错误码 iretd VECTOR 0x00, ZERO VECTOR 0x01, ZERO VECTOR 0x02, ZERO VECTOR 0x03, ZERO VECTOR 0x04, ZERO VECTOR 0x05, ZERO VECTOR 0x06, ZERO VECTOR 0x07, ZERO VECTOR 0x08, ERROR_CODE VECTOR 0x09, ZERO VECTOR 0x0a, ERROR_CODE VECTOR 0x0b, ERROR_CODE VECTOR 0x0c, ZERO VECTOR 0x0d, ERROR_CODE VECTOR 0x0e, ERROR_CODE VECTOR 0x0f, ZERO VECTOR 0x10, ZERO VECTOR 0x11, ERROR_CODE VECTOR 0x12, ZERO VECTOR 0x13, ZERO VECTOR 0x14, ZERO VECTOR 0x15, ZERO VECTOR 0x16, ZERO VECTOR 0x17, ZERO VECTOR 0x18, ERROR_CODE VECTOR 0x19, ZERO VECTOR 0x1a, ERROR_CODE VECTOR 0x1b, ERROR_CODE VECTOR 0x1c, ZERO VECTOR 0x1d, ERROR_CODE VECTOR 0x1e, ERROR_CODE VECTOR 0x1f, ZERO VECTOR 0x20, ZERO

这里主要是修改了宏,现在的宏是先保存上下文环境,然后入栈中断向量号调用 C 语言中 idt_table 相应的处理程序,调用完之后,还原上下文环境,从中断返回。
interrupt.c完整代码:

编译,链接,写入硬盘:还是刚才的那一套操作:

计算机上的时钟可以分为两类:内部时钟和外部时钟
外部时钟和内部时钟是两套独立运行的定时体系。
定时器和计数器实际上是一回事,都是在做计时的功能,也就是到指定时间后发信号给 CPU
8253 定时/计数器是通过倒计时的方式定时,需要先设置一个初始值,每隔一个时钟周期减去1,减到0就给CPU发送信号,然后重新初始化
8253 芯片的计数器内部有3个主要部件:全都是16位宽的
8253 芯片的每隔计数器都有3个引脚:

8253 芯片内部有3个计数器,工作相互独立,互不影响,作用和端口如图所示:

端口0x43是控制字寄存器,功能如图所示:


计数器启动的条件:
启动类型:
停止类型:
六种工作方式:

让 8253 开始工作的方法比 8259A 简单多了:
#ifndef __DEVICE_TIME_H
#define __DEVICE_TIME_H
#include "stdint.h"
void timer_init(void);
#endif
#include "timer.h" #include "io.h" #include "print.h" #define IRQ0_FREQUENCY 100 // IRQ0 频率 #define INPUT_FREQUENCY 1193180 // 8253input频率 #define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY // IRQ0计数初值 #define COUNTER0_PORT 0x40 // 计数器端口 #define COUNTER0_NO 0 // 控制字中使用的计数器号码 #define COUNTER_MODE 2 // 计数器工作方式 #define READ_WRITE_LATCH 3 // 读写方式, 先读写低8位, 再读写高8位 #define PIT_CONTROL_PORT 0x43 // 控制字寄存器端口 /* 把操作的计数器counter_no、读写锁属性rwl、计数器模式counter_mode写入模式控制寄存器井赋予初始值counter_value */ static void frequency_set(uint8_t counter_port, uint8_t counter_no, uint8_t rwl, uint8_t counter_mode, uint16_t counter_value) { // 往控制字寄存器端口 0x43 写入控制字 outb(PIT_CONTROL_PORT, (uint8_t) (counter_no << 6 | rwl << 4 | counter_mode << 1)); // 先写入低 8 位 outb(counter_port, (uint8_t) counter_value); // 再写入高8位 outb(counter_port, (uint8_t) counter_value >> 8); } /* 初始化 PIT8253 */ void timer_init() { put_str("timer_init start\n"); // 设置8253的定时周期, 即发送中断的周期 frequency_set(COUNTER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE); put_str("timer_init done\n"); }

#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
/* 负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); // 初始化中断
timer_init(); // 初始化 PIT
}
编译、链接、写入硬盘:
nasm -f elf -o build/print.o lib/kernel/print.asm
nasm -f elf -o build/kernel.o kernel/kernel.asm
gcc -m32 -I lib/kernel/ -c -o build/timer.o device/timer.c
gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/main.o kernel/main.c
gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/interrupt.o kernel/interrupt.c
gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/init.o kernel/init.c
ld -m elf_i386 -Ttext=0xc0001500 -e main -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o build/timer.o
运行:

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。