配合视频学习效果更佳:https://www.bilibili.com/video/BV1Nk4y1s7Qg/?vd_source=701807c4f8684b13e922d0a8b116af31
代码仓库地址:https://github.com/xukanshan/the_truth_of_operationg_system
中断就是发生了事情通知CPU,但是处不处理就看情况。中断机制的本质是来了一个中断信号后,cpu去处理,然后调用相应的中断处理程序。
外部中断:
来自CPU外部的中断,因为外部中断源必须是某个硬件,所以也叫硬件中断。CPU为外部中断提供了两条信号线,如下图:
-
从INTR引脚接收到的中断信号都是不影响系统运行的,可以随时处理,所以是可屏蔽的,也叫可屏蔽中断,每个中断都有中断向量号。
-
从NMI引脚,全是对系统运行有致命伤害的,所以是不可屏蔽的,自然叫不可屏蔽中断,共用一个中断向量号,为2。
-
Linux把可屏蔽中断分为上半部分和下半部分,上半部分需要立即执行,下半部分可以推迟执行,例子见书p300。
内部中断:
- 软中断和异常,软中断是软件主动发起的。
- 异常是指令执行期间CPU内部产生的错误引起的,不可屏蔽。异常按照轻重程度分为:Fault(故障,可以被修复,比如缺页异常page fault),Trap(陷阱,软件掉进了CPU设下的陷阱,比如调试过程中用的int3),Abort(终止,一旦发生,操作系统为了自保只能将此程序从进程表中去掉)
异常和不可屏蔽中断的中断向量号是由CPU自动提供的,而来自外部设备的可屏蔽中断号是由中断代理提供的,软中断是由软件提供的。
当CPU接收到一个中断时,需要用中断向量在中断段描述符表中检索对应的描述符(中断门描述符),在该描述符中找到中断处理程序的起始地址(一个段描述符选择子与偏移),然后执行中断处理程序。
计算机为了实现对中断的高效管理,而引入了中断控制器,由它负责接收外部设备的中断,负责对所有中断进行仲裁,决定哪个中断优先被CPU受理。
接下来我们来实现一个简陋的时钟中断。大致流程如下图:
init_all函数用来初始化所有的设备及数据结构,我们打算在主函数中调用它来完成初试化工作。init_all首先调用idt_init,它用来初始化中断相关的内容,其初始化也要分成几部分来做,pic_init用来初始化可编程中断控制器8259A,idt_desc_init用来初始化中断描述符表IDT,最后再加载IDT。
本节代码核心逻辑:
- 创建33个中断处理函数
- 写函数构建中断描述符表
- 写函数初始化中断控制器8295A,并只打开时钟中断
- 把2和3封装进入中断始化函数
idt_init
,调用idt_init
函数完成中断描述符表初始化与中断控制器初始化,并加载idtr寄存器的值 - 把4封装进入总初始化函数
init_all
,调用这个函数完成中断初始化 - 在main中打开中断测试
首先我们先写好中断发生后的中断处理程序
p320剖析kernel.S代码:
1、代码功能
创建33个中断处理函数
2、实现原理
中断信号进入中断控制器进行处理之后,会被分配中断号,通过特定的中断号码,可以调用特地的中断处理程序去处理。0—19中断号为处理器内部固定的异常类型,20-31是Intel保留的。同时为了演示中断机理,写一个时钟中断处理程序,所以共33个。
3、代码逻辑
定义33个中断处理程序,每个程序包含处理部分与本程序的地址
4、怎么写代码?
A、定义没有压入错误码但为了统一管理需要压入0的宏参数;定义要压入错误码所以我们什么都不做的宏参数
B、定义一个文本段,里面放着要打印的字符串信息,然后定义一个标号(就在文本段下方)。由于编译器的特性,会将同一类型的SECTION组合成一个大的SEGMENT,所以D中调用宏所形成的每个中断处理程序中的入口地址部分(这个入口地址会被定义成文本段)会统一聚集在这个要打印的字符串这里(因为它是被定义成了文本段),也就是字符串信息下面的标号处,于是这个标号便可以管理所有的中断处理程序地址
C、定义一个中断处理程序宏,宏中包含程序段:程序处理部分(打印字符串信息)、文本段:本程序的入口地址部分
D、调用C定义的宏实现33个中断处理程序的定义(传不同的参数),要理清楚哪些中断要压入错误码,哪些中断不会压入错误码。不压入错误码的我们压入一串0,这样能实现中断统一定义(p303表7-1)
5、代码实现如下: myos/kernel/kernel.S
[bits 32]
%define ERROR_CODE nop ; 有些中断进入前CPU会自动压入错误码(32位),为保持栈中格式统一,这里不做操作.
%define ZERO push 0 ; 有些中断进入前CPU不会压入错误码,对于这类中断,我们为了与前一类中断统一管理,就自己压入32位的0
extern put_str ;声明外部函数,为的是调用put_strsection .data
intr_str db "interrupt occur!", 0xa, 0 ;第二个是一个换行符,第三个定义一个ascii码为0的字符,用来表示字符串的结尾
global intr_entry_table
intr_entry_table: ;编译器会将之后所有同属性的section合成一个大的segment,所以这个标号后面会聚集所有的中断处理程序的地址%macro VECTOR 2 ;汇编中的宏用法见书p320
section .text ;中断处理函数的代码段
intr%1entry: ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少,此标号来表示中断处理程序的入口%2 ;这一步是根据宏传入参数的变化而变化的push intr_strcall put_stradd esp,4 ; 抛弃调用put_str压入的字符串地址参数; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI mov al,0x20 ; 中断结束命令EOIout 0xa0,al ;向主片发送OCW2,其中EOI位为1,告知结束中断,详见书p317out 0x20,al ;向从片发送OCW2,其中EOI位为1,告知结束中断add esp,4 ;对于会压入错误码的中断会抛弃错误码(这个错误码是执行中断处理函数之前CPU自动压入的),对于不会压入错误码的中断,就会抛弃上面push的0iret ; 从中断返回,32位下等同指令iretdsection .data ;这个段就是存的此中断处理函数的地址dd intr%1entry ; 存储各个中断入口程序的地址,形成intr_entry_table数组,定义的地址是4字节,32位
%endmacroVECTOR 0x00,ZERO ;调用之前写好的宏来批量生成中断处理函数,传入参数是中断号码与上面中断宏的%2步骤,这个步骤是什么都不做,还是压入0看p303
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
6、其他代码详解查看书p321;以上代码的核心就是33个中断处理函数,并且这些中断处理函数的入口地址形成了一个数组。
然后我们进行IDT表建构工作,核心就是为上文写下的中断处理函数建立对应的中断描述符表。下图是中断描述符的结构,字段含义参考段描述符P152
p324剖析interrupt.c代码:
1、代码功能
构建IDT表,为上文写下的中断处理函数建立对应的中断描述符表
2、实现原理
依据中断描述符表格式,将中断描述符表与中断处理函数建立映射
3、代码逻辑
创建33个中断门描述符结构体,然后通过循环将中断门描述符与特定中断处理函数建立映射
4、怎么写代码?
A、定义中断门描述符结构体,并定义一个中断门描述符结构体数组(33项)
B、写一个函数make_idt_desc,通过传入中断门描述符结构体指针,属性项,特定中断处理函数地址(通过引入intr_entry_table实现引用),将中断门描述符与特定中断处理函数建立联系
C、写一个函数idt_desc_init,循环调用B函数,完成构建中断描述符表
5、代码实现如下:
先定义内核用的段描述符选择子,中断门描述符attr字段 (myos/kernel/global.h)
#ifndef __KERNEL_GLOBAL_H
#define __KERNEL_GLOBAL_H
#include "stdint.h"//选择子的RPL字段
#define RPL0 0
#define RPL1 1
#define RPL2 2
#define RPL3 3//选择子的TI字段
#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)定义模块化的中断门描述符attr字段,attr字段指的是中断门描述符高字第8到16bit
#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位的门,不用,定义它只为和32位门区分#define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P << 7) + (IDT_DESC_DPL0 << 5) + IDT_DESC_32_TYPE) //DPL为0的中断门描述符attr字段
#define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE) //DPL为3的中断门描述符attr字段#endif
接下来定义了一些数据类型(intr_handler) (myos/kernel/interrupt.h)
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
typedef void* intr_handler; //将intr_handler定义为void*同类型
#endif
核心代码: (myos/kernel/interrupt.c)
#include "interrupt.h" //里面定义了intr_handler类型
#include "stdint.h" //各种uint_t类型
#include "global.h" //里面定义了选择子
#include "print.h"#define INTR_DESC_CONT 0x21 //支持的中断描述符个数33//按照中断门描述符格式定义结构体
struct gate_desc {uint16_t func_offset_low_word; //函数地址低字uint16_t selector; //选择子字段uint8_t dcount; //此项为双字计数字段,是门描述符中的第4字节。这个字段无用uint8_t attribute; //属性字段uint16_t func_offset_high_word; //函数地址高字
};// 静态函数声明,非必须
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; //中断门描述符(结构体)数组,名字叫idtextern intr_handler intr_entry_table[IDT_DESC_CNT]; //引入kernel.s中定义好的中断处理函数地址数组,intr_handler就是void* 表明是一般地址类型//此函数用于将传入的中断门描述符与中断处理函数建立映射,三个参数:中断门描述符地址,属性,中断处理函数地址
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;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;
}//此函数用来循环调用make_idt_desc函数来完成中断门描述符与中断处理函数映射关系的建立,传入三个参数:中断描述符表某个中段描述符(一个结构体)的地址
//属性字段,中断处理函数的地址
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]); }put_str(" idt_desc_init done\n");
}
6、其他代码详解查看书p325
接下来就是设定中断控制器。对任何硬件的控制都要通过端口,我们现在先把常用的端口读写功能利用内联汇编封装成C函数。这四个函数定义在io.h中,这样包含此.h文件就能够直接使用inline函数(原封不动展开,直接操作寄存器)。比一般的包含一个.h引入一个函数声明再链接一起要快得多,因为一般方式会涉及到call与ret指令。详细理由见p327 。内联汇编基础见p283
代码剖析略,具体代码如下: (myos/lib/kernel/io.h)
#ifndef __LIB_IO_H
#define __LIB_IO_H
#include "stdint.h"//一次送一字节的数据到指定端口,static指定只在本.h内有效,inline是让处理器将函数编译成内嵌的方式,就是在该函数调用处原封不动地展开//此函数有两个参数,一个端口号,一个要送往端口的数据
static inline void outb(uint16_t port, uint8_t data) {
/*********************************************************a表示用寄存器al或ax或eax,对端口指定N表示0~255, d表示用dx存储端口号, %b0表示对应al,%w1表示对应dx */ asm volatile ( "outb %b0, %w1" : : "a" (data), "Nd" (port));
}//利用outsw(端口输出串,一次一字)指令,将ds:esi指向的addr处起始的word_cnt(存在ecx中)个字写入端口port,ecx与esi会自动变化
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));
} //S表示寄存器esi/si/* 将从端口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指向的内存,我们在设置段描述符时, 已经将ds,es,ss段的选择子都设置为相同的值了,此时不用担心数据错乱。*/asm volatile ("cld; rep insw" : "+D" (addr), "+c" (word_cnt) : "d" (port) : "memory");
} //D表示寄存器edi/di //通知编译器,内存已经被改变了#endif
接下来就是对中断控制器进行编程了,设定好中断控制器,并只接受来自时钟中断的信号
p330剖析interrupt.c代码:
1、代码功能
设置中断控制器,对中断控制器操作,只接受来自时钟中断的信号
2、实现原理
CPU直接与中断打交道,不仅浪费CPU强大的性能,且CPU为了接收各种各样的中断信号,将会无比冗余(cpu将会有过多引脚),引入专门的中断控制器来先处理一下中断。我们需要先设置中断处理器(初始化),然后操作它来处理对应的中断信号。设置与操作都是通过它暴露在外的寄存器来进行。操作详情见p330
3、代码逻辑
初始化中断处理器
设定只接受时钟中断信号
4、怎么写代码?
A、看书p315设定相应的ICW与OCW
第一轮设定:(因为要按照ICW1-4的顺序推送,先主后从)
主片ICW1:00010001,0x11(送入主片控制端口):
- 0号位为1,表示要写入ICW4,x86系统必须为1;
- 1号位为0,表示级联;
- 2号位为0,用于是设定8085的调用时间间隔,x86不需要设置;
- 3号位为0,表示边沿触发;
- 4号位为1,ICW1的标记;
- 高3位x86不需要设置,直接为0。
主片ICW2:00100000,0x20(送入主片的数据端口):ICW2用来设置起始中断向量号,由于中断控制器上的IRQ接口是按顺序排列的,所以我们这里设定的实际就是IRQ0的中断向量号。这里我们设定32(也就是第33个中断向量号),因为前32个(0-31)已经被占用了。而且只需要填入高5位,也就是填一个8的倍数,然后8295A的8个IRQ接口就在此基础上顺序排号。如第一个主片,八个接口就是,IRQ0 = 32 + 0; IRQ1 = 32 + 1… 第一个从片就是IRQ0 = 32 + 8 + 0, IRQ1 = 32 + 8 + 1;
主片ICW3:00000100,0x04(送入主片数据端口):8位中哪位置1,表示哪个IRQ与从片连接,前面的值表示主片的IRQ2用于与从片级联。
主片ICW4:00000001,0x01(送入主片数据端口):
- 0号位为1,表示x86处理器;
- 1号位为0,表示手动结束中断(我们的中断处理程序中有通知主从片结束中断的步骤);
- 2号位为0,因为3号位设定为0(非缓冲模式工作),所以此位无用;在非缓冲模式下,8259A的数据总线直接连接到系统总线上,而不是通过缓冲器。当中断发生时,8259A会直接向CPU发送中断信号,而不经过任何缓冲或处理。这种模式可能会使系统在处理大量的中断请求时表现得不那么稳定,因为它对系统总线的要求更高。然而,非缓冲模式的系统设计会更简单一些,因为不需要缓冲器的附加硬件。
- 4号位为0,表示全嵌套模式,也就是优先处理较低中断请求线编号的中断请求(IRQ0最优先),特殊全嵌套模式是可以允许在中断处理过程中,如果来了一个优先级更高的中断请求,就暂停当前正在执行的中断,转而去执行那个优先级更高的中断请求;
- 高3位无用。
第二轮设定:
从片ICW1:00010001,0x11(送入从片控制端口):含义参照主片ICW1。
从片ICW2:00101000,0x28(送入从片的数据端口):主片起始中断向量号是32,主片自己8个IRQ,所以从片自然从40开始。
从片ICW3:00000010,0x02(送入从片数据端口):用来表明主片哪个IRQ与自己级联,前面的值表明是主片的IRQ2用于与自己级联。
从片ICW4:00000001,0x01(送入从片数据端口),含义参照主片ICW4。
设定只接受时钟中断的OCW1:
主片OCW1:11111110,0xfe(送入主片数据端口),我们先只打开时钟中断看看效果,而时钟中断在主片IRQ0上,所以OCW1的0号位置为0,表示放行IRQ0送入的中断信号。
从片OCW1:11111111,0xff(送入从片数据端口),从片中断信号全部屏蔽
B、将A设定好的按照P330步骤推送至中断控制器的特定寄存器(通过io.h中封装的函数),并封装成一个pic_init函数
5、新加入的代码如下: (myos/kernel/interrupt.c)
#include "io.h" //里面封装了一系列与端口操作的函数#define PIC_M_CTRL 0x20 // 这里用的可编程中断控制器是8259A,主片的控制端口是0x20
#define PIC_M_DATA 0x21 // 主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 // 从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 // 从片的数据端口是0xa1/* 初始化可编程中断控制器8259A */
static void pic_init(void) {/* 初始化主片 */outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.outb (PIC_M_DATA, 0x04); // ICW3: IR2接从片. outb (PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI/* 初始化从片 */outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.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");
}
6、其他代码详解查看书p331
接下来我们就调用之前我们写好的函数,并定义加载到IDTR寄存器中的值(参照书P306图IDTR结构),并最终加载IDTR来完成整个idt的构建工作,并封装成一个函数idt_init。
myos/kernel/interrupt.c 新加入如下代码,代码剖析略
/*完成有关中断的所有初始化工作*/
void idt_init() {put_str("idt_init start\n");idt_desc_init(); //调用上面写好的函数完成中段描述符表的构建pic_init(); //设定化中断控制器,只接受来自时钟中断的信号/* 加载idt */uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16)); //定义要加载到IDTR寄存器中的值asm volatile("lidt %0" : : "m" (idt_operand));put_str("idt_init done\n");
}
至此,只需要调用idt_init,整个idt的建构工作就完成了。
在 myos/kernel/interrup.h 中声明idt_init函数,其他文件只需要包含interrupt.h就可以调用idt_init函数
myos/kernel/interrupt.h中加入如下代码:
void idt_init(void);
我们写一个init_all调用上面写好的idt_init
myos/kernel/init.c 代码剖析略
#include "init.h"
#include "print.h"
#include "interrupt.h"/*负责初始化所有模块 */
void init_all() {put_str("init_all\n");idt_init(); //初始化中断
}
为了其他的函数调用我们的init_all,我们需要建立头文件init.h,声明函数init_all,其他的函数包含我们的头文件,就可以调用我们的函数
myos/kernel/init.h 代码剖析略
#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all(void);
#endif
最后,我们来写一个main.c来验证我们的之前关于中断的所有工作的正确性
myos/kernel/main.c 代码剖析略
#include "print.h"
#include "init.h"
void main(void)
{put_str("\nThis is Kernel!\n");init_all();asm volatile("sti" //为了演示中断,这里先临时开启中断);while(1);}
接下来进行编译,为了目录不至于太乱,建立build目录(myos/build)用于将所有目标文件和编译后的内核文件都放在此目录中
nasm -f elf -o build/print.o lib/kernel/print.S
gcc-4.4 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/main.o -m32 kernel/main.c (-fno-builtin是不使用GCC的内建函数https://blog.csdn.net/baiyu9821179/article/details/73007124)
nasm -f elf -o build/kernel.o kernel/kernel.S
gcc-4.4 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/interrput.o -m32 kernel/interrupt.c
gcc-4.4 -m32 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/init.o kernel/init.c
ld -m elf_i386 -Ttext 0x00001500 -e main -o build/kernel.bin build/kernel.o build/main.o build/init.o build/interrput.o build/print.o
dd if=build/kernel.bin of=/home/rlk/Desktop/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc
我们程序的含义,就是每发生一次时钟中断,就打印一次信息!