真象还原 --内存管理/线程/输入输出 study(3)

真象还原 --内存管理/线程/输入输出 study(3)

H_Haozi Lv3

A 前情提要

在此之前,第一部分我们完成了基础的环境配置,bochs配置,以及MBR,loader的的基础编写,成功的进入了保护模式并且开启了内存分页功能

第二部分完成了对内联汇编中断初始化定时器初始化,也实现了基础打印函数*

这一部分主要学习内存管理/线程/同步相关内容

ps:如果参考本系列文章来实操,需要结合《操作系统真象还原》一起观看,否则会缺失很多细节


B 实现 assert 断言

断言其实就是在 C 语言中学过的 assert

在我们系统中,我们实现两种断言,一种是为内核系统使用的ASSERT,另一种是为用户进程使用的assert,用户进程离现在还早,咱们本节先实现专供内核使用的 ASSERT。

一方面,当内核运行中出现问题时,多属于严重的错误,着实没必要再运行下去了。另一方面,断言在输出报错信息时,屏幕输出不应该被其他进程干扰,这样咱们才能专注于报错信息。综上两点原因,

ASSERT 排查出错误后,最好在关中断的情况下打印报错信息。内核运行时,为了通过时钟中断定时调度其他任务,大部分情况下中断是打开的,如何在开中断的情况下把中断关闭,这就是我们等会要实现的内容

首先实现开关中断,这里我们在interrupt.cinterrupt.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
// /home/mouse/OS_mouse/tool/bochs/mouse/kernel/interrupt.h

#ifndef __INTERRUPUT_H
#define __INTERRUPUT_H
#include "stdint.h"

typedef void* intr_handler;
void idt_init();

/*定义中断的两种状态
* INTR_OFF 值为0,表示关中断
* INTR_ON 值为1,表示开中断
*/
enum intr_status{ //中断状态
INTR_OFF,
INTR_ON
};


enum intr_status intr_enable(); /*开中断并且返回开中断前的状态*/
enum intr_status intr_disable(); /*关中断并且返回关中断前的状态 */
enum intr_status intr_get_status(); /*获取当前中断状态*/
enum intr_status intr_set_status(enum intr_status status); /*将中断状态设置成 status*/

#endif
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
// /home/mouse/OS_mouse/tool/bochs/mouse/kernel/interrupt.c

#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "print.h" //put_str
#include "io.h" //io操作,联合汇编


#define IDT_DESC_CNT 0x33 // 目前总共支持的中断数
#define PIC_M_CTRL 0x20 // 主片的控制端口是 0x20
#define PIC_M_DATA 0x21 // 主片的数据端口是 0x21
#define PIC_S_CTRL 0xa0 // 从片的控制端口是 0xa0
#define PIC_S_DATA 0xa1 // 从片的数据端口是 0xa1

#define EFLAGS_IF 0X00000200 //eflags 寄存器的 if 位为0
//获取 eflags 寄存器的值,EFLAG_VAR 是返回值
#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;
};

// 静态函数声明,非必须
// intr_handler 是自己定义的(interrupt.h) typedef void* intr_handler;
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.S中的中断处理函数入口函数

//添加部分:
static void exception_init(void); //完成一般中断处理函数注册及异常名称注册
static void general_intr_handler(uint8_t vec_nr); //通用的中断处理函数,一般在异常出现时处理
const char* intr_name[IDT_DESC_CNT]; //用于保留异常的名字
intr_handler idt_table[IDT_DESC_CNT]; //中断处理函数程序数组,在kernel.S 中定义的 intrXXentry是中断处理函数的入口,最终调用 idt_table 里面的函数


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

/*创建中断门描述符*/
/*
* 参数: 中断门描述符的指针,中断描述符内的属性,中断描述符内对应的中断处理函数
*
*/
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; //定义在global.h : 指向内核数据段的选择子
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]);
}
put_str("idt_desc_init done\n");
}

//添加部分
//通用的中断处理函数,一般在异常出现时处理
static void general_intr_handler(uint8_t vec_nr)
{
if(vec_nr == 0x27 || vec_nr == 0x2f) //IRQ7s和IRQ15会产生伪中断,无需处理,0x2f是从片8259A上的最后一个IRQ引脚,保留项
{
return;
}
put_str("int vector : 0x");
put_int(vec_nr);
put_str("\n");
}

//页中断,我当时调试使用的,大家可以不用管
static void page_fault_handler(uint8_t vec_nr)
{
if(vec_nr == 0x27 || vec_nr == 0x2f) //IRQ7s和IRQ15会产生伪中断,无需处理,0x2f是从片8259A上的最后一个IRQ引脚,保留项
{
return;
}
put_str("int vector : 0x");
put_int(vec_nr);
put_str("\n");

uint32_t faulting_address;
asm volatile ("mov %%cr2, %0" : "=r" (faulting_address)); // 读取CR2
put_str("Page fault at address: 0x");
put_int(faulting_address); // 十六进制打印
put_str("\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"; // 先统一赋值为 unknow
}
idt_table[14] = page_fault_handler; //页错误处理函数
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";
}

/*开中断并且返回开中断前的状态 intr_statusl类型定义再interrupt.h中 */
/*这里在每个if分支都添加return语句,可以让程序更加快,但更大,空间换时间*/
enum intr_status intr_enable()
{
enum intr_status old_status;
if(INTR_ON == intr_get_status())
{
old_status = INTR_ON;
return old_status;
}
else
{
old_status = INTR_OFF;
asm volatile("sti"); //开中断,sti指令将IF置1
return old_status;
}
}

/*关中断并且返回关中断前的状态 */
enum intr_status intr_disable()
{
enum intr_status old_status;
if(INTR_ON == intr_get_status())
{
old_status = INTR_ON;
asm volatile("cli" : : : "memory"); //关中断,cli指令将IF位置置0
return old_status;
}
else
{
old_status = INTR_OFF;
return old_status;
}
}

/*将中断状态设置成 status*/
enum intr_status intr_set_status(enum intr_status status)
{
return (status & INTR_ON) ?intr_enable() : intr_disable();
}

/*获取当前中断状态*/
enum intr_status intr_get_status()
{
uint32_t eflags = 0;
GET_EFLAGS(eflags); //宏实现
return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}

/*完成有关中断的所有初始化工作*/
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" ::""(idt_operand));
put_str("idt_init done\n");
}

然后来实现断言功能: 我们来仿照C语言来实现,ASSERT(条件表达式) 如果表达式成立则什么都不做,如果不成立则打印出错信息并停止执行,在路径/mouse/kernel下添加debug.c debug.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// /home/mouse/OS_mouse/tool/bochs/mouse/kernel/debug.h

#ifndef __KERNEL_DEBUG_H
#define __KERNEL_DEBUG_H

void panic_spin(char* filename,int line,const char* func,const char* condition);

/*************************** __VA_ARGS__ *******************************
* __VA_ARGS__ 是预处理器所支持的专用标识符
* 代表所有与省略号相对应的参数。
* "..."表示定义的宏其参数可变。*/
#define PANIC(...) panic_spin (__FILE__, __LINE__, __func__, __VA_ARGS__)
/***********************************************************************/

#ifdef NDEBUG
#define ASSERT(CONDITION) ((VOID) 0 ) //定义为空,相当于关闭这个宏
#else /* 符号#让编译器将宏的参数转化为字符串字面量 */
#define ASSERT(CONDITION) if(CONDITION){} else{ PANIC(#CONDITION); }

#endif /*NDEBUG*/

#endif /*__KERNEL_DEBUG_H*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// /home/mouse/OS_mouse/tool/bochs/mouse/kernel/debug.c

#include "debug.h"
#include "print.h"
#include "interrupt.h"

/*打印文件名,行号,函数名,条件并使程序悬停*/
void panic_spin(char* filename,int line,const char* func,const char* condition)
{
intr_disable(); //关闭中断,因为有的时候会单独调用panic_spin
put_str("\n\n\n!!!!! error !!!!!\n");
put_str("filename:"); put_str((char*)filename);put_str("\n");
put_str("line:0x");put_int(line);put_str("\n");
put_str("function:"); put_str((char*)func);put_str("\n");
put_str("condition:");put_str((char*)condition);put_str("\n");
while(1);
}

最后修改主函数,主动进入断言:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/***** /home/mouse/OS_mouse/tool/bochs/mouse/kernel/main.c *****/
#include "print.h"
#include "init.h"
#include "interrupt.h"
#include "debug.h"
int main()
{
// while(1);
put_str("I am kernel!\n");
init_all(); //初始化所有中断
// asm volatile("sti") ; //暂时打开中断,这里先不打开
ASSERT(1 == 2); //测试断言
while(1); //防止退出
return 0;
}

然后make make img 运行就能看到相关提示了,比如函数名,错误行号,错误的内容等


C 实现字符串操作函数

为了后面的开发更加方便,这里实现与字符串相关的函数,此后这里的函数会被经常用到

这里在lib文件下建立 string.c文件,Makefile 文件和之前一样,已经在上一章中介绍过了,注意解除Makefile里面的注释,让编译器编译string.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
#include "string.h"
#include "global.h"
#include "debug.h"

/* 将dst_起始的size个字节置为value */
void memset(void* dst_, uint8_t value, uint32_t size) {
ASSERT(dst_ != NULL);
uint8_t* dst = (uint8_t*)dst_;
while (size-- > 0)
*dst++ = value;
}

/* 将src_起始的size个字节复制到dst_ */
void memcpy(void* dst_, const void* src_, uint32_t size) {
ASSERT(dst_ != NULL && src_ != NULL);
uint8_t* dst = dst_;
const uint8_t* src = src_;
while (size-- > 0)
*dst++ = *src++;
}

/* 连续比较以地址a_和地址b_开头的size个字节,若相等则返回0,若a_大于b_返回+1,否则返回-1 */
int memcmp(const void* a_, const void* b_, uint32_t size) {
const char* a = a_;
const char* b = b_;
ASSERT(a != NULL || b != NULL);
while (size-- > 0) {
if(*a != *b) {
return *a > *b ? 1 : -1;
}
a++;
b++;
}
return 0;
}

/* 将字符串从src_复制到dst_ */
char* strcpy(char* dst_, const char* src_) {
ASSERT(dst_ != NULL && src_ != NULL);
char* r = dst_; // 用来返回目的字符串起始地址
while((*dst_++ = *src_++));
return r;
}

/* 返回字符串长度 */
uint32_t strlen(const char* str) {
ASSERT(str != NULL);
const char* p = str;
while(*p++);
return (p - str - 1);
}

/* 比较两个字符串,若a_中的字符大于b_中的字符返回1,相等时返回0,否则返回-1. */
int8_t strcmp (const char* a, const char* b) {
ASSERT(a != NULL && b != NULL);
while (*a != 0 && *a == *b) {
a++;
b++;
}
/* 如果*a小于*b就返回-1,否则就属于*a大于等于*b的情况。在后面的布尔表达式"*a > *b"中,
* 若*a大于*b,表达式就等于1,否则就表达式不成立,也就是布尔值为0,恰恰表示*a等于*b */
return *a < *b ? -1 : *a > *b;
}

/* 从左到右查找字符串str中首次出现字符ch的地址(不是下标,是地址) */
char* strchr(const char* str, const uint8_t ch) {
ASSERT(str != NULL);
while (*str != 0) {
if (*str == ch) {
return (char*)str; // 需要强制转化成和返回值类型一样,否则编译器会报const属性丢失,下同.
}
str++;
}
return NULL;
}

/* 从后往前查找字符串str中首次出现字符ch的地址(不是下标,是地址) */
char* strrchr(const char* str, const uint8_t ch) {
ASSERT(str != NULL);
const char* last_char = NULL;
/* 从头到尾遍历一次,若存在ch字符,last_char总是该字符最后一次出现在串中的地址(不是下标,是地址)*/
while (*str != 0) {
if (*str == ch) {
last_char = str;
}
str++;
}
return (char*)last_char;
}

/* 将字符串src_拼接到dst_后,将回拼接的串地址 */
char* strcat(char* dst_, const char* src_) {
ASSERT(dst_ != NULL && src_ != NULL);
char* str = dst_;
while (*str++);
--str; // 别看错了,--str是独立的一句,并不是while的循环体
while((*str++ = *src_++)); // 当*str被赋值为0时,此时表达式不成立,正好添加了字符串结尾的0.
return dst_;
}

/* 在字符串str中查找指定字符ch出现的次数 */
uint32_t strchrs(const char* str, uint8_t ch) {
ASSERT(str != NULL);
uint32_t ch_cnt = 0;
const char* p = str;
while(*p != 0) {
if (*p == ch) {
ch_cnt++;
}
p++;
}
return ch_cnt;
}

同时这里要添加 NULL 的定义,可以先暂时添加到 stdint.h文件中:

1
#define NULL ((void*)0) 

编译写入运行后大家就可以在主函数中调用这些函数来测试,比如 strlen("1234"),然后用put_int输出验证等等


D 位图 bitmap 及其函数的实现

位图,也就是 bitmap,广泛用于资源管理,是一种管理资源的方式、手段。“资源”包括很多,比如内存或硬盘,对于此类大容量资源的管理一般都会采用位图的方式

位是指 bit,即字节中的位,1 字节中有 8 个位。图是指 map,map 这个词在很久之前就介绍过啦,地图本质上就是映射的意思,映射,即对应关系。综合起来,位图就是用字节中的 1 位来映射其他单位大小的资源,按位与资源之间是一对一的对应关系

总结一下,位图相当于一组资源的映射。位图中的每一位和被管理的单位资源都是一对一的关系,故位图主要用于管理容量较大的资源

这里还是通过实际的位图代码来理解,这里在mouse/lib/kernel/下创建 bitmap.c bitmap.h文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// /home/mouse/OS_mouse/tool/bochs/mouse/lib/kernel/bitmap.h

#ifndef __LIB_KERNEL_BITMAP_H
#define __LIB_KERNEL_BITMAP_H
#include "global.h"
#define BITMAP_MASK 1

struct bitmap
{
uint32_t btmp_bytes_len; //在遍历位图时,整体上以字节为单位,细节上是以位位单位,所以此处位图的指针必须是单字节
uint8_t* bits; //一个字节
};

void bitmap_init(struct bitmap* btmp);
bool bitmap_scan_test(struct bitmap* btmp,uint32_t bit_idx);
int bitmap_scan(struct bitmap* btmp,uint32_t cnt);
void bitmap_set(struct bitmap* btmp,uint32_t bit_idx,int8_t value);

#endif
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
// /home/mouse/OS_mouse/tool/bochs/mouse/lib/kernel/bitmap.c
#include "bitmap.h"
#include "string.h"
#include "print.h"
#include "interrupt.h"
#include "debug.h"

//初始化位图 btmp
void bitmap_init(struct bitmap* btmp)
{
memset(btmp->bits,0,btmp->btmp_bytes_len); //清零
}

//判断位图的第bit_idx位是否为1,如果为1,返回true,否则返回false
bool bitmap_scan_test(struct bitmap* btmp,uint32_t bit_idx)
{
uint32_t byte_idx = bit_idx/8; //向下取整
uint32_t bit_odd = bit_idx%8; //取余获得索引数组的位
return (btmp->bits[byte_idx]) & (BITMAP_MASK << bit_odd); //掩码判断具体的位是否为1
}

//在位图中连续申请cnt个位,成功返回起始下表,失败返回-1
int bitmap_scan(struct bitmap* btmp,uint32_t cnt)
{
uint32_t idx_byte = 0; //用于记录空闲位所在字节

//先逐字节比较,蛮力
while((0xff == btmp->bits[idx_byte]) && (idx_byte < btmp->btmp_bytes_len))
{
idx_byte++; //如果为0xff,表示该字节已经没有空闲位置,向下一个字节查找
}
ASSERT(idx_byte < btmp->btmp_bytes_len);
if(idx_byte == btmp->btmp_bytes_len) //内存池找不到可用空间
{
return -1;
}

//若找到了空闲位,逐位比较,然后返回索引:
int idx_bit = 0;
while((uint8_t)(BITMAP_MASK << idx_bit) & btmp->bits[idx_byte])
{
idx_bit++;
}

int bit_idx_start = idx_byte*8 + idx_bit; //空闲位图内的下标
if(cnt == 1)
{
return bit_idx_start; //刚好一个
}
uint32_t bit_left = (btmp->btmp_bytes_len*8 - bit_idx_start);//获得剩余可判断的位
uint32_t next_bit = bit_idx_start+1;
uint32_t count = 1; //记录空闲位的个数
bit_idx_start = -1; //先改成-1,如果没找到直接返回
while(bit_left-- > 0)
{
if(!(bitmap_scan_test(btmp,next_bit)))
{
//若next_bit == 0
count++;
}
else
{
count = 0;
}
if(count == cnt) //找到了连续的cnt个空位
{
bit_idx_start = next_bit - cnt +1; //计算起始位
break;
}
next_bit++; //查找下一个位
}
return bit_idx_start;
}

