第七章 实现中断处理 柳暗花明又一村

本文最后更新于:1 年前

第七章 实现中断处理 柳暗花明又一村

写在前面

落笔前已经完成了这一章节的内容,本章难度不大但要编写的代码比较多,中断处理是os的必需内容,所以还是简单介绍下。

正文

我把代码和头文件分别介绍一下:

主体代码

kernel/kernel.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
[bits 32]
%define ERROR_CODE nop ; 若在相关的异常中cpu已经自动压入了错误码,为保持栈中格式统一,这里不做操作.
%define ZERO push 0 ; 若在相关的异常中cpu没有压入错误码,为了统一栈中格式,就手工压入一个0

extern put_str;
extern idt_table;

section .data
global intr_entry_table
intr_entry_table:

%macro VECTOR 2
section .text
intr%1entry: ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少

%2 ; 中断若有错误码会压在eip后面
; 以下是保存上下文环境
push ds
push es
push fs
push gs
pushad ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
mov al,0x20 ; 中断结束命令EOI
out 0xa0,al ; 向从片发送
out 0x20,al ; 向主片发送

push %1 ; 不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便
call [idt_table + %1*4] ; 调用idt_table中的C版本中断处理函数
jmp intr_exit

section .data
dd intr%1entry ; 存储各个中断入口程序的地址,形成intr_entry_table数组
%endmacro

section .text
global intr_exit
intr_exit:
; 以下是恢复上下文环境
add esp, 4 ; 跳过中断号
popad
pop gs
pop fs
pop es
pop ds
add esp, 4 ; 跳过error_code
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 ;时钟中断对应的入口

定义了33个中断处理程序
数组intr_entry_table记录每个程序入口地址
虽然定义了33个中断处理程序,但是从0x14到0x1f这12个中断向量号本实验是不可能产生的。

kernel/interrupt.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.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

#define IDT_DESC_CNT 0x21 // 目前总共支持的中断数33

#define EFLAGS_IF 0x00000200 // eflags寄存器中的if位为1
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl %0" : "=g" (EFLAG_VAR))


/*中断门描述符结构体*/
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]; // idt是中断描述符表,本质上就是个中断门描述符数组


char* intr_name[IDT_DESC_CNT]; // 用于保存异常的名字


/******** 定义中断处理程序数组 ********
* 在kernel.S中定义的intrXXentry只是中断处理程序的入口,
* 最终调用的是ide_table中的处理程序*/
intr_handler idt_table[IDT_DESC_CNT];

/********************************************/
extern intr_handler intr_entry_table[IDT_DESC_CNT]; // 声明引用定义在kernel.S中的中断处理函数入口数组





/* 初始化可编程中断控制器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

/* IRQ2用于级联从片,必须打开,否则无法响应从片上的中断
主片上打开的中断有IRQ0的时钟,IRQ1的键盘和级联从片的IRQ2,其它全部关闭 */
outb (PIC_M_DATA, 0xfe); 只打开时钟中断,其余均屏蔽

/* 打开从片上的IRQ14,此引脚接收硬盘控制器的中断 */
outb (PIC_S_DATA, 0xff);

put_str(" pic_init done\n");
}

/* 创建中断门描述符 */
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;
}

/*初始化中断描述符表*/
static void idt_desc_init(void) {
int i, lastindex = IDT_DESC_CNT - 1;
for (i = 0; i < IDT_DESC_CNT; i++) {
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
}
/* 单独处理系统调用,系统调用对应的中断门dpl为3,
* 中断处理程序为单独的syscall_handler */
put_str(" idt_desc_init done\n");
}



/* 通用的中断处理函数,一般用在异常出现时的处理 */
static void general_intr_handler(uint8_t vec_nr) {
if (vec_nr == 0x27 || vec_nr == 0x2f) { // 0x2f是从片8259A上的最后一个irq引脚,保留
return; //IRQ7和IRQ15会产生伪中断(spurious interrupt),无须处理。
}
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/kernel.S的call [idt_table + %1*4] */
idt_table[i] = general_intr_handler; // 默认为general_intr_handler。
// 以后会由register_handler来注册具体处理函数。
intr_name[i] = "unknown"; // 先统一赋值为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 Not Available Exception";
intr_name[8] = "#DF 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[15] 第15项是intel保留项,未使用
intr_name[16] = "#MF x87 FPU Floating-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");
}