//设置位图btmp的bit_idx位为value
void bitmap_set(struct bitmap* btmp,uint32_t bit_idx,int8_t value)
{
ASSERT((value == 0) || (value ==1));
uint32_t byte_idx = bit_idx/8; //向下取整
uint32_t bit_odd = bit_idx %8; //获得余数,即索引数组的位

//一般会使用0x1这样的数字对字节进行位操作,将1任意移动后再取反,或者取反再移位
if(value) //value ==1
{
btmp->bits[byte_idx] |= (BITMAP_MASK <<bit_odd);
}
else
{
btmp->bits[byte_idx] &= ~(BITMAP_MASK <<bit_odd);
}
}

注意还要添加Makefile里面的c资源bitmap.c,下面就开始初入内存管理吧:


E 内存管理系统

用户程序所占用的内存空间是由操作系统分配的,内存是如何分配的并且该给用户进程分配多少字节呢?这就是咱们要解决的问题。
所以,从现在起,咱们要循序渐进地实现内存管理系统,一直到函数 malloc 和 free 的完成

E.1 内存池规划

  • 物理内存池

这里我们首先来规划物理内存池,这里将它分成两部分,一部分称为用户物理内存池,一部分称为内核物理内存池(只给操作系统使用)

同时,每次分配内存也是按单位大小来获取,这个单位的大小是 4KB,也称为页,所以内存池管理的是一个个为4KB的内存块,从内存池中获取的内存大小至少为 4KB 或者为 4KB 的倍数(当然,以后会实现更颗粒度的内存管理)

  • 虚拟内存池

前情回顾:

在分页机制下程序中的地址都是虚拟地址,虚拟地址的范围取决于地址总线的宽度,咱们是在 32 位环境下,所以虚拟地址空间为 4GB。除了地址空间比较大以外,分页机制的另一个好处是每个任务都有自己的 4GB 虚拟地址空间,也就是各程序中的虚拟地址不会与其他程序冲突,都可以为相同的虚拟地址,不仅用户进程是这样,内核也是;同时虚拟地址池的单位也是4KB

同时这里也是分为用户虚拟地址池内核虚拟地址池

下面还是直接动手实践: 在/mouse/kernel目录下建立 memory.h memory.c,有关内存管理的代码都放在这里,同时记得要在Makefile 里面添加东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// /home/mouse/OS_mouse/tool/bochs/mouse/kernel/memory.h

#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H
#include "stdint.h"
#include "bitmap.h"


//虚拟地址池,用于虚拟地址管理
struct virtual_addr
{
struct bitmap vaddr_bitmap; //虚拟地址的位图
uint32_t vaddr_start; //虚拟地址的起始地址
};

extern struct pool kernel_pool, user_pool;

void mem_init(void);

#endif
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
// /home/mouse/OS_mouse/tool/bochs/mouse/kernel/memory.c

#include "memory.h"
#include "stdint.h"
#include "print.h"

#define PG_SIZE 4096

/*************** 位图地址 ********************
* 因为0xc009f000是内核主线程栈顶,0xc009e000是内核主线程的pcb.
* 一个页框大小的位图可表示128M内存, 位图位置安排在地址0xc009a000,
* 这样本系统最大支持4个页框的位图,即512M */
#define MEM_BITMAP_BASE 0xc009a000
/*************************************/

/* 0xc0000000是内核从虚拟地址3G起. 0x100000意指跨过低端1M内存,使虚拟地址在逻辑上连续 */
#define K_HEAP_START 0xc0100000

/* 内存池结构,生成两个实例用于管理内核内存池和用户内存池 */
struct pool {
struct bitmap pool_bitmap; // 本内存池用到的位图结构,用于管理物理内存
uint32_t phy_addr_start; // 本内存池所管理物理内存的起始地址
uint32_t pool_size; // 本内存池字节容量
};

struct pool kernel_pool, user_pool; // 生成内核内存池和用户内存池
struct virtual_addr kernel_vaddr; // 此结构是用来给内核分配虚拟地址

/* 初始化内存池 */
static void mem_pool_init(uint32_t all_mem) {
put_str(" mem_pool_init start\n");
uint32_t page_table_size = PG_SIZE * 256; // 页表大小= 1页的页目录表+第0和第768个页目录项指向同一个页表+
// 第769~1022个页目录项共指向254个页表,共256个页框
uint32_t used_mem = page_table_size + 0x100000; // 0x100000为低端1M内存
uint32_t free_mem = all_mem - used_mem;
uint16_t all_free_pages = free_mem / PG_SIZE; // 1页为4k,不管总内存是不是4k的倍数,
// 对于以页为单位的内存分配策略,不足1页的内存不用考虑了。
uint16_t kernel_free_pages = all_free_pages / 2;
uint16_t user_free_pages = all_free_pages - kernel_free_pages;

/* 为简化位图操作,余数不处理,坏处是这样做会丢内存。
好处是不用做内存的越界检查,因为位图表示的内存少于实际物理内存*/
uint32_t kbm_length = kernel_free_pages / 8; // Kernel BitMap的长度,位图中的一位表示一页,以字节为单位
uint32_t ubm_length = user_free_pages / 8; // User BitMap的长度.

uint32_t kp_start = used_mem; // Kernel Pool start,内核内存池的起始地址
uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE; // User Pool start,用户内存池的起始地址

kernel_pool.phy_addr_start = kp_start;
user_pool.phy_addr_start = up_start;

kernel_pool.pool_size = kernel_free_pages * PG_SIZE;
user_pool.pool_size = user_free_pages * PG_SIZE;

kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
user_pool.pool_bitmap.btmp_bytes_len = ubm_length;

/********* 内核内存池和用户内存池位图 ***********
* 位图是全局的数据,长度不固定。
* 全局或静态的数组需要在编译时知道其长度,
* 而我们需要根据总内存大小算出需要多少字节。
* 所以改为指定一块内存来生成位图.
* ************************************************/
// 内核使用的最高地址是0xc009f000,这是主线程的栈地址.(内核的大小预计为70K左右)
// 32M内存占用的位图是2k.内核内存池的位图先定在MEM_BITMAP_BASE(0xc009a000)处.
kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;

/* 用户内存池的位图紧跟在内核内存池位图之后 */
user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);
/******************** 输出内存池信息 **********************/
put_str(" kernel_pool_bitmap_start:");
put_int((int)kernel_pool.pool_bitmap.bits);
put_str(" kernel_pool_phy_addr_start:");
put_int(kernel_pool.phy_addr_start);
put_str("\n");
put_str(" user_pool_bitmap_start:");
put_int((int)user_pool.pool_bitmap.bits);
put_str(" user_pool_phy_addr_start:");
put_int(user_pool.phy_addr_start);
put_str("\n");

/* 将位图置0*/
bitmap_init(&kernel_pool.pool_bitmap);
bitmap_init(&user_pool.pool_bitmap);

/* 下面初始化内核虚拟地址的位图,按实际物理内存大小生成数组。*/
kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length; // 用于维护内核堆的虚拟地址,所以要和内核内存池大小一致

/* 位图的数组指向一块未使用的内存,目前定位在内核内存池和用户内存池之外*/
kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);

kernel_vaddr.vaddr_start = K_HEAP_START;
bitmap_init(&kernel_vaddr.vaddr_bitmap);
put_str(" mem_pool_init done\n");
}

/* 内存管理部分初始化入口 */
void mem_init() {
put_str("mem_init start\n");
uint32_t mem_bytes_total = (*(uint32_t*)(0xb00));
mem_pool_init(mem_bytes_total); // 初始化内存池
put_str("mem_init done\n");
}

最后将mem_init()函数添加到 init.c中即可,同时如果编译报错bool未定义 的话可以在stdint.h中添加定义如下:

1
2
3
4
5
6
7
#define BOOL int
#define bool int
#define true 1
#define false 0
#define TRUE 1
#define FALSE 0
#define NULL ((void*)0)

然后make make img ,运行就可以看到结果大致如下:

Mosuel am kernel!
init all
idt init start
idt desc init done
pic init done
idt init done
timer init start
timer init donemem init startmem pool init start
kernel pool bitmap _start:c009A000 kernel pool >phy_addr_start:200000user pool bitmap
start:c009A1E0 user pool phy_addr start:1100000mem >pool init donemem init done

大家可以仔细看看代码中的注释,下面就是如何来分配内存了


E.2 分配页内存

有了内存池之后,就是分配内存了,我们先来学习如何分配页内存(支持一次分配 n 个页的内存,即n*4096 字节)

这里分为了三步,也是我们等会要实现的

(1)首先处理高 10 位的 pde 索引,从而处理器得到页表物理地址。
(2)其次处理中间 10 位的 pte 索引,进而处理器得到普通物理页的物理地址。
(3)最后是把低 12 位作为普通物理页的页内偏移地址,此偏移地址加上物理页的物理地址,得到的地址之和便是最终的物理地址,处理器到此物理地址上进行读写操作。

下面还是修改之前的两个文件:

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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
// /home/mouse/OS_mouse/tool/bochs/mouse/kernel/memory.c

#include "memory.h"
#include "stdint.h"
#include "print.h"
#include "string.h"
#include "bitmap.h"
#include "debug.h"


#define PG_SIZE 4096

#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)
#define PTE_IDX(addr) ((addr & 0x003FF000) >> 12)

/*************** 位图地址 ********************
* 因为0xc009f000是内核主线程栈顶,0xc009e000是内核主线程的pcb.
* 一个页框大小的位图可表示128M内存, 位图位置安排在地址0xc009a000,
* 这样本系统最大支持4个页框的位图,即512M */
#define MEM_BITMAP_BASE 0xc009a000
/*************************************/

/* 0xc0000000是内核从虚拟地址3G起. 0x100000意指跨过低端1M内存,使虚拟地址在逻辑上连续 */
#define K_HEAP_START 0xc0100000

/* 内存池结构,生成两个实例用于管理内核内存池和用户内存池 */
struct pool {
struct bitmap pool_bitmap; // 本内存池用到的位图结构,用于管理物理内存
uint32_t phy_addr_start; // 本内存池所管理物理内存的起始地址
uint32_t pool_size; // 本内存池字节容量
};

struct pool kernel_pool, user_pool; // 生成内核内存池和用户内存池
struct virtual_addr kernel_vaddr; // 此结构是用来给内核分配虚拟地址

/* 在 pf 表示的虚拟内存池中申请 pg_cnt 个虚拟页,
* 成功则返回虚拟页的起始地址,失败则返回 NULL */
static void* vaddr_get(enum pool_flags pf, uint32_t pg_cnt)
{
int vaddr_start = 0, bit_idx_start = -1;
uint32_t cnt = 0;
if (pf == PF_KERNEL)
{
bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);
if (bit_idx_start == -1)
{
return NULL;
}
while(cnt < pg_cnt)
{
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
}
vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
}
else
{
// 用户内存池,将来实现用户进程再补充
}
return (void*)vaddr_start;
}

/* 得到虚拟地址 vaddr 对应的 pte 指针*/
uint32_t* pte_ptr(uint32_t vaddr)
{
/* 先访问到页表自己 +
* 再用页目录项 pde(页目录内页表的索引)作为 pte 的索引访问到页表 +
* 再用 pte 的索引作为页内偏移 */
uint32_t* pte = (uint32_t*)(0xffc00000 + \
((vaddr & 0xffc00000) >> 10) + \
PTE_IDX(vaddr) * 4);
return pte;
}

/* 得到虚拟地址 vaddr 对应的 pde 的指针 */
uint32_t* pde_ptr(uint32_t vaddr)
{
/* 0xfffff 用来访问到页表本身所在的地址 */
uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr) * 4);
return pde;
}

/* 在 m_pool 指向的物理内存池中分配 1 个物理页,
* 成功则返回页框的物理地址,失败则返回 NULL */
static void* palloc(struct pool* m_pool)
{
/* 扫描或设置位图要保证原子操作 */
int bit_idx = bitmap_scan(&m_pool->pool_bitmap, 1); // 找一个物理页面
if (bit_idx == -1 )
{
return NULL;
}
bitmap_set(&m_pool->pool_bitmap, bit_idx, 1); // 将此位 bit_idx 置 1
uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start);
return (void*)page_phyaddr;
}


/* 页表中添加虚拟地址_vaddr 与物理地址_page_phyaddr 的映射 */
static void page_table_add(void* _vaddr, void* _page_phyaddr)
{
uint32_t vaddr = (uint32_t)_vaddr, page_phyaddr = (uint32_t)_page_phyaddr;
uint32_t* pde = pde_ptr(vaddr);
uint32_t* pte = pte_ptr(vaddr);
/************************ 注意 *************************
* 执行*pte,会访问到空的 pde。所以确保 pde 创建完成后才能执行*pte,
* 否则会引发 page_fault。因此在*pde 为 0 时,
*pte 只能出现在下面 else 语句块中的*pde 后面。
***********************************************************/
/* 先在页目录内判断目录项的 P 位,若为 1,则表示该表已存在 */
if (*pde & 0x00000001)
{
// 页目录项和页表项的第 0 位为 P,此处判断目录项是否存在
ASSERT(!(*pte & 0x00000001));
if (!(*pte & 0x00000001))
{
// 只要是创建页表,pte 就应该不存在,多判断一下放心
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
// US=1,RW=1,P=1
}
else
{ //目前应该不会执行到这,因为上面的 ASSERT 会先执行
PANIC("pte repeat");
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
// US=1,RW=1,P=1
}
}
else
{
// 页目录项不存在,所以要先创建页目录再创建页表项
/* 页表中用到的页框一律从内核空间分配 */
uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);
*pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);

/* 分配到的物理页地址 pde_phyaddr 对应的物理内存清 0,
* 避免里面的陈旧数据变成了页表项,从而让页表混乱。
* 访问到 pde 对应的物理地址,用 pte 取高 20 位便可。
* 因为 pte 基于该 pde 对应的物理地址内再寻址,
* 把低 12 位置 0 便是该 pde 对应的物理页的起始*/
memset((void*)((int)pte & 0xfffff000), 0, PG_SIZE);
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
// US=1,RW=1,P=1
}
}

/* 分配 pg_cnt 个页空间,成功则返回起始虚拟地址,失败时返回 NULL */
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt)
{
ASSERT(pg_cnt > 0 && pg_cnt < 3840);
/*********** malloc_page 的原理是三个动作的合成: ***********
1 通过 vaddr_get 在虚拟内存池中申请虚拟地址
2 通过 palloc 在物理内存池中申请物理页
3 通过 page_table_add 将以上得到的虚拟地址和物理地址在页表中完成映射
********************************************************************/
void* vaddr_start = vaddr_get(pf, pg_cnt);
if (vaddr_start == NULL)
{
return NULL;
}
uint32_t vaddr = (uint32_t)vaddr_start, cnt = pg_cnt;
struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
/* 因为虚拟地址是连续的,但物理地址可以是不连续的,所以逐个做映射*/
while (cnt-- > 0)
{
void* page_phyaddr = palloc(mem_pool);
if (page_phyaddr == NULL)
{ //失败时要将曾经已申请的虚拟地址和
//物理页全部回滚,在将来完成内存回收时再补充
return NULL;
}
page_table_add((void*)vaddr, page_phyaddr); // 在页表中做映射
vaddr += PG_SIZE; // 下一个虚拟页
}
return vaddr_start;
}

/* 从内核物理内存池中申请 1 页内存,
成功则返回其虚拟地址,失败则返回 NULL */
void* get_kernel_pages(uint32_t pg_cnt)
{
void* vaddr = malloc_page(PF_KERNEL, pg_cnt);
if (vaddr != NULL)
{ // 若分配的地址不为空,将页框清 0 后返回
memset(vaddr, 0, pg_cnt * PG_SIZE);
}
return vaddr;
}


/* 初始化内存池 */
static void mem_pool_init(uint32_t all_mem)
{
put_str(" mem_pool_init start\n");
uint32_t page_table_size = PG_SIZE * 256; // 页表大小= 1页的页目录表+第0和第768个页目录项指向同一个页表+
// 第769~1022个页目录项共指向254个页表,共256个页框
uint32_t used_mem = page_table_size + 0x100000; // 0x100000为低端1M内存
uint32_t free_mem = all_mem - used_mem;
uint16_t all_free_pages = free_mem / PG_SIZE; // 1页为4k,不管总内存是不是4k的倍数,
// 对于以页为单位的内存分配策略,不足1页的内存不用考虑了。
uint16_t kernel_free_pages = all_free_pages / 2;
uint16_t user_free_pages = all_free_pages - kernel_free_pages;

/* 为简化位图操作,余数不处理,坏处是这样做会丢内存。
好处是不用做内存的越界检查,因为位图表示的内存少于实际物理内存*/
uint32_t kbm_length = kernel_free_pages / 8; // Kernel BitMap的长度,位图中的一位表示一页,以字节为单位
uint32_t ubm_length = user_free_pages / 8; // User BitMap的长度.

uint32_t kp_start = used_mem; // Kernel Pool start,内核内存池的起始地址
uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE; // User Pool start,用户内存池的起始地址

kernel_pool.phy_addr_start = kp_start;
user_pool.phy_addr_start = up_start;

kernel_pool.pool_size = kernel_free_pages * PG_SIZE;
user_pool.pool_size = user_free_pages * PG_SIZE;

kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
user_pool.pool_bitmap.btmp_bytes_len = ubm_length;

/********* 内核内存池和用户内存池位图 ***********
* 位图是全局的数据,长度不固定。
* 全局或静态的数组需要在编译时知道其长度,
* 而我们需要根据总内存大小算出需要多少字节。
* 所以改为指定一块内存来生成位图.
* ************************************************/
// 内核使用的最高地址是0xc009f000,这是主线程的栈地址.(内核的大小预计为70K左右)
// 32M内存占用的位图是2k.内核内存池的位图先定在MEM_BITMAP_BASE(0xc009a000)处.
kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;

/* 用户内存池的位图紧跟在内核内存池位图之后 */
user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);
/******************** 输出内存池信息 **********************/
put_str(" kernel_pool_bitmap_start:");
put_int((int)kernel_pool.pool_bitmap.bits);
put_str(" kernel_pool_phy_addr_start:");
put_int(kernel_pool.phy_addr_start);
put_str("\n");
put_str(" user_pool_bitmap_start:");
put_int((int)user_pool.pool_bitmap.bits);
put_str(" user_pool_phy_addr_start:");
put_int(user_pool.phy_addr_start);
put_str("\n");

/* 将位图置0*/
bitmap_init(&kernel_pool.pool_bitmap);
bitmap_init(&user_pool.pool_bitmap);

/* 下面初始化内核虚拟地址的位图,按实际物理内存大小生成数组。*/
kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length; // 用于维护内核堆的虚拟地址,所以要和内核内存池大小一致

/* 位图的数组指向一块未使用的内存,目前定位在内核内存池和用户内存池之外*/
kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);

kernel_vaddr.vaddr_start = K_HEAP_START;
bitmap_init(&kernel_vaddr.vaddr_bitmap);
put_str(" mem_pool_init done\n");
}

/* 内存管理部分初始化入口 */
void mem_init()
{
put_str("mem_init start\n");
uint32_t mem_bytes_total = (*(uint32_t*)(0xb00));
mem_pool_init(mem_bytes_total); // 初始化内存池
put_str("mem_init done\n");
}

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
// /home/mouse/OS_mouse/tool/bochs/mouse/kernel/memory.h

#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H
#include "stdint.h"
#include "bitmap.h"

enum pool_flags{
PF_KERNEL = 1, //内核内存池
PF_USER = 2 //用户内存池
};

#define PG_P_1 1 // 页表项或页目录项存在属性位
#define PG_P_0 0 // 页表项或页目录项存在属性位
#define PG_RW_R 0 // R/W 属性位值,读/执行
#define PG_RW_W 2 // R/W 属性位值,读/写/执行
#define PG_US_S 0 // U/S 属性位值,系统级
#define PG_US_U 4 // U/S 属性位值,用户级


//虚拟地址池,用于虚拟地址管理
struct virtual_addr{
struct bitmap vaddr_bitmap; //虚拟地址的位图
uint32_t vaddr_start; //虚拟地址的起始地址
};

extern struct pool kernel_pool, user_pool;

void mem_init(void);
void* get_kernel_pages(uint32_t pg_cnt);

#endif

下面说一下大概流程:

首先在mem_init 函数中通过读取0xb00地址处的内容,来获取总内存大小,这个是之前我们写的loader.S文件中自动获取的,然后调用mem_pool_init进行实际的内存池初始化;

实际的内存管理主要分为几个部分:

  1. 内存池的初始化(主要包括先计算实际页表+低端1MB使用的空间,然后得到剩余的可用内存),同时也要给位图(用来管理这些位图)单独分配一块内存(0xc009a00),从而可以规划内核物理内存池,用户物理内存池的,虚拟内存池等的起始地址
  2. 然后是虚拟地址分配的函数:这里分为:先用位图扫描算法查找空闲的虚拟页,然后标记已经使用,最后返回虚拟地址
  3. 物理页的分配函数: 同样是扫描物理页的位图,然后计算对应的物理地址并返回
  4. 最重要的就是建立虚拟地址到物理地址的映射,并且当虚拟地址对应的物理地址对应的页目录项不存在的时候,也要分配一个物理页作为新的页表

这里总结一下我们申请内存的流程:

用户使用 malloc 等函数来申请,然后系统分配一个虚拟地址(但是暂时不分配实际的物理内存),然后操作系统如果是第一次访问这个新分配的虚拟地址的时候,发现如果还没有分配页表项,这个时候操作系统寻找一个新的物理页创建一个页表,然后建立当时分配的虚拟地址和页表(物理地址)之间的映射


F 线程

这里首先介绍线程,进程在后面介绍


F.1 什么是线程

还是引用书中的介绍:

在处理器数量不变的情况下,多任务操作系统采用了一种称为多道程序设计的方式,使处理器在所有任务之间来回切换,这样就给用户一种所有任务并行运行的错觉,通过任务调度器来实现

任务调度器就是操作系统中用于把任务轮流调度上处理器运行的一个软件模块,它是操作系统的一部分。调度器在内核中维护一个任务表(也称进程表、线程表或调度表),然后按照一定的算法,从任务表中选择一个任务,然后把该任务放到处理器上运行,当任务运行的时间片到期后,再从任务表中找另外一个任务放到处理器上运行,周而复始,让任务表中的所有任务都有机会运行。正是因为有了调度器,多任务操作系统才能得以实现,它是多任务系统的核心,它的好坏直接影响了系统的效率

优点是可以让每个任务都可以”同时”执行,缺点也有,那就是每次切换任务的时候也是需要耗费时间的,也就是会是整体的执行时间变长了一些,但这个代价可以使得一些重要紧急的任务及时完成

处理器只知道加电后按照程序计数器中的地址不断地执行下去,在不断执行的过程中,我们把程序计数器中的下一条指令地址所组成的执行轨迹称为程序的控制执行流,让我们再深入描述一下。执行流就是一段逻辑上独立的指令区域,是人为给处理器安排的处理单元。指令是具备“能动性”的数据,因此只有指令才有“执行”的能力,它相当于是动作的发出者,由它指导处理器产生相应的行为。指令是由处理器来执行的,它引领处理器“前进”的方向,用“流”来表示处理器中程序计数器的航向,借此比喻处理器依次把此区域中的指令执行完后,所形成的像河流一样曲直不一的执行轨迹、执行路径(由顺序执行指令及跳转指令导致)
执行流是独立的,它的独立性体现在每个执行流都有自己的栈、一套自己的寄存器映像和内存资源,这是 Intel 处理器在硬件上规定的,其实这正是执行流的上下文环境


F.2 进程的身份证–PCB

虽然我们知道了大概得多任务操作系统,但是这里会有很多问题

(1)要加载一个任务上处理器运行,任务由哪来?也就是说,调度器从哪里才能找到该任务?
(2)即使找到了任务,任务要在系统中运行,其所需要的资源从哪里获得?
(3)即使任务已经变成进程运行了,此进程应该运行多久呢?总不能让其独占处理器吧。
(4)即使知道何时将其换下处理器,那当前进程所使用的这一套资源(寄存器内容)应该存在哪里?
(5)进程被换下的原因是什么?下次调度器还能把它换上处理器运行吗?
(6)前面都说过了,进程独享地址空间,它的地址空间在哪里?
当然还有很多很多问题

为了解决这些问题,操作系统为每个进程提供了一个PCB,即程序控制块(Process Control Block),用它来记录与这个进程相关的信息,比如优先级、PID、进程状态等等,同时PCB的具体格式取决于操作系统到功能复杂度

最后,操作系统单独维护一个进程表,将所有的PCB结构加载到这个表,然后由调度器来找对应的PCB,从而获取相关的信息,将寄存器映像(里面会保存进程的”现场”)加载到处理,然后新的进程就开始运行了,同时新进程的栈使用的也是PCB里面的栈

通过上面的描述,会发现PCB一般都很大,通常都是以页为单位,我们的这个简易操作系统比较小,就只占一页


F.3 实现线程的方式

这里有两种方式,一种是内核实现(0特权级),一种是用户实现(3特权级),但是两种都是为了用户进程服务的,所以线程必须可以运行用户的代码。下面主要介绍一下二者的优缺点:

  • 用户程序实现

优点:

  1. 首先是容易移植,即便移动到一个不支持线程的操作系统上,同样也可以支持线程的用户程序
  2. 因为是用户程序自己实现,所以可以根据实际情况为某些线程加权调度
  3. 将线程的寄存器映像装载到 CPU 时,可以在用户空间完成,即不用陷入到内核态,这样就免去了进入内核时的入栈及出栈操作

缺点

  1. 进程中的某个线程出现了阻塞(通常由系统调用造成),但是操作系统并不知道这个进程中有线程,于是将整个进程挂起,这样所有的线程都无法运行了
  2. 同时用户实现的时候,没有保险的机制使处理让出使用权给子线程,只能调用类似 pthread_yield 或 pthread_exit 之类的方法使线程发扬“高风亮节”让出处理器使用权,此类方法通过回调方式触发进程内的线程调度器,让调度器有机会选择进程内的其他线程上处理器运行
  3. 同时虽然减少了陷入内核态的步骤,相当于提速了,但是整个进程占用的处理器的时间片是有限的,再分给线程,那其实反而抵消了内部调度带来的提速

  • 内核空间实现

注意,这里所说的“实现线程”是指由内核提供原生线程机制,用户进程中不再单独实现

优点:

相比在用户空间中实现线程,内核提供的线程相当于让进程多占了处理器资源,比如系统中运行有进程 A 和一传统型进程 B,此时进程 A 中显式创建了 3 个线程,这样一来,进程 A 加上主线程便有了 4 个线程,加上进程 B,内核调度器眼中便有了 5 个独立的执行流,尽管其中 4 个都属于进程 A,但对调度器来说这 4个线程和进程一样被调度,因此调度器调度完一圈后,进程 A 使用了 80%的处理器资源,这才是真正的提速

  1. 实现了较大幅度的提速,理由如上
  2. 当某一个线程阻塞的时候,因为是由内核空间实现的,所以只会阻塞这一个线程,其它线程并不受影响,也相当于提速

缺点

  1. 用户进程需要通过系统调用陷入内核,这多少增加了一些现场保护的栈操作,这还是会消耗
    些处理器时间,但和上面的大幅度提速相比,这不算什么大事

以上就是两种实现方式,这里我们选择使用内核空间实现,还比较快~

F.4 在内核空间实现线程

下面先来构造 PCB 相关的基础部分,这里添加一个文件夹thread和两个文件thread.h thread.c,下面还是直接看代码吧,我注释都写在代码里面了
同时Makefile也要更改,以后我会直接将修改的Makfile和添加的Makefile放在代码后面,大家可以自己展开参考代码:

F4.1 PCB/线程的实现

最后的效果就是可以一直打印创建的线程传入的参数,但现在还不能多线程,请大家慢慢往下看

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
// /home/mouse/OS_mouse/tool/bochs/mouse/thread/thread.h

#ifndef __KERNEL_THREAD_H
#define __KERNEL_THREAD_H
#include "stdint.h"

//自定义的通用函数类型,将在很多线程函数中作为形参
typedef void thread_func(void*);

//进程或线程的状态
enum task_status{
TASK_RUNNING,
TASK_READY,
TASK_BLOCKED,
TASK_WAITNG,
TASK_HANGING,
TASK_DIED
};

/*********** 中断栈 intr_stack *****************************
* 此结构用于中断发生时保护程序(线程或进程)的上下文环境:
* 进程或线程被外部中断或软中断打断时,会按照此结构压入上下文
* 寄存器,intr_exit 中的出栈操作是此结构的逆操作
* 此栈在线程自己的内核栈中位置固定,所在页的最顶端
***********************************************************/
struct intr_stack{
uint32_t vec_no; // kernel.S 宏 VECTOR 中 push %1 压入的中断号
uint32_t edi;
uint32_t esi;
uint32_t ebp;
uint32_t esp_dummy;
// 虽然 pushad 把 esp 也压入,但 esp 是不断变化的,所以会被 popad 忽略
uint32_t ebx;
uint32_t edx;
uint32_t ecx;
uint32_t eax;
uint32_t gs;
uint32_t fs;
uint32_t es;
uint32_t ds;
/* 以下由 cpu 从低特权级进入高特权级时压入 */
uint32_t err_code; // err_code 会被压入在 eip 之后
void (*eip) (void);
uint32_t cs;
uint32_t eflags;
void* esp;
uint32_t ss;
};

struct thread_stack {
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;

/* 线程第一次执行时,eip 指向待调用的函数 kernel_thread其他时候,eip 是指向 switch_to 的返回地址*/
/* switch_to是将来用来实现任务切换的函数 */
void (*eip) (thread_func* func, void* func_arg);

/************* 以下仅供第一次被调度上 cpu 时使用 ************/
/* 参数 unused_ret 只为占位置充数为返回地址 */
void (*unused_retaddr);
thread_func* function; // 由 kernel_thread 所调用的函数名
void* func_arg; // 由 kernel_thread 所调用的函数所需的参数
};


/* 进程或线程的 pcb,程序控制块 */
struct task_struct {
uint32_t* self_kstack; // 各内核线程都用自己的内核栈
enum task_status status;
uint8_t priority; // 线程优先级
char name[16];
uint32_t stack_magic; //栈的边界标记,用于检测栈的溢出
};

// 初始化线程栈---将待执行的函数和参数放到 thread_stack 中相应的位置
void thread_create(struct task_struct* pthread,thread_func function,void *func_arg);
//初始化线程的基本信息
void init_thread(struct task_struct* pthread,char*name,int prio);
//执行优先级为 prio 的线程,线程名为 name,线程执行的函数是 function(func_arg),返回线程的控制块
struct task_struct* thread_start(char* name,
int prio,
thread_func function,
void* func_arg);

#endif
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
// /home/mouse/OS_mouse/tool/bochs/mouse/thread/thread.c

#include "thread.h"
#include "string.h"
#include "stdint.h"
#include "global.h"
#include "memory.h"

#define PG_SIZE 4096

// 由kernel_thread 去执行 function(func_arg)
static void kernel_thread(thread_func* function,void* func_arg)
{
function(func_arg);
}

// 初始化线程栈---将待执行的函数和参数放到 thread_stack 中相应的位置
void thread_create(struct task_struct* pthread,thread_func function,void *func_arg)
{
//先预留中断使用栈的空间,在thread.h中定义
pthread->self_kstack -= sizeof(struct intr_stack);

//再留出来线程的空间
pthread->self_kstack -= sizeof(struct thread_stack);

struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack; //这个时候指向了栈的起始地址
kthread_stack->eip = kernel_thread; //线程首次调度从kernel_thread开始执行
kthread_stack->function = function;
kthread_stack->func_arg = func_arg;
kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0; //设栈初始值为0
}

//初始化线程的基本信息
void init_thread(struct task_struct* pthread,char*name,int prio)
{
memset(pthread,0,sizeof(*pthread)); //先清零所有内容
strcpy(pthread->name,name); //名称
pthread->status = TASK_RUNNING; //默认状态
pthread->priority = prio; //优先级
//self_kstack是线程自己内核态下使用的栈顶地址
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);
pthread->stack_magic = 0x22332233; //自定义的魔数
}

//执行优先级为 prio 的线程,线程名为 name,线程执行的函数是 function(func_arg),返回线程的控制块
struct task_struct* thread_start(char* name,
int prio,
thread_func function,
void* func_arg)
{
//pcb都位于内核空间,包括用户进程的pcb也在内核空间
struct task_struct* thread = get_kernel_pages(1); //获得1页空间
init_thread(thread,name,prio); //初始化进程
thread_create(thread,function,func_arg); //指定函数,参数

//将栈指针(ESP)切换到新线程的栈顶
asm volatile ("movl %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; ret" : : "g" (thread->self_kstack) : "memory");
return thread;
}
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
/***** /home/mouse/OS_mouse/tool/bochs/mouse/kernel/main.c *****/
#include "print.h"
#include "init.h"
#include "interrupt.h"
#include "debug.h"
#include "string.h"
#include "memory.h"
#include "thread.h"

void k_thread_one(void* arg);

int main()
{
put_str("I am kernel!\n");
init_all(); //初始化所有中断
//创建一个线程,线程名,优先级,线程函数名,参数
thread_start("k_thread_one",31,k_thread_one,"argA");
// asm volatile("sti") ; //暂时打开中断,这里先不打开
return 0;
}