idt_desc_init():填写33个中断处理程序对应的中断门描述符
pic_init() ; 初始化8259A芯片
加载idt:即赋值IDTR

我们写的33个中断处理程序,前20个的中断向量号是异常,由cpu产生
第21个到第32个中断向量号需保留,所以名字赋值成unknown。所以两块8259A芯片产生的15个中断向量号从0x20到0x2f。其中IRQ7为并口1,IRQ15是保留,伪中断经常用这两个号,这两个接口无法通过IMR屏蔽寄存器来屏蔽,所以需要软件单独处理。伪中断是一种硬件中断,比如中断线路上的电气信号异常等。本次实验只打开了IRQ0,所以由8259A芯片产生的中断向量号只可能是0x20,0x27、0x2f。CPU还可能产生0~19号中断向量号(第15号除外)。上述本次实验可能产生中断向量号,都会进入中断处理程序,在中断处理程序调用general_intr_handler,打印自己的中断向量号,0x27、0x2f的伪中断除外。

kernel/init.c

1
2
3
4
5
6
7
8
9
10
11
12
#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();
}

device/timer.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

#include "io.h"
#include "print.h"
#include "interrupt.h"


#define IRQ0_FREQUENCY 100
#define INPUT_FREQUENCY 1193180
#define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY
#define CONTRER0_PORT 0x40
#define COUNTER0_NO 0
#define COUNTER_MODE 2
#define READ_WRITE_LATCH 3
#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));
/* 先写入counter_value的低8位 */
outb(counter_port, (uint8_t)counter_value);
/* 再写入counter_value的高8位 */
outb(counter_port, (uint8_t)counter_value >> 8);
}


/* 初始化PIT8253 */
void timer_init() {
put_str("timer_init start\n");
/* 设置8253的定时周期,也就是发中断的周期 */
frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
put_str("timer_init done\n");
}

kernel/main.c

1
2
3
4
5
6
7
8
9
#include "print.h"
#include "init.h"

int main(void) {
put_str("I am kernel\n");
init_all();
asm volatile("sti");
while(1);
}

头文件

kernel/init.h

1
2
3
4
#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all(void);
#endif

kernel/interrupt.h

1
2
3
4
5
6
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;
void idt_init(void);
#endif

intr_handler是用来修饰33个中断处理程序入口地址数组intr_entry_table[IDT_DESC_CNT]的,该数组位于某个数据段,成员均是地址。

void*是空指针类型,表地址。

lib/kernel/io.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**************	 机器模式   ***************
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) {
/*********************************************************
a表示用寄存器al或ax或eax,对端口指定N表示0~255, d表示用dx存储端口号,
%b0表示对应al,%w1表示对应dx */
asm volatile ( "outb %b0, %w1" : : "a" (data), "Nd" (port));
/******************************************************/
}

/* 将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));
/******************************************************/
}

/* 将从端口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");
/******************************************************/
}

#endif

平时写函数都是把函数体写在.c的文件里,函数声明写在.h里,其他文件要调用就include .h的文件即可。但是这次却把这些函数写在了.h里。static作用域只是此文件,也就是只有此文件才可以调用。

对端口的读写或一段连续的内存对端口读写都可以调用汇编指令inb(AT&A)/ins(intel), outsw等。
但是为了内核使用c语言在main函数完成8259A的设置,所以可以使用c函数封装内联汇编形式,这些函数都是static inline,意味着当调用此函数时不再是函数调用,而是就地展开,不再需要传递参数入栈,执行快,但程序体积更庞大。
之所以这样是因为对端口操作属于I/O操作,非常慢,一旦用户程序调用他们,可能会等很长时间,所以在函数调用方面就要尽可能快来追求更短的总时间。

kernel/global.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#ifndef __KERNEL_GLOBAL_H
#define __KERNEL_GLOBAL_H
#include "stdint.h"

#define RPL0 0
#define RPL1 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位的门,不用,定义它只为和32位门区分
#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

device/timer.h

1
2
3
4
5
6
#ifndef __DEVICE_TIME_H
#define __DEVICE_TIME_H
#include "stdint.h"
void timer_init(void);
void mtime_sleep(uint32_t m_seconds);
#endif

写在后面

不知不觉已经走过了将近一半的路程,回过头看实在艰辛,不过想想自己在这几天收获的东西真是很多。下一章节不准备向下进行了,我想把这半程的过程重新整理一下,并且把代码托管到github上。收拾一下行囊,再出发!