void k_thread_one(void* arg)
{
/* 用 void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char * para = arg;
while(1)
{
put_str(arg);
}

}
1
2
3
4
5
6
7
8
# =================================================================================
# Makefile 子文件 /home/mouse/OS_mouse/tool/bochs/mouse/thread/Makefile
# =================================================================================

THREAD_SRCS := thread.c # 例如:thread.c, scheduler.c
THREAD_ASMS := # 例如:thread_switch.S

# 如果没有汇编文件,THREAD_ASMS 应为空
主Makefile: 点击查看更多
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
# =================================================================================
# Makefile 内核构建文件 /home/mouse/OS_mouse/tool/bochs/mouse/kernel/Makefile
# =================================================================================

# .PHONY 标明伪目标,它代表了一个需要被执行的动作或任务,而非一个需要被生成的文件
# 可以在本文件所在目录下 使用make all 、 make clean 等命令执行一系列任务
.PHONY: all kernel user clean img dirs

# ================================ 基础路径 =======================================
# 其中的 ":=" 表示立即展开使用,即右边的变量值会被立刻赋值,"$" 可以引用之前创建的变量
# =================================================================================
MOUSE_DIR := ..
BUILD_DIR := $(MOUSE_DIR)/build
BUILD_KERNEL_DIR := $(BUILD_DIR)/kernel
BUILD_USER_DIR := $(BUILD_DIR)/user
IMG_PATH := /home/mouse/OS_mouse/tool/bochs/hd60M.img

# ================================ 编译工具 =======================================
CC := gcc
ASM := nasm
LD := ld

# C编译器标志,这里指出输出32位,编译器不识别/使用内建函数,禁用栈保护机制,指定当前为独立式环境(标准库不存在),
# -I 分别包含需要的头文件路径(共用库,内核库,用户库,内核,设备)
CFLAGS := -m32 -fno-builtin -fno-stack-protector -ffreestanding \
-I$(MOUSE_DIR)/lib -I$(MOUSE_DIR)/lib/kernel -I$(MOUSE_DIR)/lib/user \
-I$(MOUSE_DIR)/kernel -I$(MOUSE_DIR)/device -I$(MOUSE_DIR)/thread -c
ASMFLAGS := -f elf
LDFLAGS := -m elf_i386 -Ttext 0xc0001500 -e main

# ===================== 包含子Makefile,导入子模块源文件 ===============================
include $(MOUSE_DIR)/lib/kernel/Makefile
include $(MOUSE_DIR)/lib/user/Makefile
include $(MOUSE_DIR)/device/Makefile
include $(MOUSE_DIR)/lib/Makefile
include $(MOUSE_DIR)/thread/Makefile

# ============================== 本目录源文件 ===========================================
KERNEL_SRCS := main.c init.c interrupt.c debug.c memory.c
KERNEL_ASMS := kernel.S

# ========================== 为各模块添加前缀路径 =======================================
# $(addprefix <prefix>,<names>)是一个内置函数,能将路径prefix添加到names前
# ======================================================================================
KERNEL_SRCS := $(addprefix $(MOUSE_DIR)/kernel/,$(KERNEL_SRCS))
KERNEL_ASMS := $(addprefix $(MOUSE_DIR)/kernel/,$(KERNEL_ASMS))

LIB_KERNEL_SRCS := $(addprefix $(MOUSE_DIR)/lib/kernel/,$(LIB_KERNEL_SRCS))
LIB_KERNEL_ASMS := $(addprefix $(MOUSE_DIR)/lib/kernel/,$(LIB_KERNEL_ASMS))

LIB_USER_SRCS := $(addprefix $(MOUSE_DIR)/lib/user/,$(LIB_USER_SRCS))
LIB_USER_ASMS := $(addprefix $(MOUSE_DIR)/lib/user/,$(LIB_USER_ASMS))

DEVICE_SRCS := $(addprefix $(MOUSE_DIR)/device/,$(DEVICE_SRCS))
DEVICE_ASMS := $(addprefix $(MOUSE_DIR)/device/,$(DEVICE_ASMS))

LIB_SRCS := $(addprefix $(MOUSE_DIR)/lib/,$(LIB_SRCS))
LIB_ASMS := $(addprefix $(MOUSE_DIR)/lib/,$(LIB_ASMS))

THREAD_SRCS := $(addprefix $(MOUSE_DIR)/thread/,$(THREAD_SRCS))
THREAD_ASMS := $(addprefix $(MOUSE_DIR)/thread/,$(THREAD_ASMS))

# ========================== 汇总全部源文件 ================================================
ALL_C_SRCS := $(KERNEL_SRCS) $(LIB_KERNEL_SRCS) $(LIB_USER_SRCS) $(DEVICE_SRCS) $(LIB_SRCS) $(THREAD_SRCS)
ALL_ASMS := $(KERNEL_ASMS) $(LIB_KERNEL_ASMS) $(LIB_USER_ASMS) $(DEVICE_ASMS) $(LIB_ASMS) $(THREAD_ASMS)

# ========================== 生成对应的 .o 文件路径 ========================================
# $(filter <pattern...>,<text>):这个函数用于从 <text>中筛选出符合模式 <pattern>的单词
# $(patsubst <pattern>,<replacement>,<text>):将 <text>中所有匹配 <pattern>的单词替换为
# <replacement>的形式,<pattern>中可以使用通配符 %
# 通过这个函数,将生成的.o文件分别生成到build路径下的不同路径(这里除了用户,都生成到/build/kernel)
# =========================================================================================
OBJS_KERNEL := \
$(patsubst $(MOUSE_DIR)/%.c,$(BUILD_KERNEL_DIR)/%.o,$(filter $(MOUSE_DIR)/%.c,$(ALL_C_SRCS))) \
$(patsubst $(MOUSE_DIR)/%.S,$(BUILD_KERNEL_DIR)/%.o,$(filter $(MOUSE_DIR)/%.S,$(ALL_ASMS)))

OBJS_USER := \
$(patsubst $(MOUSE_DIR)/lib/user/%.c,$(BUILD_USER_DIR)/%.o,$(filter $(MOUSE_DIR)/lib/user/%.c,$(ALL_C_SRCS))) \
$(patsubst $(MOUSE_DIR)/lib/user/%.S,$(BUILD_USER_DIR)/%.o,$(filter $(MOUSE_DIR)/lib/user/%.S,$(ALL_ASMS)))

KERNEL_BIN := $(BUILD_KERNEL_DIR)/kernel.bin

# ==================================== 主要规则 ==========================================
all: dirs kernel user

# 以@开头的指令,终端只会显示命令的输出,不显示命令本身--这里创建build文件夹
dirs:
@echo "Creating build directories..."
@mkdir -p $(BUILD_KERNEL_DIR) $(BUILD_USER_DIR)

kernel: $(KERNEL_BIN)
user: $(OBJS_USER)

$(KERNEL_BIN): $(OBJS_KERNEL) $(OBJS_USER)
@echo "Linking kernel object files to generate kernel.bin..."
$(LD) $(LDFLAGS) -o $@ $(filter $(BUILD_KERNEL_DIR)/%.o,$^) $(filter $(BUILD_USER_DIR)/%.o,$^)
@echo "Kernel linking completed!"

# ================================= 编译规则 ============================================
$(BUILD_KERNEL_DIR)/%.o: $(MOUSE_DIR)/%.c | dirs
@echo "Compiling C: $<"
@mkdir -p $(@D)
$(CC) $(CFLAGS) -o $@ $<

$(BUILD_KERNEL_DIR)/%.o: $(MOUSE_DIR)/%.S | dirs
@echo "Assembling: $<"
@mkdir -p $(@D)
$(ASM) $(ASMFLAGS) -o $@ $<

$(BUILD_USER_DIR)/%.o: $(MOUSE_DIR)/lib/user/%.c | dirs
@echo "Compiling user C: $<"
@mkdir -p $(@D)
$(CC) $(CFLAGS) -o $@ $<

$(BUILD_USER_DIR)/%.o: $(MOUSE_DIR)/lib/user/%.S | dirs
@echo "Assembling user S: $<"
@mkdir -p $(@D)
$(ASM) $(ASMFLAGS) -o $@ $<

# ================================== 镜像生成 ==========================================
img: $(KERNEL_BIN)
@echo "Writing kernel image to disk..."
dd if=$(KERNEL_BIN) of=$(IMG_PATH) bs=512 count=200 seek=9 conv=notrunc
@echo "Kernel image writing completed!"

# ================================== 清理规则 ==========================================
clean:
@echo "Cleaning build files..."
-rm -f $(BUILD_KERNEL_DIR)/*.o $(BUILD_KERNEL_DIR)/kernel.bin
-rm -f $(BUILD_USER_DIR)/*.o
@echo "Cleanup completed!"

在学习多线程调度之前,我们还要学一个数据结构,就是大家数据结构学过的,双向链表,用来维护内核中的各种队列(比如进程的就绪队列,锁的等待队列等等……)


F4.2 双向链表

话不多说,直接开写,位置放在内核库下,也就是/mouse/lib/kernel,创建 lsit.c list.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
// /home/mouse/OS_mouse/tool/bochs/mouse/lib/kernel/list.h
#include "global.h"
#include "stdint.h"

#ifndef __LIB_KERNEL_LIST_H
#define __LIB_KERNEL_LIST_H

#define offset(struct_type,member) (int)(&((struct_type*)0)->member)
#define elem2entry(struct_type, struct_member_name, elem_ptr) \
(struct_type*)((int)elem_ptr - offset(struct_type, struct_member_name))

//定义链表节点成员结构 ---- 结点钟不需要数据成员,只要求前驱和后继节点指针
struct list_elem{
struct list_elem* prev; //前驱结点
struct list_elem* next; //后继结点
};

// 链表结构,用来实现队列
struct list
{
struct list_elem head; // head 是队首,固定不变,不是第一个元素,第一个元素是 head.next
struct list_elem tail; // tail 是队尾,同样不变
};

//自定义函数类型function,用于list_traversal当做回调函数
typedef bool (function)(struct list_elem*, int arg);

//队列的具体实现
void list_init (struct list*);
void list_insert_before(struct list_elem* before, struct list_elem* elem);
void list_push(struct list* plist, struct list_elem* elem);
void list_iterate(struct list* plist);
void list_append(struct list* plist, struct list_elem* elem);
void list_remove(struct list_elem* pelem);
struct list_elem* list_pop(struct list* plist);
bool list_empty(struct list* plist);
uint32_t list_len(struct list* plist);
struct list_elem* list_traversal(struct list* plist, function func, int arg);
bool elem_find(struct list* plist, struct list_elem* obj_elem);

#endif
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
// /home/mouse/OS_mouse/tool/bochs/mouse/lib/kernel/list.c

#include "list.h"
#include "interrupt.h"
#include "stdint.h"

//初始化双向链表
void list_init(struct list* list)
{
list->head.prev = NULL;
list->head.next = &list->tail;
list->tail.prev = &list->head;
list->tail.next = NULL;
}

// 把链表元素elem插入在元素before之前
void list_insert_before(struct list_elem* before,struct list_elem* elem)
{
enum intr_status old_status = intr_disable(); //关闭中断

//更新前驱元素的后继元素为elem,暂时让before脱离链表
before->prev->next = elem;

//更新elem自己的前驱元素为before的前驱
elem->prev = before->prev;
elem->next = before; //befroe回到链表

before->prev = elem; //更新before的前驱

intr_set_status(old_status); //打开中断
}

// 添加元素到列表队首,类似栈push操作
void list_push(struct list* plist, struct list_elem* elem)
{
list_insert_before(plist->head.next, elem); // 在队头插入elem
}

// 追加元素到链表队尾,类似队列的先进先出操作
void list_append(struct list* plist, struct list_elem* elem)
{
list_insert_before(&plist->tail, elem); // 在队尾的前面插入
}

/* 使元素pelem脱离链表 */
void list_remove(struct list_elem* pelem)
{
enum intr_status old_status = intr_disable();

pelem->prev->next = pelem->next;
pelem->next->prev = pelem->prev;

intr_set_status(old_status);
}

/* 将链表第一个元素弹出并返回,类似栈的pop操作 */
struct list_elem* list_pop(struct list* plist)
{
struct list_elem* elem = plist->head.next;
list_remove(elem);
return elem;
}

/* 从链表中查找元素obj_elem,成功时返回true,失败时返回false */
bool elem_find(struct list* plist, struct list_elem* obj_elem)
{
struct list_elem* elem = plist->head.next;
while (elem != &plist->tail) {
if (elem == obj_elem)
{
return true;
}
elem = elem->next;
}
return false;
}

/* 把列表plist中的每个元素elem和arg传给回调函数func,
* arg给func用来判断elem是否符合条件.
* 本函数的功能是遍历列表内所有元素,逐个判断是否有符合条件的元素。
* 找到符合条件的元素返回元素指针,否则返回NULL. */
struct list_elem* list_traversal(struct list* plist, function func, int arg)
{
struct list_elem* elem = plist->head.next;
/* 如果队列为空,就必然没有符合条件的结点,故直接返回NULL */
if (list_empty(plist))
{
return NULL;
}

while (elem != &plist->tail)
{
if (func(elem, arg))
{ // func返回ture则认为该元素在回调函数中符合条件,命中,故停止继续遍历
return elem;
} // 若回调函数func返回true,则继续遍历
elem = elem->next;
}
return NULL;
}

/* 返回链表长度 */
uint32_t list_len(struct list* plist)
{
struct list_elem* elem = plist->head.next;
uint32_t length = 0;
while (elem != &plist->tail)
{
length++;
elem = elem->next;
}
return length;
}

/* 判断链表是否为空,空时返回true,否则返回false */
bool list_empty(struct list* plist)
{ // 判断队列是否为空
return (plist->head.next == &plist->tail ? true : false);
}

在实现双向链表之后就可以来进一步完善thread.cthread.h

F4.3 简单优先级调度基础 & 任务调度器

这里添加了很多内容,我这里还是直接给出最终的文件,相比于之前的文件大家可以参考注释理解,这里只做简要说明:
这个代码只要自己认真写/读一遍,基本都能理解原理

完整的调度过程需要三部分的配合
(1)时钟中断处理函数
(2)调度器 schedule
(3)任务切换函数 switch_to

所以这里还是涉及到中断处理函数,包括注册等等,也同样写在下面并给出完整文件(主要修改interrupt.c general_intr_handler() timer.c,然后添加了时钟的中断处理函数等),同时优化了打印异常信息时候的打印方式(添加了指定位置的清屏),也添加了设置光标的函数print.S

第三步的任务切换函数还涉及到对上下文的保护,我们创建一个文件switch.S,放在thread文件夹下;

其中上下文保护分为两个部分,第一部分是保存任务进入中断前的全部寄存器,目的是能让任务恢复到中断前;第二部分是保存这 4 个寄存器:esi、edi、ebx 和 ebp,目的是让任务恢复执行在任务切换发生时剩下尚未执行的内核代码,保证顺利走到退出中断的出口,利用第一部分保护的寄存器环境彻底恢复任务,这里第一部分已经在kernel.S文件中由intr%1entry实现

最后在init中添加初始化,在主函数中添加两个进程编译运行后进行验证:


F.5 源码 & 修改部分

话不多说,全是代码,在每个代码块上面简要说明修改添加了什么~

  • 添加修改光标位置的函数 set_cursor( )
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
;/home/mouse/OS_mouse/tool/bochs/mouse/lib/kernel/print.S

;----------------------------定义视频段的选择子---------------------------------
;一般要放在配置文件中,这里偷懒一下(因为只有三行)
TI_GDT equ 0 ;描述符表指示符​:0:使用全局描述符表;1:则表示使用局部描述符表​
RPL0 equ 0 ;请求特权级​:表示选择子的特权级。0是最高特权级(操作系统内核),还有1、2、3(用户通常为3)
SELECTOR_VIDEO equ (0X0003<<3) + TI_GDT + RPL0 ;​段选择子​:一个具体的值,用于在GDT中索引一个段描述符(0x0003表示全局描述符表的第四个描述符,也就是显存段的)


[bits 32]
section .text
put_int_buffer dq 0 ; 定义 8 字节缓冲区用于数字到字符的转换

;----------------------------- put_int -----------------------
;功能: 将小端字节序的数字变成对应的 ASCII 后,倒置
;输入:栈中参数为待打印的数字
;输出:在屏幕上打印十六进制数字,并不会打印前缀 0x
; 如打印十进制 15 时,只会直接打印 f,不会是 0xf
;--------------------------------------------------------------
global put_int ;声明为外部可调用
put_int:
pushad ; 保存所有通用寄存器状态
mov ebp, esp ; 设置基址指针为当前栈顶
mov eax, [ebp+4 * 9] ; 获取栈中参数(返回地址4字节 + pushad的8个4字节)
mov edx, eax ; 复制参数到EDX用于处理
mov edi, 7 ; 设置缓冲区起始偏移(指向最高字节位置)
mov ecx, 8 ; 循环计数器:32位数字对应8个十六进制位
mov ebx, put_int_buffer ; EBX指向转换缓冲区基地址

; 将32位数字按十六进制从低位到高位处理
.16based_4bits: ; 处理每个4位十六进制数字
and edx, 0x0000000F ; 取当前最低4位
cmp edx, 9 ; 判断是数字(0-9)还是字母(A-F)
jg .is_A2F ; 大于9则为字母
add edx, '0' ; 转换为数字字符ASCII
jmp .store
.is_A2F:
sub edx, 10 ; 计算字母偏移量(A-F对应10-15)
add edx, 'A' ; 转换为字母字符ASCII

; 将字符存储到缓冲区(高位字符在低地址)
.store:
mov [ebx+edi], dl ; 存储转换后的字符
dec edi ; 前移缓冲区位置(向低地址)
shr eax, 4 ; 右移4位处理下一个十六进制位
mov edx, eax ; 更新EDX为剩余未处理部分
loop .16based_4bits ; 循环处理所有8个十六进制位

; 准备打印:跳过高位连续的'0'
.ready_to_print:
inc edi ; 调整EDI到第一个字符位置(之前为-1)
.skip_prefix_0:
cmp edi, 8 ; 检查是否已处理完所有字符
je .full0 ; 如果全是0则跳转到全0处理
.go_on_skip:
mov cl, [put_int_buffer+edi] ; 读取当前字符
inc edi ; 指向下一个字符
cmp cl, '0' ; 检查是否为'0'
je .skip_prefix_0 ; 如果是'0'则继续跳过
dec edi ; 回退到第一个非0字符位置
jmp .put_each_num ; 开始打印非0部分
.full0:
mov cl, '0' ; 全0情况只打印单个'0'
.put_each_num:
push ecx ; 压栈字符参数供put_char使用
call put_char ; 调用字符打印函数
add esp, 4 ; 清理栈空间
inc edi ; 指向缓冲区下一个字符
mov cl, [put_int_buffer+edi] ; 读取下一个字符
cmp edi, 8 ; 检查是否已打印所有字符
jl .put_each_num ; 继续打印直到结束
popad ; 恢复所有通用寄存器
ret ; 函数返回

;----------------------------- put_str -----------------------
;功能: 通过put_char打印以0字符结尾的字符串
;输入:栈中参数为打印的字符串
;输出:无
;--------------------------------------------------------------
global put_str ;声明为外部可调用
put_str:
;由于本函数中只用到了 ebx 和 ecx,只备份这两个寄存器
push ebx
push ecx
xor ecx,ecx ;清零
mov ebx,[esp+12] ;备份的2个寄存器+返回地址(8+2)
.goon:
mov cl,[ebx]
cmp cl,0 ;如果处理到了字符串末尾('0'),则跳转到结束处返回
jz .str_over
push ecx ;为put_char函数传递参数
call put_char
add esp,4 ;回收参数所占用的空间
inc ebx ;让ebx指向下一个字符
jmp .goon ;循环判断字符串直到末尾
.str_over:
pop ebx ;还原寄存器
pop ecx
ret ;返回

;----------------------------- put_char -----------------------
;功能: 将栈中的一个字符写入光标所在处
;--------------------------------------------------------------
global put_char ;声明为外部可调用
put_char:
pushad ;备份32位寄存器环境 push all double,指压入所有双字节长的寄存器(32个字节)
;需保证gs为正确的视频段选择子,为保险,每次打印都为其赋值
mov ax,SELECTOR_VIDEO ;不能直接将立即数送入段寄存器
mov gs,ax

;------------------------------ 获取当前光标位置 ------------------------------
;先或获得高八位
mov dx,0x03d4 ;索引寄存器
mov al,0x0e ;用于提供光标位置的高八位
out dx,al ;将索引 0xe 写入索引寄存器
mov dx,0x03d5 ;通过读写0x3d5来获得或设置光标位置
in al,dx ;得到了光标位置的高八位,读入al
mov ah,al ;在写入ah寄存器(in指令要求源操作数和目标数必须一样都是8位/16位,所以这里多移动一次)

;再获取低 8 位
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
in al, dx

mov bx,ax ;将光标存入 bx(常用于bx位基址寻址)

;--------------------- 获取待打印字符并判断类型->跳转相关函数 --------------------

mov ecx ,[esp+36] ;返回地址占 4 字节,pushad占 32 字节 所以共36字节,然后就可以获得待打印的字符

cmp cl,0xd ;CR(回车)是0x0d
jz .is_carriage_return
cmp cl, 0xa ;LF(换行)是0x0a,这里二者都处理成回车换行(CRLF)
jz .is_line_feed

cmp cl,0x8 ;BS(backspace 退格)
jz .is_backspace
jmp .put_other ;其余字符

;--------------------------------- 相关函数的实现 -------------------------------
;----- 退格 -----
.is_backspace:
;这里退格之后,还添加了空格或者空字符0,这样就会覆盖后面的字符
dec bx ;bx--(光标--)
shl bx,1 ;bx<<1(相当于乘2,即转换为显存中的字节偏移量)

mov byte [gs:bx],0x20 ;填充字节为0或者空格(0x20是空格)
inc bx ;bx++ 写入属性
mov byte [gs:bx], 0x07 ;黑底白字
shr bx,1 ;bx>>1,恢复光标值
jmp .set_cursor ;更新光标位置

;----- 其它字符 -----
.put_other:
shl bx,1 ;bx<<1(相当于乘2,即转换为显存中的字节偏移量)

mov byte [gs:bx],cl ;填充字节为0或者空格(0x20是空格)
inc bx ;bx++ 写入属性
mov byte [gs:bx], 0x07 ;黑底白字
shr bx,1 ;bx>>1,恢复光标值
inc bx ;bx++,下一个光标值
cmp bx, 2000 ;如果小于2000,表示未写到显存的末尾,则去设置光标值
;如果超过2000,表示需要换行
jl .set_cursor ;更新光标位置(如果小于2000)

;----- 回车换行 -----
.is_line_feed: ;换行(\n)
.is_carriage_return: ;回车(\r) 光标移动到行首,这里统一都写成回车换行
xor dx,dx ;dx是被除数的高16位,清零
mov ax,bx ;ax是被除数的低16位
mov si,80 ;每行是80字符
div si ;计算行位置
sub bx,dx ;光标值减去除 80 的余数便是取整

.is_carriage_return_end: ;回车符处理结束
add bx,80 ;光标移动到下一行
cmp bx,2000 ;检查是否超出屏幕范围(80x25=2000)
.is_line_feed_end:
jl .set_cursor ;如果bx<2000则跳转到设置光标位置

;----- 滚屏 -----
;这里有两种方案,第一种是是要设置起始地址寄存器,优点是可以缓存16KB个字符,屏幕外的文本也可以很快的找回
; 第二种是固定屏幕,,直接搬运,优点是不用设置起始地址寄存器,缺点是只能缓存2000个字符
;这里书中介绍的是第二种,因为简单方便,易于理解

;第二种滚屏方法:
;1.将第 1~24 行的内容整块搬到第 0~23 行,也就是把第 0 行的数据覆盖。
;2.再将第 24 行,也就是最后一行的字符用空格覆盖,这样它看上去是一个新的空行。
;3.把光标移到第 24 行也就是最后一行行首
;搬运1~24行
.roll_screen: ;若超出屏幕大小,开始滚屏
cld
mov ecx,960 ;要搬运2000-80个字符,1920*2=3840字节
;一次搬运4字节,也就是960次
mov esi,0xc00b80a0 ;第1行行首---复制起始地址
mov edi,0xc00b8000 ;第0行行首---复制目标地址
rep movsd ;逐4字节复制(32)

mov ebx, 3840 ;最后一行首字符的第一个字节的偏移1920*2
mov ecx, 80 ;一行80个字符,一次一个字符(2字节),要移动80次a
;清理最后一行
.cls:
mov word [gs:ebx],0x0720 ;0x0720是黑底白字的空格键
add ebx,2 ;一次一个字符(字+属性)
loop .cls ;循环清理80次
mov bx,1920 ;设置光标为1920,也就是最后一行的首字符

;----- 设置光标 -----
.set_cursor:
;将光标设置为bx值
;---- 设置高8位 ----
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高 8 位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口 0x3d5 来获得或设置光标位置

mov al, bh
out dx, al

;---- 设置低8位 ----
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
;函数结尾
.put_char_done:
popad ;恢复寄存器环境
ret ;返回


global set_cursor ;声明外部可以调用,用来设置光标的位置 使用方法: set_cursor(number)
set_cursor:
pushad ;保存寄存器
mov bx,[esp+36] ;获取修改的位置

;先设置高8位
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高8位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置
mov al, bh
out dx, al

;;;;;;; 2 再设置低8位 ;;;;;;;;;
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
popad
ret

注意在print.h文件中声明一下刚刚添加的汇编函数

  • 添加中断错误的显示逻辑(在通用中断处理函数中),使用刚刚的设置光标功能

  • 添加中断的注册函数register_handler( )

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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
// /home/mouse/OS_mouse/tool/bochs/mouse/kernel/interrupt.c

#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "debug.h"
#include "print.h" //put_str
#include "io.h" //io操作,联合汇编


#define IDT_DESC_CNT 0x33 // 目前总共支持的中断数
#define PIC_M_CTRL 0x20 // 主片的控制端口是 0x20
#define PIC_M_DATA 0x21 // 主片的数据端口是 0x21
#define PIC_S_CTRL 0xa0 // 从片的控制端口是 0xa0
#define PIC_S_DATA 0xa1 // 从片的数据端口是 0xa1

#define EFLAGS_IF 0X00000200 //eflags 寄存器的 if 位为0
//获取 eflags 寄存器的值,EFLAG_VAR 是返回值
#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;
};

// 静态函数声明,非必须
// intr_handler 是自己定义的(interrupt.h) typedef void* intr_handler;
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.S中的中断处理函数入口函数

//添加部分:
static void exception_init(void); //完成一般中断处理函数注册及异常名称注册
static void general_intr_handler(uint8_t vec_nr); //通用的中断处理函数,一般在异常出现时处理
const char* intr_name[IDT_DESC_CNT]; //用于保留异常的名字
intr_handler idt_table[IDT_DESC_CNT]; //中断处理函数程序数组,在kernel.S 中定义的 intrXXentry是中断处理函数的入口,最终调用 idt_table 里面的函数


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

/*创建中断门描述符*/
/*
* 参数: 中断门描述符的指针,中断描述符内的属性,中断描述符内对应的中断处理函数
*
*/
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; //定义在global.h : 指向内核数据段的选择子
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]);
}
put_str("idt_desc_init done\n");
}

//添加部分
//通用的中断处理函数,一般在异常出现时处理
static void general_intr_handler(uint8_t vec_nr)
{
if(vec_nr == 0x27 || vec_nr == 0x2f) //IRQ7s和IRQ15会产生伪中断,无需处理,0x2f是从片8259A上的最后一个IRQ引脚,保留项
{
return;
}

//将光标置为0,从屏幕清理出一片打印异常信息的区域,方便阅读
set_cursor(0);
int cursor_pos = 0;
while(cursor_pos < 320 )
{
put_char(' '); //空格
cursor_pos++;
}

set_cursor(0); //重新设置为左上角
put_str("!!!!!!! excetion message begin !!!!!!!!\n");

set_cursor(88); // 从第 2 行第 8 个字符开始打印

put_str((char*)intr_name[vec_nr]); //打印名字哦

if (vec_nr == 14) // 若为 Pagefault,将缺失的地址打印出来并悬停,缺页异常
{
int page_fault_vaddr = 0;
asm ("movl %%cr2, %0" : "=r" (page_fault_vaddr)); // cr2 是存放造成 page_fault 的地址
put_str("\npage fault addr is ");put_int(page_fault_vaddr);
}
put_str("\n!!!!!!! excetion message end !!!!!!!!\n");
// 能进入中断处理程序就表示已经处在关中断情况下
// 不会出现调度进程的情况。故下面的死循环不会再被中断
while(1);
}

//页中断,我当时调试使用的,大家可以不用管,现在移植到上面的通用中断中判断
static void page_fault_handler(uint8_t vec_nr)
{
if(vec_nr == 0x27 || vec_nr == 0x2f) //IRQ7s和IRQ15会产生伪中断,无需处理,0x2f是从片8259A上的最后一个IRQ引脚,保留项
{
return;
}
put_str("int vector : 0x");
put_int(vec_nr);
put_str("\n");

uint32_t faulting_address;
asm volatile ("mov %%cr2, %0" : "=r" (faulting_address)); // 读取CR2
put_str("Page fault at address: 0x");
put_int(faulting_address); // 十六进制打印
put_str("\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"; // 先统一赋值为 unknow
}
// idt_table[14] = page_fault_handler; //页错误处理函数
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";
}

/*开中断并且返回开中断前的状态 intr_statusl类型定义再interrupt.h中 */
/*这里在每个if分支都添加return语句,可以让程序更加快,但更大,空间换时间*/
enum intr_status intr_enable()
{
enum intr_status old_status;
if(INTR_ON == intr_get_status())
{
old_status = INTR_ON;
return old_status;
}
else
{
old_status = INTR_OFF;
asm volatile("sti"); //开中断,sti指令将IF置1
return old_status;
}
}

/*关中断并且返回关中断前的状态 */
enum intr_status intr_disable()
{
enum intr_status old_status;
if(INTR_ON == intr_get_status())
{
old_status = INTR_ON;
asm volatile("cli" : : : "memory"); //关中断,cli指令将IF位置置0
return old_status;
}
else
{
old_status = INTR_OFF;
return old_status;
}
}

/*将中断状态设置成 status*/
enum intr_status intr_set_status(enum intr_status status)
{
return (status & INTR_ON) ?intr_enable() : intr_disable();
}

/*获取当前中断状态*/
enum intr_status intr_get_status()
{
uint32_t eflags = 0;
GET_EFLAGS(eflags); //宏实现
return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}

//注册安装中断处理函数,vector_no表示安装在中断处理程序数组的第几个元素中,function是中断处理函数
void register_handler(uint8_t vector_no, intr_handler function)
{
idt_table[vector_no] = function;
}


/*完成有关中断的所有初始化工作*/
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" ::""(idt_operand));
put_str("idt_init done\n");
}
  • 头文件添加声明
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
// /home/mouse/OS_mouse/tool/bochs/mouse/kernel/interrupt.h

#ifndef __INTERRUPUT_H
#define __INTERRUPUT_H
#include "stdint.h"

typedef void* intr_handler;

//初始化
void idt_init();
//注册安装中断处理函数,vector_no表示安装在中断处理程序数组的第几个元素中,function是中断处理函数
void register_handler(uint8_t vector_no, intr_handler function);

/*定义中断的两种状态
* INTR_OFF 值为0,表示关中断
* INTR_ON 值为1,表示开中断
*/
enum intr_status{ //中断状态
INTR_OFF,
INTR_ON
};


enum intr_status intr_enable(); /*开中断并且返回开中断前的状态*/
enum intr_status intr_disable(); /*关中断并且返回关中断前的状态 */
enum intr_status intr_get_status(); /*获取当前中断状态*/
enum intr_status intr_set_status(enum intr_status status); /*将中断状态设置成 status*/
#endif
  • 添加全局的时间片变量ticks

  • 添加时钟中断处理函数,并且在初始化中进行注册

  • 中断处理函数负责对时间片–,并根据情况触发任务调度器

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
// /home/mouse/OS_mouse/tool/bochs/mouse/device/timer.c
#include "timer.h"
#include "io.h"
#include "print.h"
#include "thread.h"
#include "list.h"
#include "interrupt.h"
#include "debug.h"

#define IRQ0_FREQUENCY 100 //100HZ
#define INPUT_FREQUENCY 1193180 //计数器0的工作脉冲信号频率
#define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY //通过宏来计算初值
#define CONTRER0_PORT 0x40 //端口2
#define COUNTER0_NO 0 //计数器0
#define COUNTER_MODE 2 //工作模式 方式2
#define READ_WRITE_LATCH 3 //读写方式,3表示先读写低8位,再读写高8位
#define PIT_CONTROL_PORT 0x43 //控制字寄存器的端口

uint32_t ticks; //ticks是内核自中断开启以来的总共的滴答数

// 把操作的计数器 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);
}

//时钟的中断函数
static void intr_timer_handler(void)
{
struct task_struct* cur_thread = running_thread(); //获取当前正在运行的线程,将其复制给PCB指针cur_thread

ASSERT(cur_thread->stack_magic == 0x22332233); //检查堆栈是否溢出

cur_thread->elapsed_ticks++; //记录此线程占用cpu的时间
ticks++; //内核从第一次进入中断至今的时间

if(cur_thread->ticks == 0) //进程的时间片用完,则开始调度新的进程上cpu
{
schedule(); //调度器
}
else
{
cur_thread->ticks--;
}
}


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

register_handler(0x20,intr_timer_handler); //注册时钟的中断处理函数
put_str("timer_init done\n");
}
  • 添加任务调度器函数,并且添加 完善main为主线程 的函数

  • 修改初始化函数

  • 修改任务开始的逻辑,引入主函数线程判断等等

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
152
153
154
155
156
157
// /home/mouse/OS_mouse/tool/bochs/mouse/thread/thread.c

#include "thread.h"
#include "string.h"
#include "stdint.h"
#include "global.h"
#include "memory.h"
#include "interrupt.h"
#include "print.h"
#include "debug.h"

#define PG_SIZE 4096

struct task_struct* main_thread; //主进程PCB
struct list thread_ready_list; //就绪队列
struct list thread_all_list; //所有任务队列
static struct list_elem* thread_tag; //用于保存队列中的线程结点

extern void switch_to(struct task_struct* cur, struct task_struct* next); //实现任务切换

//获取当前任务的PCB指针
struct task_struct* running_thread()
{
uint32_t esp;
asm volatile("mov %%esp,%0":"=g" (esp));
return (struct task_struct*)(esp & 0xfffff000); //取esp整数部分,高20位
}


// 由kernel_thread 去执行 function(func_arg)
static void kernel_thread(thread_func* function,void* func_arg)
{
intr_enable(); //执行前打开中断,防止时钟中断被屏蔽,从而无法调度其它线程
function(func_arg);
}

// 初始化线程栈---将待执行的函数和参数放到 thread_stack 中相应的位置
void thread_create(struct task_struct* pthread,thread_func function,void *func_arg)
{
//先预留中断使用栈的空间,在thread.h中定义
pthread->self_kstack -= sizeof(struct intr_stack);

//再留出来线程的空间
pthread->self_kstack -= sizeof(struct thread_stack);

struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack; //这个时候指向了栈的起始地址
kthread_stack->eip = kernel_thread; //线程首次调度从kernel_thread开始执行
kthread_stack->function = function;
kthread_stack->func_arg = func_arg;
kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0; //设栈初始值为0
}

//初始化线程的基本信息
void init_thread(struct task_struct* pthread,char*name,int prio)
{
memset(pthread,0,sizeof(*pthread)); //先清零所有内容
strcpy(pthread->name,name); //名称

//由于把main函数也封装成一个线程,并且也是一直运行的,所以直接设置成TASK_RUNNING,其余设置成就绪态
if(pthread == main_thread)
{
pthread->status = TASK_RUNNING;
}
else
{
pthread->status = TASK_READY;
}

//self_kstack是线程自己内核态下使用的栈顶地址
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);
pthread->priority = prio; //优先级
pthread->ticks = prio; //时间片
pthread->elapsed_ticks = 0; //0表示还没有开始运行
pthread->pgdir = NULL;
pthread->stack_magic = 0x22332233; //自定义的魔数,同时它也是栈的边界
}

//执行优先级为 prio 的线程,线程名为 name,线程执行的函数是 function(func_arg),返回线程的控制块
struct task_struct* thread_start(char* name,
int prio,
thread_func function,
void* func_arg)
{
//pcb都位于内核空间,包括用户进程的pcb也在内核空间
struct task_struct* thread = get_kernel_pages(1); //获得1页空间
init_thread(thread,name,prio); //初始化进程
thread_create(thread,function,func_arg); //指定函数,参数

//确保之前不在就绪队列中
ASSERT(!elem_find(&thread_ready_list,&thread->general_tag));
list_append(&thread_ready_list,&thread->general_tag); //加入就绪线程队列

//确保之前不在全部线程队列中
ASSERT(!elem_find(&thread_all_list,&thread->all_list_tag));
list_append(&thread_all_list,&thread->all_list_tag); //加入全部线程队列

//将栈指针(ESP)切换到新线程的栈顶
//asm volatile ("movl %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; ret" : : "g" (thread->self_kstack) : "memory");

return thread;
}

// 下面来将kernel中的main函数完善为主线程
static void make_main_thread(void)
{
//main线程很早就已经运行了,所以loader.S 中进入内核时的 mov esp,0xc009f00
//就是为其预留的PCB,因此pcb的地址就是 0xc009e000
//所以不需用get_kernel_pages获得1页空间
main_thread = running_thread(); //获取当前任务指针
init_thread(main_thread,"main",31);

//main函数是当前线程,当前线程不在thread_ready_list中
//所以只用将它添加在thread_all_list中
ASSERT(!elem_find(&thread_all_list,&main_thread->all_list_tag)); //确保之前不在全部线程队列中
list_append(&thread_all_list,&main_thread->all_list_tag); //加入全部线程队列
}

//任务调度器
void schedule()
{
ASSERT(intr_get_status() == INTR_OFF);

struct task_struct* cur = running_thread(); //获取当前进程PCB
if(cur->status == TASK_RUNNING)
{
//若这个线程知识cpu时间片到了,就加入到就绪队列尾
ASSERT(!elem_find(&thread_ready_list,&cur->general_tag));
list_append(&thread_ready_list,&cur->general_tag);
cur->ticks = cur->priority; //重新设置ticks为priority
cur->status = TASK_READY;
}
else
{
//什么都不用干,因为已经不在运行了,并且当前线程也不在就绪队列中
//只用取出就绪列表的下一个任务即可
}
ASSERT(!list_empty(&thread_ready_list)); //就绪列表不为空
thread_tag = NULL; //thread_tag清空
//准备将就绪队列中的第一个就绪线程弹出,并放入CPU
thread_tag = list_pop(&thread_ready_list);
struct task_struct* next = elem2entry(struct task_struct,general_tag,thread_tag); //寻找弹出的就绪队列的PCB结构体头
next->status = TASK_RUNNING;
switch_to(cur,next); //切换进程
}

//初始化线程
void thread_init(void)
{
put_str("thread_init start\n");

list_init(&thread_ready_list);
list_init(&thread_all_list);

//将当前的main函数创建为线程
make_main_thread();
put_str("thread_init done");
}
  • 添加时间调度器的声明,以及完善thread.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
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
// /home/mouse/OS_mouse/tool/bochs/mouse/thread/thread.h

#ifndef __KERNEL_THREAD_H
#define __KERNEL_THREAD_H
#include "stdint.h"
#include "list.h"
#include "debug.h"

//自定义的通用函数类型,将在很多线程函数中作为形参
typedef void thread_func(void*);

//进程或线程的状态
enum task_status{
TASK_RUNNING,
TASK_READY,
TASK_BLOCKED,
TASK_WAITNG,
TASK_HANGING,
TASK_DIED
};

/*********** 中断栈 intr_stack *****************************
* 此结构用于中断发生时保护程序(线程或进程)的上下文环境:
* 进程或线程被外部中断或软中断打断时,会按照此结构压入上下文
* 寄存器,intr_exit 中的出栈操作是此结构的逆操作
* 此栈在线程自己的内核栈中位置固定,所在页的最顶端
***********************************************************/
struct intr_stack{
uint32_t vec_no; // kernel.S 宏 VECTOR 中 push %1 压入的中断号
uint32_t edi;
uint32_t esi;
uint32_t ebp;
uint32_t esp_dummy;
// 虽然 pushad 把 esp 也压入,但 esp 是不断变化的,所以会被 popad 忽略
uint32_t ebx;
uint32_t edx;
uint32_t ecx;
uint32_t eax;
uint32_t gs;
uint32_t fs;
uint32_t es;
uint32_t ds;
/* 以下由 cpu 从低特权级进入高特权级时压入 */
uint32_t err_code; // err_code 会被压入在 eip 之后
void (*eip) (void);
uint32_t cs;
uint32_t eflags;
void* esp;
uint32_t ss;
};

struct thread_stack {
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;

/* 线程第一次执行时,eip 指向待调用的函数 kernel_thread其他时候,eip 是指向 switch_to 的返回地址*/
/* switch_to是将来用来实现任务切换的函数 */
void (*eip) (thread_func* func, void* func_arg);

/************* 以下仅供第一次被调度上 cpu 时使用 ************/
/* 参数 unused_ret 只为占位置充数为返回地址 */
void (*unused_retaddr);
thread_func* function; // 由 kernel_thread 所调用的函数名
void* func_arg; // 由 kernel_thread 所调用的函数所需的参数
};


/* 进程或线程的 pcb,程序控制块 */
struct task_struct {
uint32_t* self_kstack; // 各内核线程都用自己的内核栈,注意为首位
enum task_status status; // 任务状态
uint8_t priority; // 线程优先级
char name[16]; // 名称
uint8_t ticks; // 每次在处理器上执行的时间滴答数,每次时钟中断都会对其减1,到0则换下处理器
uint32_t elapsed_ticks; // 从cpu运行至今占用了多少cpu滴答数
struct list_elem general_tag; //用于线程在一般队列的结点
struct list_elem all_list_tag; //用于线程队列thread_all_list的结点
uint32_t* pgdir; //进程自己的虚拟地址
uint32_t stack_magic; // 栈的边界标记,用于检测栈的溢出,注意为尾部
};

// 初始化线程栈---将待执行的函数和参数放到 thread_stack 中相应的位置
void thread_create(struct task_struct* pthread,thread_func function,void *func_arg);
//初始化线程的基本信息
void init_thread(struct task_struct* pthread,char*name,int prio);
//执行优先级为 prio 的线程,线程名为 name,线程执行的函数是 function(func_arg),返回线程的控制块
struct task_struct* thread_start(char* name,
int prio,
thread_func function,
void* func_arg);
struct task_struct* running_thread();//获取当前任务的PCB指针
void schedule(); //任务调度器
void thread_init(void); //初始化线程

#endif
  • 添加切换任务的函数–同时添加保护上下文的汇编代码
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
; /home/mouse/OS_mouse/tool/bochs/mouse/thread/switch.S
[bits 32]
section .text
global switch_to
switch_to:
;栈中此处是返回地址
push esi
push edi
push ebx
push ebp

mov eax, [esp + 20] ; 得到栈中的参数cur, cur = [esp+20]
mov [eax], esp ; 保存栈顶指针esp. task_struct的self_kstack字段,
; self_kstack在task_struct中的偏移为0,
; 所以直接往thread开头处存4字节便可。

;------------------ 以上是备份当前线程的环境,下面是恢复下一个线程的环境 ----------------

mov eax, [esp + 24] ; 得到栈中的参数next, next = [esp+24]
mov esp, [eax] ; pcb的第一个成员是self_kstack成员,用来记录0级栈顶指针,
; 用来上cpu时恢复0级栈,0级栈中保存了进程或线程所有信息,包括3级栈指针
pop ebp
pop ebx
pop edi
pop esi
ret ; 返回到上面switch_to下面的那句注释的返回地址,
; 未由中断进入,第一次执行时会返回到kernel_thread
  • 添加主初始化中线程初始化函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// /home/mouse/OS_mouse/tool/bochs/mouse/kernel/init.c
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"

//初始化所有模块
void init_all()
{
put_str("init_all\n");
idt_init(); //中断
timer_init(); //定时器设备 PIT
mem_init(); //内存管理初始化
thread_init(); //线程初始化
}
  • 在主函数中调用创建线程,最后观察结果
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
/***** /home/mouse/OS_mouse/tool/bochs/mouse/kernel/main.c *****/
#include "print.h"
#include "init.h"
#include "interrupt.h"
#include "debug.h"
#include "string.h"
#include "memory.h"
#include "thread.h"

void k_thread_one(void* arg);
void k_thread_two(void* arg);

int main()
{
put_str("I am kernel!\n");
init_all(); //初始化所有
//创建一个线程,线程名,优先级,线程函数名,参数
thread_start("k_thread_one",31,k_thread_one,"argA");
thread_start("k_thread_two",31,k_thread_two,"argB");

intr_enable(); //打开中断,使时钟中断起作用

while(1)
{
put_str("Main");
}
return 0;
}

void k_thread_one(void* arg)
{
/* 用 void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char * para = arg;
while(1)
{
put_str(arg);
}
}

void k_thread_two(void* arg)
{
/* 用 void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char * para = arg;
while(1)
{
put_str(arg);
}
}

注意,在编译的时候如果有报错,可以检查是不是缺少头文件包含,或者是声明等等

然后直接运行,就可以发现三个任务可以交替运行(交替打印出不同的字符串),但是会发现两个问题:

  • 有时候三个打印的字符串可能会错开、有空格、间断、等等
  • 会发生报错,#GP General Protection Exception,一般保护性异常

这两个都会在下一章的同步机制中讲解,所以下一章见~


G. 同步

这里从上一节出现的报错,以及错误混乱现象来入手,其实主要是因为在打印的逻辑中:

  1. 获取光标
  2. 光标转化为字节地址,在该地址写入字符
  3. 更新光标

这三个任务其实必须要一起执行,否则如果中途被切换了就会出现打印错误的打印情况,这其实就是原子(不可再分)的体现,所以这里我们可以通过开关中断的情况来保证原子性

为了方便,这里只用在put_str的前后添加开关中断,这里修改主函数:

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
/***** /home/mouse/OS_mouse/tool/bochs/mouse/kernel/main.c *****/
#include "print.h"
#include "init.h"
#include "interrupt.h"
#include "debug.h"
#include "string.h"
#include "memory.h"
#include "thread.h"

void k_thread_one(void* arg);
void k_thread_two(void* arg);

int main()
{
put_str("I am kernel!\n");
init_all(); //初始化所有
//创建一个线程,线程名,优先级,线程函数名,参数
thread_start("k_thread_one",31,k_thread_one,"AAAA ");
thread_start("k_thread_two",8,k_thread_two,"BBBB ");

// intr_disable(); //关中断
// intr_enable(); //打开中断,使时钟中断起作用

while(1)
{
intr_disable(); //关中断
put_str("MMMM ");
intr_enable(); //开中断
}
return 0;
}

void k_thread_one(void* arg)
{
/* 用 void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char * para = arg;
while(1)
{
intr_disable(); //关中断
put_str(arg);
intr_enable(); //开中断
}
}

void k_thread_two(void* arg)
{
/* 用 void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char * para = arg;
while(1)
{
intr_disable(); //关中断
put_str(arg);
intr_enable(); //开中断
}
}

其实这里还有一个问题,是我遇到的,就是如果输出的字符串是5位,比如”11111”,三个任务输出长度都需要一样,那么可以正常执行
但是如果修改长度,则会报错,不限于错误操作码等

初步原因是因为恰好5位的时候避开了同时写入么?这里还没有找到原因,打算通过完成锁机制后再查看
(好像找到了,vaddr_get,这个函数之前写的有问题,现在已经修改了)

当正确添加锁机制(使用互斥锁实现控制台输出)之后便没有了问题了

同时检查代码的时候发现之前idt_init中有个有个位置写错了,在下面指出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*完成有关中断的所有初始化工作*/
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" ::""(idt_operand));
// put_str("idt_init done\n");

//修改成下面的样子

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");
}

G.1 锁/信号量

这个概念相信很多人都了解过,这里还是简要介绍一下,然后具体看代码:

G1.1 概念

信号量就是个计数器,它的计数值是自然数,用来记录所积累信号的数量。这里的信号是个泛指,取决于信号量的实际应用环境

因此对信号量的加法操作是用 up 表示,减法操作是用 down 表示。下面介绍下这两种操作。增加操作 up 包括两个微操作。
(1)将信号量的值加 1。
(2)唤醒在此信号量上等待的线程。
减少操作 down 包括三个子操作。
(1)判断信号量是否大于 0。
(2)若信号量大于 0,则将信号量减 1。
(3)若信号量等于 0,当前线程将自己阻塞,以在此信号量上等待。
信号量是个全局共享变量,up 和 down 又都是读写这个全局变量的操作,而且它们都包含一系列的子操作,因此它们必须都是原子操作

G1.2 实现线程的阻塞与唤醒

还是在thread.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
//当前线程阻塞,并且标志其状态为stat
void thread_block(enum task_status stat)
{
//stat 取值为 TASK_BLOCKED、TASK_WAITING、TASK_HANGING,
ASSERT( (stat == TASK_BLOCKED) || (stat == TASK_WAITNG) ||(stat == TASK_HANGING) );
enum intr_status old_status = intr_disable(); //关中断
struct task_struct* cur_thread = running_thread(); //获取当前线程pcb
cur_thread->status = stat;
schedule(); //调度器
//等待当前线程解除阻塞后继续运行下面的intr_set_status
intr_set_status(old_status);
}

//解除pthread线程的阻塞状态
void thread_unblock(struct task_struct* pthread)
{
enum intr_status old_status = intr_disable(); //关中断
ASSERT( (pthread->status == TASK_BLOCKED) || (pthread->status == TASK_WAITNG) ||(pthread->status == TASK_HANGING) );
if(pthread->status != TASK_READY)
{
ASSERT(!elem_find(&thread_ready_list,&pthread->general_tag));
if (elem_find(&thread_ready_list, &pthread->general_tag))
{
PANIC("thread_unblock: blocked thread in ready_list\n");
}
list_push(&thread_ready_list, &pthread->general_tag); //放到队列的最前面,使其尽快得到调度
pthread->status = TASK_READY;
}
intr_set_status(old_status);
}
1
2
3
4
5
//当前线程阻塞,并且标志其状态为stat
void thread_block(enum task_status stat);

//解除pthread线程的阻塞状态
void thread_unblock(struct task_struct* pthread);

G1.3 锁的实现

下面来实现锁,要添加两个文件,在thread文件夹下添加sync.c stnc.h,实现一个简单的信号量(只有0和1)

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
// /home/mouse/OS_mouse/tool/bochs/mouse/thread/sync.c

#include "sync.h"
#include "stdint.h"
#include "interrupt.h"

//初始化信号量
void sema_init(struct semaphore* psema,uint8_t value)
{
psema->value = value; //为信号量赋初值
list_init(&psema->waiters); //初始化信号量的等待队列
}

//初始化锁
void lock_init(struct lock* plock)
{
plock->holder = NULL; //初始为空
plock->holder_repeat_nr = 0;
sema_init(&plock->semaphore,1); //信号量初始值为1
}


//这里注意我们的信号量操作的值只存在0和1两种选项
//信号量down操作
void sema_down(struct semaphore* psema)
{
enum intr_status old_status = intr_disable(); //保证原子操作

while(psema->value == 0) //若为0,表示已经被别人持有
{
//当前线程不应该已在信号量的 waiters 队列中
ASSERT(!elem_find(&psema->waiters,&running_thread()->general_tag));
if (elem_find(&psema->waiters, &running_thread()->general_tag))
{
PANIC("sema_down: thread blocked has been in waiters_list\n");
}
//若信号量的值等于 0,则当前线程把自己加入该锁的等待队列,并且阻塞该队列
list_append(&psema->waiters, &running_thread()->general_tag);
thread_block(TASK_BLOCKED); // 阻塞线程,直到被唤醒
}
psema->value--; //若不是0,则下面的操作,也就是获得锁
ASSERT(psema->value == 0);
intr_set_status(old_status); //恢复中断
}

//信号量的up操作
void sema_up(struct semaphore* psema)
{
enum intr_status old_status = intr_disable();
ASSERT(psema->value == 0);

if (!list_empty(&psema->waiters)) //等待队列非空
{
//获取等待队列的PCB
struct task_struct* thread_blocked = elem2entry(struct task_struct, general_tag, list_pop(&psema->waiters));
thread_unblock(thread_blocked); //释放锁
}
psema->value++;
ASSERT(psema->value == 1);
intr_set_status(old_status); //恢复中断
}

//获取锁 plock
void lock_acquire(struct lock* plock)
{
//排除曾经自己已经持有锁,但是还没有释放的情况
if(plock->holder != running_thread())
{
sema_down(&plock->semaphore); //对信号量down操作(获取信号量)
plock->holder = running_thread();
ASSERT(plock->holder_repeat_nr == 0);
plock->holder_repeat_nr = 1;
}
else
{
plock->holder_repeat_nr++;
}
}

//释放锁 plock
void lock_release(struct lock* plock)
{
ASSERT(plock->holder == running_thread());
if(plock->holder_repeat_nr >1) //自己获取自己锁
{
plock->holder_repeat_nr--;
return;
}
ASSERT(plock->holder_repeat_nr == 1);

plock->holder = NULL; //持有者置空放在v操作之前
plock->holder_repeat_nr = 0;
sema_up(&plock->semaphore); //释放锁对应的信号量
}
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
// /home/mouse/OS_mouse/tool/bochs/mouse/thread/sync.h

#ifndef __THREAD_SYNC_H
#define __THREAD_SYNC_H

#include "thread.h"
#include "list.h"
#include "stdint.h"

//信号量结构
struct semaphore{
uint8_t value;
struct list waiters;
};

//锁结构
struct lock{
struct task_struct* holder; //锁的持有者
struct semaphore semaphore; //用二元信号量实现锁
uint32_t holder_repeat_nr; //锁的持有者重复申请锁的次数
};


// /home/mouse/OS_mouse/tool/bochs/mouse/thread/sync.c

#include "sync.h"
#include "stdint.h"
#include "interrupt.h"

//初始化信号量
void sema_init(struct semaphore* psema,uint8_t value);
//初始化锁
void lock_init(struct lock* plock);

//这里注意我们的信号量操作的值只存在0和1两种选项
//信号量down操作
void sema_down(struct semaphore* psema);
//信号量的up操作
void sema_up(struct semaphore* psema);
//获取锁 plock
void lock_acquire(struct lock* plock);
//释放锁 plock
void lock_release(struct lock* plock);

#endif

G.2 用锁实现终端输出

这里写的终端,它不是真正意义上的终端,甚至连伪终端都算不上,我们只是通过它让输出变得更整洁。您看到了,它就是对各种锁操作的封装,完全就是锁的应用

我们将它写在device文件夹下添加console.h console.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
// /home/mouse/OS_mouse/tool/bochs/mouse/device/console.c

#include "console.h"
#include "sync.h"
#include "print.h"
#include "stdint.h"
#include "thread.h"

static struct lock console_lock; //控制台锁

//初始化终端
void console_init()
{
lock_init(&console_lock);
}

//获取终端
void console_acquire()
{
lock_acquire(&console_lock);
}

//释放终端
void console_release()
{
lock_release(&console_lock);
}

//终端输出字符串
void console_put_str(char* str)
{
console_acquire();
put_str(str);
console_release();
}

//终端输出字符
void console_put_char(uint8_t char_asci)
{
console_acquire();
put_char(char_asci);
console_release();
}

//终端输出十六进制整数
void console_put_int(uint32_t num)
{
console_acquire();
put_int(num);
console_release();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// /home/mouse/OS_mouse/tool/bochs/mouse/device/console.h

#ifndef __DEVICE_CONSOLE_H
#define __DEVICE_CONSOLE_H
#include "stdint.h"


//初始化终端
void console_init();
//获取终端
void console_acquire();
//释放终端
void console_release();
//终端输出字符串
void console_put_str(char* str);
//终端输出字符
void console_put_char(uint8_t char_asci);
//终端输出十六进制整数
void console_put_int(uint32_t num);
#endif

最后我们添加主函数,将原本的put_str 改成 console_put_str,验证结果:

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
/***** /home/mouse/OS_mouse/tool/bochs/mouse/kernel/main.c *****/
#include "print.h"
#include "init.h"
#include "interrupt.h"
#include "debug.h"
#include "string.h"
#include "memory.h"
#include "thread.h"
#include "console.h"

void k_thread_one(void* arg);
void k_thread_two(void* arg);

int main()
{
put_str("I am kernel!\n");
init_all(); //初始化所有
//创建一个线程,线程名,优先级,线程函数名,参数
thread_start("k_thread_one",31,k_thread_one,"AA ");
thread_start("k_thread_two",8,k_thread_two,"BBBB ");

// intr_disable(); //关中断
intr_enable(); //打开中断,使时钟中断起作用

while(1)
{
console_put_str("MMMM ");
}
return 0;
}

void k_thread_one(void* arg)
{
/* 用 void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char * para = arg;
while(1)
{
console_put_str(arg);
}
}

void k_thread_two(void* arg)
{
/* 用 void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char * para = arg;
while(1)
{
console_put_str(arg);
}
}

最后还是编译写入运行,会发现不会发生报错,并且运行的顺序不再是固定的,而是”随机”的,符合任务阻塞的现象,并且更改字符串长度后依旧可以正常运行,以后我们的输出就使用这个新的函数

ok呀,终端到此结束,虽然书中表示不会更新,但说不定会找点别的更新进去,因为后面打算看看linux的其它版本的源码来继续完善这个简单的操作系统


H 输入的实现

既然前面实现了终端的输出,那么肯定少不了输入,这里开始实现输入,即从键盘中获取输入的字符

H.1 原理简介

键盘是个独立的设备,在它内部有个叫作键盘编码器的芯片,通常是 Intel 8048 或兼容芯片,它的作用是:每当键盘上发生按键操作,它就向键盘控制器报告哪个键被按下,按键是否弹起。

这个键盘控制器可并不在键盘内部,它在主机内部的主板上,通常是 Intel 8042 或兼容芯片,它的作用是接收来自键盘编码器的按键信息,将其解码后保存,然后向中断代理发中断,之后处理器执行相应的中断处理程序读入 8042 处理保存过的按键信息

无论是按下键,或是松开键,当键的状态改变后,键盘中的 8048 芯片把按键对应的扫描码(通码或断码)发送到主板上的 8042 芯片,由 8042 处理后保存在自己的寄存器中,然后向 8259A 发送中断信号,这样处理器便去执行键盘中断处理程序,将 8042 处理过的扫描码从它的寄存器中读取出来,继续进行下一步处理

这个中断处理程序就是需要我们编写的,我们只能得到键的扫描码,并不会得到ASCII码,然后我们可以将他们俩转化,然后再将它交给put_char函数即可

当然书中还介绍了键盘扫描码的分类,这里就不多说了,我们直接来进行实战环节即可,因为这个也算是科普了


H.2 测试中断程序

在测试键盘的中断程序之前,我们先来添加优化一下之前写的代码,首先是”kernel.S”文件中,添加新的中断,因为我们之前只写到了0x20,同时修改宏IDT_DESC_CNT,因为我们的总数发生了变化

其实是为了方便演示,所以我们要暂时关闭掉时钟的中断,只打开键盘的中断,所以我们现在还要写8259A的中断屏蔽寄存器(pic_init()函数

下面开始操作:

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
; /home/mouse/OS_mouse/tool/bochs/mouse/kernel/kernel.S

; 略..............

;这里将中断都指向对应的函数[idt_table + %1*4]
;第 1 个参数 0x1e 是中断向量号,第二个参数主要是用来占位,表示是否自动压入错误码(ERROR_CODE)
VECTOR 0X00,ZERO ; 除法错误异常
VECTOR 0x01,ZERO ; 调试异常
VECTOR 0x02,ZERO ; 非屏蔽中断 (NMI)
VECTOR 0x03,ZERO ; 断点 (INT3)
VECTOR 0x04,ZERO ; 溢出 (INTO)
VECTOR 0x05,ZERO ; 边界范围超出 (BOUND)
VECTOR 0x06,ZERO ; 无效操作码 (UD2)
VECTOR 0x07,ZERO ; 设备不可用
VECTOR 0x08,ERROR_CODE ; 双重错误
VECTOR 0x09,ZERO ; 协处理器段超限
VECTOR 0x0a,ERROR_CODE ; 无效TSS
VECTOR 0x0b,ERROR_CODE ; 段不存在
VECTOR 0x0c,ZERO ; 栈段错误
VECTOR 0x0d,ERROR_CODE ; 通用保护错误
VECTOR 0x0e,ERROR_CODE ; 页错误
VECTOR 0x0f,ZERO ; 保留
VECTOR 0x10,ZERO ; x87浮点异常
VECTOR 0x11,ERROR_CODE ; 对齐检查
VECTOR 0x12,ZERO ; 机器检查
VECTOR 0x13,ZERO ; SIMD浮点异常
VECTOR 0x14,ZERO ; 虚拟化异常
VECTOR 0x15,ZERO ; 控制保护异常
VECTOR 0x16,ZERO ; 保留
VECTOR 0x17,ZERO ; 保留
VECTOR 0x18,ERROR_CODE ; 超线程技术异常
VECTOR 0x19,ZERO ; VMM通信异常
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 ; 通常用于系统调用或定时器中断,这里用来测试中断
VECTOR 0x21,ZERO ;键盘中断对应的入口
VECTOR 0x22,ZERO ;级联用的
VECTOR 0x23,ZERO ;串口 2 对应的入口
VECTOR 0x24,ZERO ;串口 1 对应的入口
VECTOR 0x25,ZERO ;并口 2 对应的入口
VECTOR 0x26,ZERO ;软盘对应的入口
VECTOR 0x27,ZERO ;并口 1 对应的入口
VECTOR 0x28,ZERO ;实时时钟对应的入口
VECTOR 0x29,ZERO ;重定向
VECTOR 0x2a,ZERO ;保留
VECTOR 0x2b,ZERO ;保留
VECTOR 0x2c,ZERO ;ps/2 鼠标
VECTOR 0x2d,ZERO ;fpu 浮点单元异常
VECTOR 0x2e,ZERO ;硬盘
VECTOR 0x2f,ZERO ;保留
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
// /home/mouse/OS_mouse/tool/bochs/mouse/kernel/interrupt.c

#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "debug.h"
#include "print.h" //put_str
#include "io.h" //io操作,联合汇编


#define IDT_DESC_CNT 0x33 // 目前总共支持的中断数,这里写的偏大一点
#define PIC_M_CTRL 0x20 // 主片的控制端口是 0x20
#define PIC_M_DATA 0x21 // 主片的数据端口是 0x21
#define PIC_S_CTRL 0xa0 // 从片的控制端口是 0xa0
#define PIC_S_DATA 0xa1 // 从片的数据端口是 0xa1

#define EFLAGS_IF 0x00000200 // eflags寄存器中的if位为1
//获取 eflags 寄存器的值,EFLAG_VAR 是返回值
#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;
};

// 静态函数声明,非必须
// intr_handler 是自己定义的(interrupt.h) typedef void* intr_handler;
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.S中的中断处理函数入口函数

//添加部分:
static void exception_init(void); //完成一般中断处理函数注册及异常名称注册
static void general_intr_handler(uint8_t vec_nr); //通用的中断处理函数,一般在异常出现时处理
const char* intr_name[IDT_DESC_CNT]; //用于保留异常的名字
intr_handler idt_table[IDT_DESC_CNT]; //中断处理函数程序数组,在kernel.S 中定义的 intrXXentry是中断处理函数的入口,最终调用 idt_table 里面的函数


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


//outb (PIC_M_DATA, 0xfe); //接受时钟产生的中断
outb(PIC_S_DATA,0xfd); //接收键盘中断
outb(PIC_S_DATA, 0xff); //其余全部关闭
put_str(" pic_init done\n");
}

//略 ....................

然后来开始写键盘的测试驱动,放在device文件夹下,创建keyboard.c/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
// /home/mouse/OS_mouse/tool/bochs/mouse/device/keyboard.c

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

#define KBD_BUF_PORT 0x60 //键盘buffer寄存器的端口号:0x60

//键盘中断处理函数
static void intr_keyboard_handler(void)
{
put_char('k');
//必须要读取输出缓冲区寄存器,否则8042不再继续响应键盘中断
uint8_t scancode = inb(KBD_BUF_PORT);
put_int(scancode); //输出一下看看
return;
}

//键盘初始化
void keyboard_init()
{
put_str("Keyboard init start\n");
register_handler(0x21,intr_keyboard_handler);
put_str("Keyboard init done\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
// /home/mouse/OS_mouse/tool/bochs/mouse/device/keyboard.h

#ifndef __DEVICE_KEYBOARD_H
#define __DEVICE_KEYBOARD_H
#include "stdint.h"

//键盘中断处理函数
static void intr_keyboard_handler(void);
//键盘初始化
void keyboard_init();

#endif
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
/***** /home/mouse/OS_mouse/tool/bochs/mouse/kernel/main.c *****/
#include "print.h"
#include "init.h"
#include "interrupt.h"
#include "debug.h"
#include "string.h"
#include "memory.h"
#include "thread.h"
#include "console.h"

void k_thread_one(void* arg);
void k_thread_two(void* arg);

int main()
{
put_str("I am kernel!\n");
init_all(); //初始化所有
//创建一个线程,线程名,优先级,线程函数名,参数
// thread_start("k_thread_one",31,k_thread_one,"AA ");
// thread_start("k_thread_two",8,k_thread_two,"BBBB ");

// intr_disable(); //关中断
intr_enable(); //打开中断,使时钟中断起作用

while(1)
{
// console_put_str("MMMM ");
}
return 0;
}

void k_thread_one(void* arg)
{
/* 用 void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char * para = arg;
while(1)
{
console_put_str(arg);
}
}

void k_thread_two(void* arg)
{
/* 用 void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char * para = arg;
while(1)
{
console_put_str(arg);
}
}

修改完之后主要还要修改对应的Makefile文件,添加编译的c文件,主函数中取消对多任务的注册,写成死循环即可

然后编译写入运行,对着终端按下键盘,就会发现屏幕上有输出(扫描码)


H.3 编写键盘驱动

下面的驱动主要通过扫描码到ASCII码的转化来实现,来编写一个映射表实现键盘的读入

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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
// /home/mouse/OS_mouse/tool/bochs/mouse/device/keyboard.c
#include "keyboard.h"
#include "print.h"
#include "interrupt.h"
#include "io.h"
#include "global.h"

#define KBD_BUF_PORT 0x60 // 键盘buffer寄存器端口号为0x60

/* 用转义字符定义部分控制字符 */
#define esc '\033' // 八进制表示字符,也可以用十六进制'\x1b'
#define backspace '\b'
#define tab '\t'
#define enter '\r'
#define delete '\177' // 八进制表示字符,十六进制为'\x7f'

/* 以上不可见字符一律定义为0 */
#define char_invisible 0
#define ctrl_l_char char_invisible
#define ctrl_r_char char_invisible
#define shift_l_char char_invisible
#define shift_r_char char_invisible
#define alt_l_char char_invisible
#define alt_r_char char_invisible
#define caps_lock_char char_invisible

/* 定义控制字符的通码和断码 */
#define shift_l_make 0x2a
#define shift_r_make 0x36
#define alt_l_make 0x38
#define alt_r_make 0xe038
#define alt_r_break 0xe0b8
#define ctrl_l_make 0x1d
#define ctrl_r_make 0xe01d
#define ctrl_r_break 0xe09d
#define caps_lock_make 0x3a

/* 定义以下变量记录相应键是否按下的状态,
* ext_scancode用于记录makecode是否以0xe0开头 */
static bool ctrl_status, shift_status, alt_status, caps_lock_status, ext_scancode;

/* 以通码make_code为索引的二维数组 */
static char keymap[][2] = {
/* 扫描码 未与shift组合 与shift组合*/
/* ---------------------------------- */
/* 0x00 */ {0, 0},
/* 0x01 */ {esc, esc},
/* 0x02 */ {'1', '!'},
/* 0x03 */ {'2', '@'},
/* 0x04 */ {'3', '#'},
/* 0x05 */ {'4', '$'},
/* 0x06 */ {'5', '%'},
/* 0x07 */ {'6', '^'},
/* 0x08 */ {'7', '&'},
/* 0x09 */ {'8', '*'},
/* 0x0A */ {'9', '('},
/* 0x0B */ {'0', ')'},
/* 0x0C */ {'-', '_'},
/* 0x0D */ {'=', '+'},
/* 0x0E */ {backspace, backspace},
/* 0x0F */ {tab, tab},
/* 0x10 */ {'q', 'Q'},
/* 0x11 */ {'w', 'W'},
/* 0x12 */ {'e', 'E'},
/* 0x13 */ {'r', 'R'},
/* 0x14 */ {'t', 'T'},
/* 0x15 */ {'y', 'Y'},
/* 0x16 */ {'u', 'U'},
/* 0x17 */ {'i', 'I'},
/* 0x18 */ {'o', 'O'},
/* 0x19 */ {'p', 'P'},
/* 0x1A */ {'[', '{'},
/* 0x1B */ {']', '}'},
/* 0x1C */ {enter, enter},
/* 0x1D */ {ctrl_l_char, ctrl_l_char},
/* 0x1E */ {'a', 'A'},
/* 0x1F */ {'s', 'S'},
/* 0x20 */ {'d', 'D'},
/* 0x21 */ {'f', 'F'},
/* 0x22 */ {'g', 'G'},
/* 0x23 */ {'h', 'H'},
/* 0x24 */ {'j', 'J'},
/* 0x25 */ {'k', 'K'},
/* 0x26 */ {'l', 'L'},
/* 0x27 */ {';', ':'},
/* 0x28 */ {'\'', '"'},
/* 0x29 */ {'`', '~'},
/* 0x2A */ {shift_l_char, shift_l_char},
/* 0x2B */ {'\\', '|'},
/* 0x2C */ {'z', 'Z'},
/* 0x2D */ {'x', 'X'},
/* 0x2E */ {'c', 'C'},
/* 0x2F */ {'v', 'V'},
/* 0x30 */ {'b', 'B'},
/* 0x31 */ {'n', 'N'},
/* 0x32 */ {'m', 'M'},
/* 0x33 */ {',', '<'},
/* 0x34 */ {'.', '>'},
/* 0x35 */ {'/', '?'},
/* 0x36 */ {shift_r_char, shift_r_char},
/* 0x37 */ {'*', '*'},
/* 0x38 */ {alt_l_char, alt_l_char},
/* 0x39 */ {' ', ' '},
/* 0x3A */ {caps_lock_char, caps_lock_char}
/*其它按键暂不处理*/
};

/* 键盘中断处理程序 */
static void intr_keyboard_handler(void) {

/* 这次中断发生前的上一次中断,以下任意三个键是否有按下 */
bool ctrl_down_last = ctrl_status;
bool shift_down_last = shift_status;
bool caps_lock_last = caps_lock_status;

bool break_code;
uint16_t scancode = inb(KBD_BUF_PORT);

/* 若扫描码是e0开头的,表示此键的按下将产生多个扫描码,
* 所以马上结束此次中断处理函数,等待下一个扫描码进来*/
if (scancode == 0xe0) {
ext_scancode = true; // 打开e0标记
return;
}

/* 如果上次是以0xe0开头,将扫描码合并 */
if (ext_scancode) {
scancode = ((0xe000) | scancode);
ext_scancode = false; // 关闭e0标记
}

break_code = ((scancode & 0x0080) != 0); // 获取break_code

if (break_code) { // 若是断码break_code(按键弹起时产生的扫描码)

/* 由于ctrl_r 和alt_r的make_code和break_code都是两字节,
所以可用下面的方法取make_code,多字节的扫描码暂不处理 */
uint16_t make_code = (scancode &= 0xff7f); // 得到其make_code(按键按下时产生的扫描码)

/* 若是任意以下三个键弹起了,将状态置为false */
if (make_code == ctrl_l_make || make_code == ctrl_r_make) {
ctrl_status = false;
} else if (make_code == shift_l_make || make_code == shift_r_make) {
shift_status = false;
} else if (make_code == alt_l_make || make_code == alt_r_make) {
alt_status = false;
} /* 由于caps_lock不是弹起后关闭,所以需要单独处理 */

return; // 直接返回结束此次中断处理程序

}
/* 若为通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_code */
else if ((scancode > 0x00 && scancode < 0x3b) || \
(scancode == alt_r_make) || \
(scancode == ctrl_r_make)) {
bool shift = false; // 判断是否与shift组合,用来在一维数组中索引对应的字符
if ((scancode < 0x0e) || (scancode == 0x29) || \
(scancode == 0x1a) || (scancode == 0x1b) || \
(scancode == 0x2b) || (scancode == 0x27) || \
(scancode == 0x28) || (scancode == 0x33) || \
(scancode == 0x34) || (scancode == 0x35)) {
/****** 代表两个字母的键 ********
0x0e 数字'0'~'9',字符'-',字符'='
0x29 字符'`'
0x1a 字符'['
0x1b 字符']'
0x2b 字符'\\'
0x27 字符';'
0x28 字符'\''
0x33 字符','
0x34 字符'.'
0x35 字符'/'
*******************************/
if (shift_down_last) { // 如果同时按下了shift键
shift = true;
}
} else { // 默认为字母键
if (shift_down_last && caps_lock_last) { // 如果shift和capslock同时按下
shift = false;
} else if (shift_down_last || caps_lock_last) { // 如果shift和capslock任意被按下
shift = true;
} else {
shift = false;
}
}

uint8_t index = (scancode &= 0x00ff); // 将扫描码的高字节置0,主要是针对高字节是e0的扫描码.
char cur_char = keymap[index][shift]; // 在数组中找到对应的字符

/* 只处理ascii码不为0的键 */
if (cur_char) {
put_char(cur_char);
return;
}

/* 记录本次是否按下了下面几类控制键之一,供下次键入时判断组合键 */
if (scancode == ctrl_l_make || scancode == ctrl_r_make) {
ctrl_status = true;
} else if (scancode == shift_l_make || scancode == shift_r_make) {
shift_status = true;
} else if (scancode == alt_l_make || scancode == alt_r_make) {
alt_status = true;
} else if (scancode == caps_lock_make) {
/* 不管之前是否有按下caps_lock键,当再次按下时则状态取反,
* 即:已经开启时,再按下同样的键是关闭。关闭时按下表示开启。*/
caps_lock_status = !caps_lock_status;
}
} else {
put_str("unknown key\n");
}
}

/* 键盘初始化 */
void keyboard_init() {
put_str("keyboard init start\n");
register_handler(0x21, intr_keyboard_handler);
put_str("keyboard init done\n");
}

头文件没有更改,运行后可以实现基本的输入


H.4 环形输入缓冲区

但是真正的中断还要实现输入缓冲区,因为我们通常在输入一串指令,然后以回车来结束,在结束之前我们就需要找一个缓冲区来存储这些

缓冲区是多个线程共同使用的共享内存,线程在并行访问它时难免会乱套,我们不能指望线程们会老老实实排好队,以串行的方式逐个使用缓冲区。缓冲区大小无关紧要,问题的关键在于缓冲区操作上,因此最好是在缓冲区的操作方法上下功夫,保证对缓冲区是互斥访问,并且不会对其过度使用,从而确保不会使缓冲区遭到破坏。也就是说,只要我们能够设计出合理的缓冲区操作方式,就能够解决生产者与消费者问题

对于缓冲区的访问,我们提供两个指针,一个是头指针,用于往缓冲区中写数据,另一个是尾指针,用于从缓冲区中读数据。每次通过头指针往缓冲区中写入一个数据后,使头指针加 1 指向缓冲区中下一个可写入数据的地址,每次通过尾指针从缓冲区中读取一个数据后,使尾指针加 1 指向缓冲区中下一个可读入数据的地址,也就是说,缓冲区相当于一个队列,数据在队列头被写入,在队尾处被读出

下面来具体实现一下,创建ioqueue.c/h,也放在device下:

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
// /home/mouse/OS_mouse/tool/bochs/mouse/device/ioqueue.c

#include "ioqueue.h"
#include "stdint.h"
#include "debug.h"
#include "interrupt.h"
#include "global.h"


//初始化io队列ioq
void ioqueue_init(struct ioqueue* ioq)
{
lock_init(&ioq->lock); //初始化io队列的锁
ioq->producer = ioq->consumer = NULL; //生产者和消费者都置空
ioq->head = ioq->tail= 0; //队列的首位指针指向缓冲区的数组的第0个位置
}

//返回pos在缓冲区的下一个位置值
static int32_t next_pos(int32_t pos)
{
return (pos+1) % bufsize;
}

//判断队列是否已经满
bool ioq_full(struct ioqueue* ioq)
{
ASSERT(intr_get_status() == INTR_OFF);
return (next_pos(ioq->head) == ioq->tail);
}

//判断队列是否为空
bool ioq_empty(struct ioqueue* ioq)
{
ASSERT(intr_get_status() == INTR_OFF);
return (ioq->head == ioq->tail);
}

//是当前消费者或者生产者在此缓冲区上等待
static void ioq_wait(struct task_struct** waiter)
{
//*waiter 相当于 ioq->producer / ioq->consumer
ASSERT(*waiter == NULL && waiter !== NULL);
*waiter == running_thread();
thread_block(TASK_BLOCKED);
}

//唤醒waiter
static void wakeup(struct task_struct** waiter)
{
ASSERT(*waiter == NULL);
thread_unblock(*waiter);
*waiter = NULL;
}

//消费者从ioq队列获取一个字符
char ioq_getchar(struct ioqueue* ioq)
{
ASSERT(intr_get_status() == INTR_OFF);

//若缓冲区为空,则把ioq->consumer(消费者)记为当前线程自己
//目的是将来生产者往缓冲区里装商品时候,生产者知道该唤醒哪个消费者
//也就是唤醒当前线程的自己
while(ioq_empty(ioq))
{
lock_acquire(&ioq->lock);
ioq_wait(&ioq->consumer);
lock_release(&ioq->lock);
}

char byte = ioq->buf[ioq->tail]; //从缓冲区取出
ioq->tail = next_pos(ioq->tail); //把游标移动到下一个位置

if(ioq->producer != NULL)
{
wakeup(&ioq->producer); //唤醒生产者
}
return byte;
}


//生产者往 ioq 队列中写入一个字符 byte
void ioq_putchar(struct ioqueue* ioq, char byte)
{
ASSERT(intr_get_status() == INTR_OFF);

//若缓冲区(队列)已经满了,把生产者 ioq->producer 记为自己,
//为的是当缓冲区里的东西被消费者取完后让消费者知道唤醒哪个生产者,
//也就是唤醒当前线程自己*/
while (ioq_full(ioq))
{
lock_acquire(&ioq->lock);
ioq_wait(&ioq->producer);
lock_release(&ioq->lock);
}
ioq->buf[ioq->head] = byte; // 把字节放入缓冲区中
ioq->head = next_pos(ioq->head); // 把写游标移到下一位置

if (ioq->consumer != NULL)
{
wakeup(&ioq->consumer); // 唤醒消费者
}
}
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
// /home/mouse/OS_mouse/tool/bochs/mouse/device/ioqueue.h

#ifndef __DEVICE_IPQUEUE_H
#define __DEVICE_IPQUEUE_H
#include "thread.h"
#include "sync.h"
#include "stdint.h"

#define bufsize 64

//环形队列
struct ioqueue{
//生产者消费者问题
struct lock lock;

//生产者,如果缓冲区不满就继续往里面放数据,否则就睡眠
//这里记录哪个生产者在此缓冲区上睡眠
struct task_struct* producer;

//消费者,如果不空就继续从里面拿数据,否则就睡眠
//这里记录哪个消费者在此缓冲区上睡眠
struct task_struct* consumer;

char buf[bufsize]; //缓冲区大小
int32_t head; //队首,数据往队首处写入
int32_t tail; //队尾,数据从队尾读出
};

void ioqueue_init(struct ioqueue* ioq);
//返回pos在缓冲区的下一个位置值
static int32_t next_pos(int32_t pos);
//判断队列是否已经满
bool ioq_full(struct ioqueue* ioq);
//判断队列是否为空
bool ioq_empty(struct ioqueue* ioq);
//消费者从ioq队列获取一个字符
char ioq_getchar(struct ioqueue* ioq);
//生产者往 ioq 队列中写入一个字符 byte
void ioq_putchar(struct ioqueue* ioq, char byte);


#endif

同时,简单修改一下之前写的键盘的代码,给出主要修改部分

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
/*略.......*/

struct ioqueue kbd_buf; //定义环形缓冲区

/*略.......*/

/* 只处理ascii码不为0的键 */
if (cur_char)
{
//如果cur_char 不为0,则添加到ked_buf中(提前判断缓冲区是否满)
if(!ioq_full(&kbd_buf))
{
put_char(cur_char); //临时的
ioq_putchar(&kbd_buf,cur_char);
}
return;
}

/*略.......*/

/* 键盘初始化 */
void keyboard_init() {
put_str("keyboard init start\n");
ioqueue_init(&kbd_buf); //初始化环形缓冲区
register_handler(0x21, intr_keyboard_handler);
put_str("keyboard init done\n");
}

这样重新编译写入运行之后,实现的效果就是如果一直输入,会发现输入63个字符之后,就无法继续输入了(屏幕上的显示就是键盘程序中put_char(cur_char);的体现),符合代码里面的缓冲区写满的现象


H.5 测试生产者和消费实例

这里为了测试,我们使用另外两个任务来作为消费者来读取缓冲区的内容,所以这里我们还要重新打开时钟的中断,同时在keyboard中添加对缓冲区的外部声明,使得外部的函数可以对其进行访问,最后在main函数中添加消费者的线程,记得要删除之前测试添加的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// /home/mouse/OS_mouse/tool/bochs/mouse/kernel/interrupt.c
/* 初始化可编程中断控制器 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


outb (PIC_M_DATA, 0xfc); //打开时钟和键盘中断
outb(PIC_S_DATA, 0xff); //其余全部关闭
put_str(" pic_init done\n");
}
1
2
3
4
5
6
7
8
9
10
// /home/mouse/OS_mouse/tool/bochs/mouse/device/keyboard.h

#ifndef __DEVICE_KEYBOARD_H
#define __DEVICE_KEYBOARD_H

extern struct ioqueue kbd_buf; //声明缓冲区变量,使得外部可以访问
//键盘初始化
void keyboard_init();

#endif

这里要删除这个临时的输出,显示效果会更好

1
2
3
4
5
6
7
8
9
10
if (cur_char) 
{
//如果cur_char 不为0,则添加到ked_buf中(提前判断缓冲区是否满)
if(!ioq_full(&kbd_buf))
{
// put_char(cur_char); //临时的
ioq_putchar(&kbd_buf,cur_char);
}
return;
}
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
/***** /home/mouse/OS_mouse/tool/bochs/mouse/kernel/main.c *****/
#include "print.h"
#include "init.h"
#include "interrupt.h"
#include "debug.h"
#include "string.h"
#include "memory.h"
#include "thread.h"
#include "console.h"

#include "ioqueue.h"
#include "keyboard.h"

void k_thread_one(void* arg);
void k_thread_two(void* arg);

int main()
{
put_str("I am kernel!\n");
init_all(); //初始化所有
//创建一个线程,线程名,优先级,线程函数名,参数
thread_start("k_thread_one",31,k_thread_one,"A__:");
thread_start("k_thread_two",8,k_thread_two,"B__:");

intr_enable(); //打开中断,使时钟中断起作用

while(1)
{
;
}
return 0;
}

void k_thread_one(void* arg)
{
/* 用 void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char * para = arg;
while(1)
{
enum intr_status old_status = intr_disable();

if(!ioq_empty(&kbd_buf))
{
console_put_str(para); //输出
char byte = ioq_getchar(&kbd_buf); //缓冲区获取
console_put_char(byte);
}
intr_set_status(old_status);
}
}

void k_thread_two(void* arg)
{
/* 用 void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char * para = arg;
while(1)
{
enum intr_status old_status = intr_disable();

if(!ioq_empty(&kbd_buf))
{
console_put_str(para); //输出
char byte = ioq_getchar(&kbd_buf); //缓冲区获取
console_put_char(byte);
}
intr_set_status(old_status);
}
}

编译写入运行之后的效果是,如果我长按一个键,那么屏幕上会显示出A__:B__: 和按下的字符交替

这一章就到此结束了

I 用户进程前的小结

本篇主要学了内存管理,内核进程,实现了信号量和锁,同时编写了键盘的输入输出驱动,整体代码量增多了,但是主要是一些数据结构,比如说队列,位图,用来实现唤醒缓冲区,就绪队列等等

下面就是来实现用户进程,实现安全隔离……

参考文献

  1. [操作系统真象还原 (郑纲) (Z-Library)] — 大家可以自己在网上查找相关资源

留言

有问题请指出,你可以选择以下方式:

  1. 在下方评论区留言
  2. 邮箱留言
  • Title: 真象还原 --内存管理/线程/输入输出 study(3)
  • Author: H_Haozi
  • Created at : 2025-10-28 19:02:00
  • Updated at : 2025-11-05 14:51:05
  • Link: https://redefine.ohevan.com/2025/10/28/os_elephant_three/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments