真象还原 --用户进程/完善内核/硬盘驱动 study(4)

真象还原 --用户进程/完善内核/硬盘驱动 study(4)

H_Haozi Lv3

A 前情提要

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

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

第三部分完成了对内存管理线程同步,也实现了基础生产者与消费者*

这一部分主要学习用户进程/完善内核(系统调用,完善内存)/硬盘驱动相关内容

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

B 用户进程

这里概念就不多说了(大家可以自己查询TSS–任务状态段),真象还原是仿照Linux的任务切换方法,其中我们也会使用到TSS,但是只是希望为0特权级的任务提供栈

Linux 为每个 CPU 创建一个 TSS,在各个 CPU 上的所有任务共享同一个 TSS,各 CPU 的 TR 寄存器保存各 CPU 上的 TSS,在用 ltr 指令加载 TSS 后,该 TR 寄存器永远指向同一个 TSS,之后再也不会重新加载 TSS。在进程切换时,只需要把 TSS 中的 SS0 及 esp0 更新为新任务的内核栈的段地址及栈指针

Linux 中任务切换不使用 call 和 jmp 指令,这也避免了任务切换的低效

这里我们直接开始写代码吧


B.1 定义并初始化TSS

首先是在global.h中添加等会需要用到的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
// /home/mouse/OS_mouse/tool/bochs/mouse/kernel/global.h
#ifndef __KERNEL_GLOBAL_H
#define __KERNEL_GLOBAL_H
#include "stdint.h"


// ---------------- GDT描述符属性 ----------------

#define DESC_G_4K 1
#define DESC_D_32 1
#define DESC_L 0 // 64位代码标记,此处标记为0便可。
#define DESC_AVL 0 // cpu不用此位,暂置为0
#define DESC_P 1
#define DESC_DPL_0 0
#define DESC_DPL_1 1
#define DESC_DPL_2 2
#define DESC_DPL_3 3
/*
代码段和数据段属于存储段,tss和各种门描述符属于系统段
s为1时表示存储段,为0时表示系统段.
*/
#define DESC_S_CODE 1
#define DESC_S_DATA DESC_S_CODE
#define DESC_S_SYS 0
#define DESC_TYPE_CODE 8 // x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.
#define DESC_TYPE_DATA 2 // x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.
#define DESC_TYPE_TSS 9 // B位为0,不忙


#define RPL0 0
#define RPL1 1
#define RPL2 2
#define RPL3 3

#define TI_GDT 0
#define TI_LDT 1

#define SELECTOR_K_CODE ((1 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_K_DATA ((2 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_K_STACK SELECTOR_K_DATA
#define SELECTOR_K_GS ((3 << 3) + (TI_GDT << 2) + RPL0)
/* 第3个段描述符是显存,第4个是tss */
#define SELECTOR_U_CODE ((5 << 3) + (TI_GDT << 2) + RPL3)
#define SELECTOR_U_DATA ((6 << 3) + (TI_GDT << 2) + RPL3)
#define SELECTOR_U_STACK SELECTOR_U_DATA

#define GDT_ATTR_HIGH ((DESC_G_4K << 7) + (DESC_D_32 << 6) + (DESC_L << 5) + (DESC_AVL << 4))
#define GDT_CODE_ATTR_LOW_DPL3 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_CODE << 4) + DESC_TYPE_CODE)
#define GDT_DATA_ATTR_LOW_DPL3 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_DATA << 4) + DESC_TYPE_DATA)


//--------------- TSS描述符属性 ------------
#define TSS_DESC_D 0

#define TSS_ATTR_HIGH ((DESC_G_4K << 7) + (TSS_DESC_D << 6) + (DESC_L << 5) + (DESC_AVL << 4) + 0x0)
#define TSS_ATTR_LOW ((DESC_P << 7) + (DESC_DPL_0 << 5) + (DESC_S_SYS << 4) + DESC_TYPE_TSS)
#define SELECTOR_TSS ((4 << 3) + (TI_GDT << 2 ) + RPL0)

struct gdt_desc {
uint16_t limit_low_word;
uint16_t base_low_word;
uint8_t base_mid_byte;
uint8_t attr_low_byte;
uint8_t limit_high_attr_high;
uint8_t base_high_byte;
};


//-------------- IDT描述符属性 ------------
#define IDT_DESC_P 1
#define IDT_DESC_DPL0 0
#define IDT_DESC_DPL3 3
#define IDT_DESC_32_TYPE 0xE // 32位的门
#define IDT_DESC_16_TYPE 0x6 // 16位的门,不用,定义它只为和32位门区分
#define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P << 7) + (IDT_DESC_DPL0 << 5) + IDT_DESC_32_TYPE)
#define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE)

#define PG_SIZE 4096

#endif

然后创建一个目录userprog,以后用来存放有关用户进程的文件,同理,添加一个Makefile,修改后和添加的文件放在下面了

下面主要是对tss的初始化

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

#include "tss.h"
#include "stdint.h"
#include "global.h"
#include "string.h"
#include "print.h"

/* 任务状态段tss结构 */
struct tss {
uint32_t backlink;
uint32_t* esp0;
uint32_t ss0;
uint32_t* esp1;
uint32_t ss1;
uint32_t* esp2;
uint32_t ss2;
uint32_t cr3;
uint32_t (*eip) (void);
uint32_t eflags;
uint32_t eax;
uint32_t ecx;
uint32_t edx;
uint32_t ebx;
uint32_t esp;
uint32_t ebp;
uint32_t esi;
uint32_t edi;
uint32_t es;
uint32_t cs;
uint32_t ss;
uint32_t ds;
uint32_t fs;
uint32_t gs;
uint32_t ldt;
uint32_t trace;
uint32_t io_base;
};
static struct tss tss;

/* 更新tss中esp0字段的值为pthread的0级线 */
void update_tss_esp(struct task_struct* pthread)
{
tss.esp0 = (uint32_t*)((uint32_t)pthread + PG_SIZE);
}

/* 创建gdt描述符 */
static struct gdt_desc make_gdt_desc(uint32_t* desc_addr, uint32_t limit, uint8_t attr_low, uint8_t attr_high)
{
uint32_t desc_base = (uint32_t)desc_addr;
struct gdt_desc desc;
desc.limit_low_word = limit & 0x0000ffff;
desc.base_low_word = desc_base & 0x0000ffff;
desc.base_mid_byte = ((desc_base & 0x00ff0000) >> 16);
desc.attr_low_byte = (uint8_t)(attr_low);
desc.limit_high_attr_high = (((limit & 0x000f0000) >> 16) + (uint8_t)(attr_high));
desc.base_high_byte = desc_base >> 24;
return desc;
}

/* 在gdt中创建tss并重新加载gdt */
void tss_init()
{
put_str("tss_init start\n");
uint32_t tss_size = sizeof(tss);
memset(&tss, 0, tss_size);
tss.ss0 = SELECTOR_K_STACK;
tss.io_base = tss_size;

/* gdt段基址为0x900,把tss放到第4个位置,也就是0x900+0x20的位置 */

/* 在gdt中添加dpl为0的TSS描述符 */
*((struct gdt_desc*)0xc0000920) = make_gdt_desc((uint32_t*)&tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH);

/* 在gdt中添加dpl为3的数据段和代码段描述符 */
*((struct gdt_desc*)0xc0000928) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
*((struct gdt_desc*)0xc0000930) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH);

/* gdt 16位的limit 32位的段基址 */
uint64_t gdt_operand = ((8 * 7 - 1) | ((uint64_t)(uint32_t)0xc0000900 << 16)); // 7个描述符大小
asm volatile ("lgdt %0" : : "m" (gdt_operand));
asm volatile ("ltr %w0" : : "r" (SELECTOR_TSS));
put_str("tss_init and ltr done\n");
}

1
2
3
4
5
6
7
8
// /home/mouse/OS_mouse/tool/bochs/mouse/userprog/tss.h

#ifndef __USERPROG_TSS_H
#define __USERPROG_TSS_H
#include "thread.h"
void update_tss_esp(struct task_struct* pthread);
void tss_init(void);
#endif

下面是对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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# =================================================================================
# 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 -I$(MOUSE_DIR)/userprog -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
include $(MOUSE_DIR)/userprog/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))

USERPROG_SRCS := $(addprefix $(MOUSE_DIR)/userprog/,$(USERPROG_SRCS))
USERPROG_ASMS := $(addprefix $(MOUSE_DIR)/userprog/,$(USERPROG_ASMS))

# ========================== 汇总全部源文件 ================================================
ALL_C_SRCS := $(KERNEL_SRCS) $(LIB_KERNEL_SRCS) $(LIB_USER_SRCS) $(DEVICE_SRCS) $(LIB_SRCS) $(THREAD_SRCS) $(USERPROG_SRCS)
ALL_ASMS := $(KERNEL_ASMS) $(LIB_KERNEL_ASMS) $(LIB_USER_ASMS) $(DEVICE_ASMS) $(LIB_ASMS) $(THREAD_ASMS) $(USERPROG_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,$(filter-out $(MOUSE_DIR)/lib/user/%,$(ALL_C_SRCS)))) \
$(patsubst $(MOUSE_DIR)/%.S,$(BUILD_KERNEL_DIR)/%.o,$(filter $(MOUSE_DIR)/%.S,$(filter-out $(MOUSE_DIR)/lib/user/%,$(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 $@ $<

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

$(BUILD_USER_DIR)/%.o: $(MOUSE_DIR)/userprog/%.S | dirs
@echo "Assembling userprog 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!"

mouse: clean all img
@echo "Mouse build process (clean -> all -> img) completed!"

1
2
3
4
5
#userprog/Makefile


USERPROG_SRCS := tss.c process.c # (列出该目录下所有.c文件)
USERPROG_ASMS := # 例如: start.S (列出该目录下所有.S文件,如果没有则留空)

注意要在init.c文件中添加函数tss_init()

这些准备工作更加偏向概念,这里就不多说了,大家可以仔细阅读一下原书内容,下面才正式开始实现用户进程


B.2 实现用户进程(1)

首先在要在进程的基础上来实现进程,我们先来大致了解一下原理,做点准备:

进程与内核线程最大的区别是进程有单独的 4GB 空间,这里指的是虚拟地址,每个进程都拥有 4GB 的虚拟地址空间,虚拟地址连续而物理地址可以不连续,这就是保护模式下分页机制的优势

为演示此特性,我们需要单独为每个进程维护一个虚拟地址池,用此地址池来记录该进程的虚拟中,哪些已被分配,哪些可以分配。与各个进程相关的数据,如果数据量不大的话,最好是存储在该进程的 PCB 中,这样便于管理。在上一节中已经知道,进程是基于线程实现的,因此它和线程一样使用相同的 pcb 结构,即 struct task_struct,我们要做的就是在此结构中增加一个成员,用它来跟踪用户空间虚拟地址的分配情况

下面就来修改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
//略.....

#include "memory.h"
#include "bitmap.h"

//略.....

/* 进程或线程的 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; //进程自己的虚拟地址

struct virtual_addr userprog_vaddr; //用户进程的虚拟地址

uint32_t stack_magic; // 栈的边界标记,用于检测栈的溢出,注意为尾部
};

//略.....

然后再memory.c中添加相关的功能,在内存池struct pool添加一个锁用来在申请内存的时候作为互斥使用,后面添加了在用户内存池分配内存的函数等,以及让物理地址和虚拟地址建立映射,最后再添加一个通过虚拟地址返回物理地址的函数,记得还要在初始化函数中添加锁的初始化,这里还是给出片段,总体的代码我应该会添加到本节的最后:

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

//略.......

#include "thread.h"
#include "sync.h"


//略.......

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

//略.......

/* 在 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 //用户内存池
{
struct task_struct* cur = running_thread(); //获取当前PCB
bit_idx_start = bitmap_scan(&cur->userprog_vaddr.vaddr_bitmap,pg_cnt); //申请pg_cnt个位置
if(bit_idx_start == -1)
{
return NULL;
}
while(cnt<pg_cnt)
{
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap,bit_idx_start+cnt++,1); //设置位图
}
vaddr_start = cur->userprog_vaddr.vaddr_start + bit_idx_start*PG_SIZE;
ASSERT((uint32_t)vaddr_start < (0xc0000000 - PG_SIZE)); //(0xc0000000 - PG_SIZE)作为用户 3 级栈已经在 start_process 被分配
}
return (void*)vaddr_start;
}

//略.......

//在用户空间申请4K内存,并返回虚拟地址
void* get_user_pages(uint32_t pg_cnt)
{
lock_acquire(&user_pool.lock); //锁
void* vadder = malloc_page(PF_USER,pg_cnt);
memset(vadder,0,pg_cnt*PG_SIZE);
lock_release(&user_pool.lock);
return vadder;
}

//将地址vaddr与pf的物理地址关联,仅支持一页空间分配
void* get_a_page(enum pool_flags pf,uint32_t vaddr)
{
struct pool* mem_pool = pf & PF_KERNEL? &kernel_pool : &user_pool;
lock_acquire(&mem_pool->lock);

struct task_struct* cur = running_thread(); //获取当前pcb
int32_t bit_idx = -1;

//若当前的用户进程申请用户内存,就修改用户进程自己的虚拟地址位图
if(cur->pgdir != NULL && pf == PF_USER)
{
bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE;
ASSERT(bit_idx > 0);
bitmap_set(&cur->userprog_vaddr.vaddr_bitmap,bit_idx,1);
}
//如果是内核线程申请内核内存,就修改kernel_vaddr
else if(cur->pgdir == NULL & pf == PF_KERNEL)
{
bit_idx = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
ASSERT(bit_idx > 0);
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx, 1);
}
else
{
PANIC("get_a_page:not allow kernel alloc userspace or user alloc kernelspace by get_a_page");
}

void* page_phyaddr = palloc(mem_pool); //分配一个物理页,返回页框地址
if(page_phyaddr == NULL)
{
return NULL;
}
page_table_add((void*)vaddr, page_phyaddr); //关联物理地址和虚拟地址
lock_release(&mem_pool->lock);
return (void*)vaddr;
}

//得到虚拟地址映射到的物理地址
uint32_t addr_v2p(uint32_t vaddr)
{
uint32_t* pte = pte_ptr(vaddr); //得到虚拟地址 vaddr 对应的 pte 指针
//(*pte)是页表所在的物理页框地址,去掉低12位的页表项属性+虚拟地址vadder的低12位
return ((*pte & 0xfffff000) + (vaddr & 0x00000fff));
}

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

咳咳,后面还要继续修改memory.c函数…….


B.3 实现用户进程(2)

因为是用户进程,所以我肯定要进入特权级3,现在又该如何从搞特权级转入低优先级嘞

,一般情况下,CPU 不允许从高特权级转向低特权级,除非是从中断和调用门返回的情况下。咱们系统中不打算使用调用门,因此,咱们进入特权级 3 只能借助从中断返回的方式,但用户进程还没有运行,何谈被中断?更谈不上从中断返回了……但是 CPU 比较呆头呆脑,我们可以骗过 CPU,在用户进程运行之前,使其以为我们在中断处理环境中,这样便“假装”从中断返回

如何假装:首先得在特权级 0 的环境中,其次是执行 iretd 指令

iretd 指令会用到栈中的数据作为返回地址,还会加载栈中 eflags的值到 eflags 寄存器,如果栈中 cs.rpl 若为更低的特权级,处理器的特权级检查通过后,会将栈中 cs 载入到 CS 寄存器,栈中 ss 载入 SS 寄存器,随后处理器进入低特权级,所以在栈中提前准备好数据即可

但是既然涉及到栈的操作了,那不如将进程的上下文也存到栈中,然后通过一系列的pop操作将用户进程的数据装载到寄存器,然后通过iretd指令退出中断,我们在退出中断的函数intr_exit中修改(kernel.S)

  • 即便是假装,退出中断也要经过intr_exit
  • 需要提前准备好栈,在里面装填用户进程的上下文,借用pop出栈的机会将上下文载入CPU寄存器
  • 要在栈中存储CS选择子,将RPL修改为3(用户特权级)
  • 栈中段寄存器的选择子必须指向 DPL 为 3 的内存段 用户进程只能访问DPL为3的内存段
  • 必须使栈中 eflags 的 IF 位为 1 响应新的中断
  • 必须使栈中 eflags 的 IOPL 位为 0 IO操作

下面来开始继续写代码,首先是global添加的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//--------------   IDT描述符属性  ------------
#define IDT_DESC_P 1
#define IDT_DESC_DPL0 0
#define IDT_DESC_DPL3 3
#define IDT_DESC_32_TYPE 0xE // 32位的门
#define IDT_DESC_16_TYPE 0x6 // 16位的门,不用,定义它只为和32位门区分
#define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P << 7) + (IDT_DESC_DPL0 << 5) + IDT_DESC_32_TYPE)
#define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE)

#define EFLAGS_MBS (1 << 1) // 此项必须要设置
#define EFLAGS_IF_1 (1 << 9) // if 为 1,开中断
#define EFLAGS_IF_0 0 // if 为 0,关中断
#define EFLAGS_IOPL_3 (3 << 12) //IOPL3,用于测试用户程序在非系统调用下进行 IO
#define EFLAGS_IOPL_0 (0 << 12) // IOPL0

#define NULL ((void*)0)
#define DIV_ROUND_UP(X, STEP) ((X + STEP - 1) / (STEP))
#define bool int
#define true 1
#define false 0

#define PG_SIZE 4096

现在还是在userprog目录下创建process.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
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
// /home/mouse/OS_mouse/tool/bochs/mouse/userprog/process.c

#include "process.h"
#include "thread.h"
#include "interrupt.h"
#include "stdint.h"
#include "memory.h"
#include "console.h"
#include "string.h"
#include "bitmap.h"
#include "debug.h"
#include "tss.h"

extern void intr_exit(void); //退出中断

extern struct list thread_ready_list; //就绪队列
extern struct list thread_all_list; //所有任务队列

//构建用户进程初始化上下文信息
void start_process(void* filename_ )
{
void* function = filename_;
struct task_struct* cur = running_thread(); //当前任务PCB
cur->self_kstack += sizeof(struct thread_stack);
struct intr_stack* proc_stack = (struct intr_stack*)cur->self_kstack;
proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy= 0;
proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;

proc_stack->gs = 0; //用户态用不上,初始化为0
proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;
proc_stack->eip = function; //待执行的用户程序地址
proc_stack->cs = SELECTOR_U_CODE;
proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);
proc_stack->esp = (void*)((uint32_t)get_a_page(PF_USER,USER_STACK3_VADDR) + PG_SIZE );
proc_stack->ss = SELECTOR_U_DATA;
asm volatile("movl %0,%%esp; jmp intr_exit": : "g" (proc_stack) : "memory");
}

//激活页表
/********************************************************
* 执行此函数时,当前任务可能是线程。
* 之所以对线程也要重新安装页表, 原因是上一次被调度的可能是进程,
* 否则不恢复页表的话,线程就会使用进程的页表了。
********************************************************/
void page_dir_activate(struct task_struct* p_thread)
{
//若为内核线程,则需要重新填充页表为0x100000
uint32_t pagedir_phy_addr = 0x100000; // 默认为内核的页目录物理地址,也就是内核线程所用的页目录表
if(p_thread->pgdir != NULL) //用户态进行有自己的页目录表
{
pagedir_phy_addr = addr_v2p((uint32_t)p_thread->pgdir); //获取物理地址
}
//更新页目录寄存器cr3,使新页表生效
asm volatile ("movl %0, %%cr3" : : "r" (pagedir_phy_addr) : "memory");
}

// 激活线程或进程的页表,更新tss中的esp0为进程的特权级0的栈
void process_activate(struct task_struct* p_thread)
{
ASSERT(p_thread != NULL);
page_dir_activate(p_thread); //激活页表
//内核线程特权级本身就是0,处理器进入中断时并不会从tss中获取0特权级栈地址,故不需要更新esp0
if(p_thread->pgdir)
{
//更新该进程的esp0,用于进程被中断时候保留上下文
update_tss_esp(p_thread);
}
}

//创建页目录表,将当前页表的表示内核空间的pde复制,
//成功则返回页目录的虚拟地址,否则返回-1 */
uint32_t* create_page_dir(void)
{
//用户进程的页表不能让用户直接访问到,所以在内核空间来申请
uint32_t* page_dir_vaddr = get_kernel_pages(1);
if(page_dir_activate == NULL)
{
console_put_str("create_page_dir: get_kernel_page failed!");
return NULL;
}

//1. 先复制页表 page_dir_vaddr + 0x300*4 是内核页目录的第768项
memcpy((uint32_t*)((uint32_t)page_dir_vaddr + 0x300*4), (uint32_t*)(0xfffff000+0x300*4), 1024);

//2. 更新页目录地址
uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t)page_dir_vaddr);
page_dir_vaddr[1023] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1;

return page_dir_vaddr;
}

//创建用户进程虚拟地址位图
void create_user_vaddr_bitmap(struct task_struct* user_prog)
{
user_prog->userprog_vaddr.vaddr_start = USER_VADDR_START;
uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8 , PG_SIZE);
user_prog->userprog_vaddr.vaddr_bitmap.bits = get_kernel_pages(bitmap_pg_cnt);
user_prog->userprog_vaddr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8;
bitmap_init(&user_prog->userprog_vaddr.vaddr_bitmap);
}

//创建用户进程
void process_execute(void* filename,char*name)
{
//pcb内核的数据结构,由内核来维护进程信息,因此要在内核内存池中申请
struct task_struct* thread = get_kernel_pages(1);
init_thread(thread,name,default_prio); //默认优先级31
create_user_vaddr_bitmap(thread); //创建位图
thread_create(thread,start_process,filename); //通过start_process函数退出中断进入用户进程
thread->pgdir = create_page_dir(); //创建页目录表

enum intr_status old_status = intr_disable();

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

intr_set_status(old_status);
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// /home/mouse/OS_mouse/tool/bochs/mouse/userprog/process.h

#ifndef __USERPROG_PROCESS_H
#define __USERPROG_PROCESS_H
#include "thread.h"
#include "stdint.h"
#define default_prio 31
#define USER_STACK3_VADDR (0xc0000000 - 0x1000)
#define USER_VADDR_START 0x8048000
void process_execute(void* filename, char* name);
void start_process(void* filename_);
void process_activate(struct task_struct* p_thread);
void page_dir_activate(struct task_struct* p_thread);
uint32_t* create_page_dir(void);
void create_user_vaddr_bitmap(struct task_struct* user_prog);
#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
// /home/mouse/OS_mouse/tool/bochs/mouse/thread/thread.c

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

#include "process.h"

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

//任务调度器
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;

process_activate(next); //激活页表等

switch_to(cur,next); //切换进程
}

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

B.4 测试用户进程

还是直接在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
/***** /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 "process.h"

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

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

int test_var_a = 0, test_var_b = 0; //全局变量

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

//创建一个用户进程,进程函数,进程名
process_execute(u_prog_a,"user_prog_a");
process_execute(u_prog_b,"user_prog_b");

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

while(1)
{
;
}
return 0;
}

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

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

void u_prog_a(void)
{
while(1)
{
test_var_a++;
}
}

void u_prog_b(void)
{
while(1)
{
test_var_b++;
}
}

在调试之前,补充一个问题,就是之前在loader.S文件,当我们进入保护模式创建页表之后,我通过info gdt会发现基地址并不是预期的0x900,而是0x903,原因已经补充到了当时出现问题的地方

如果loader.S开头运用了jmp loader_start这一命令,gdt表的基地址就会发生偏移,因为跳转指令本身可能占有3字节,可能会往后偏移,导致后面的tss添加的时候不能以0x900作为gdt基地址

所以之前在初始化tss的时候,地址发生了错误,这样编译运行便会发生报错

这里有两个修改方案,第一个就是不使用jmp loader_start指令,让基地址为正常值,同时修改mbr文件中跳转的命令,让其可以正常跳转到loader_start的位置,第二个就是修改初始化tss的时候使用的基地址(先通过info gdt得到真正的基地址,但是这样肯定不推荐的)

其中跳转+0x300,是通过计算得出的,最开始是填充偏移到0x200,然后又添加了256字节的数据区,累计起来就0x300

下面给出第一种方案的修改部分,注意修改之后还要重新编译写入磁盘

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

%include "boot.inc"

section loader vstart=LOADER_BASE_ADDR
;LOADER_STACK_TOP equ LOADER_BASE_ADDR
;jmp loader_start

; 构建gdt及其内部的描述符,拆分为上4字节,下4字节

GDT_BASE: dd 0x00000000 ;空描述段
dd 0x00000000
CODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4
DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4
VIDEO_DESC: dd 0x80000007 ;limit=(0xbffff-0xb8000)/4k=0x7
dd DESC_VIDEO_HIGH4 ;此时 dpl 为 0
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1

times 60 dq 0 ; 此处预留 50 个描述符的空位(为什么不是60?因为我开头调用jmp,使得total_mem_bytes无法刚好是0xb00) times 是 nasm 提供的伪指令,用来重复执行 times 后面表达式(编译器执行)

SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0
; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上

;times 0x200 - ($ -$$) db 0 ;填充0,使得total_mem_bytes 的节内偏移一定为0x200,地址一定为0xb00

total_mem_bytes dd 0
; total_mem_bytes 用于保存内存容量,以字节为单位,这个位置比价好记
; 当前偏移loader.bin文件头0x200 字节
; loader.bin加载地址为 0x900
; 所以 total_mem_bytes 内存地址为 0xb00
; 将来在内核中我们会引用这个地址,同时等会验证内存容量的时候,GDB调试也可以通过读取这个地址的内容来查看大小

;以下是 gdt 的指针,前 2 字节是 gdt 界限,后 4 字节是 gdt 起始地址
align 4 ;强制对齐
gdt_ptr:
dw GDT_LIMIT
dd GDT_BASE
loadermsg db 'Mosue'

;人工对齐计算大小:total_mem_bytes(4) + gdt_ptr(6) + ards_buf(239) + ards_nr(2) + loadermsg(5) = 256字节
ards_buf times 239 db 0 ;填充对齐使用
ards_nr dw 0 ;用来记录ARDS结构体数量


loader_start:

;略.............................
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
; /home/mouse/OS_mouse/tool/bochs/mouse/mbr.S
;主引导程序
;
;LOADER_BASE_ADDR equ 0xA000
;LOADER_START_SECTOR equ 0x2 这里的内容添加在了boot.inc中,下面的%include引用
;------------------------------------------------------------

%include "boot.inc"

SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax

;清屏
;利用 0x06 号功能,上卷全部行,则可清屏
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为 0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0600h
mov bx, 0700h
mov cx, 0 ; 左上角: (0, 0)
mov dx, 184fh ; 右下角: (80,25),
; VGA 文本模式中,一行只能容纳 80 个字符,共 25 行
; 下标从 0 开始,所以 0x18=24,0x4f=79
int 10h ; int 10h

; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"

mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4 ; A 表示绿色背景闪烁,4 表示前景色为红色

mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4

mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4

mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4

mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4

; 这里是主要添加的内容
mov eax,LOADER_START_SECTOR ; 起始扇区 lba 地址
mov bx,LOADER_BASE_ADDR ; 写入的地址
mov cx,8 ; 待读入的扇区数,这里1即可
call rd_disk_m_16 ; 以下读取程序的起始部分(一个扇区)

jmp LOADER_BASE_ADDR + 0x300

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

然后编译写入运行,就可以得到书中的效果,即数字一直增大,下面我们来验证是否真的是用户特权级3

首先通过指令获取地址

1
nm build/kernel/kernel.bin |grep -P 'u_prog|test_var'

mouse@ubuntu:~/OS_mouse/tool/bochs/mouse$ nm build/kernel/kernel.bin |grep -P 'u_prog|test_var'
c0007300 B test_var_a
c0007304 B test_var_b
c00015e7 T u_prog_a
c00015f9 T u_prog_b

得到变量的地址后,我们设置变量u_prog_a的断点,然后查看cs寄存器的值

1
2
3
4
5
lb 0xc00015e7     #设置断点

c #继续运行

sreg #查看寄存器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<bochs:1> lb 0xc00015e7
<bochs:2> c
(0) Breakpoint 1, 0xc00015e7 in ?? ()
Next at t=22335944
(0) [0x0000000015e7] 002b:c00015e7 (unk. ctxt): push ebp ; 55
<bochs:3> sreg
es:0x0033, dh=0x00cff300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
cs:0x002b, dh=0x00cff900, dl=0x0000ffff, valid=1
Code segment, base=0x00000000, limit=0xffffffff, Execute-Only, Non-Conforming, Accessed, 32-bit
ss:0x0033, dh=0x00cff300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
ds:0x0033, dh=0x00cff300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
fs:0x0033, dh=0x00cff300, dl=0x0000ffff, valid=1
Data segment, base=0x00000000, limit=0xffffffff, Read/Write, Accessed
gs:0x0000, dh=0x00001000, dl=0x00000000, valid=0
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0020, dh=0xc0808b00, dl=0x7500006b, valid=1
gdtr:base=0xc0000900, limit=0x37
idtr:base=0xc0007320, limit=0x197
<bochs:4>

我们通过调试来设置断点,查看cs寄存器的值,此时 cs 的值为 0x002b,我们关注最低 4 位,其值为 b,换为二进制是 1011,最低 2 位为 rpl,也就是 3,所以可以判断此时用户进程确实是在 3 特权级下,与预期符合

ok呀,终于结束了,代码一多好难找调试找错误啊…….


C 完善内核(系统调用,完善内存)

之前我们用过系统调用,现在我们要自己来实现了

C.1 系统调用简介

系统调用就是让用户进程申请操作系统的帮助,让操作系统帮其完成某项工作,也就是相当于用户进程调用了操作系统的功能,我们还是参照 Linux 系统调用的原理,模仿着它咱们实现一份简易的系统调用版本

下面还是引用部分书中的句子

Linux 系统调用是用中断门来实现的,通过软中断指令 int 来主动发起中断信号。由于要支持的系统功能很多,总不能一个系统功能调用就占用一个中断向量,真要是这样的话整个中断描述符表都不够用呢。Linux 只占用一个中断向量号,即 0x80,处理器执行指令 int 0x80 时便触发了系统调用。为了让用户程序可以通过这一个中断门调用多种系统功能,在系统调用前,Linux 在寄存器 eax 中写入子功能号,例如系统调用 open 和 close 都是不同的子功能号,当用户程序通过int 0x80 进行系统调用时,对应的中断处理例程会根据eax 的值来判断用户进程申请哪种系统调用

Linux中使用的是系统调用 syscall,原型是 int syscall(int number, …),其中的number 是 int 型,这是系统调用号,也就是前面所说的子功能号。不同的子功能需要的参数也是不同的,所以number 后面的“…”表示此函数支持变参,函数 syscall支持不同参数个数的系统调用,在新版本 Linux 中,所有的系统调用功能都可通过这一个函数完成。顺便说一句,函数 syscall 并不是由操作系统提供的,它是由 C 运行库 glibc(GNU 发布的 libc 库版本)提供的,因此 syscall实际上是库函数

书中给出了具体的例子,来讲解这个函数,库函数的syscall是简介使用系统调用的方式,那么肯定有直接使用的方式,也就是操作系统提供的_syscall,但是它已经被linux废弃了(注意,只是废弃了_syscall 这
个符号),因为此方式最多支持6个参数,但是它的实现方式和思路非常简单,我们的简易操作系统就是使用的它

下面咱们拿_syscall3举例,当然还有其他的类型(_syscall是系统调用”族,原型是_syscallX(type,name,type1,arg1,type2,arg2,…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define _syscall3(type, name, type1, arg1, type2, arg2, type3, arg3) \
type name(type1 arg1, type2 arg2, type3 arg3) { \
long __res; \
__asm__ volatile ("push %%ebx; movl %2,%%ebx; int $0x80; pop %%ebx" \
: "=a" (__res) \
: "0" (__NR_##name),"ri" ((long)(arg1)),"c" ((long)(arg2)), \
"d" ((long)(arg3)) : "memory"); \
__syscall_return(type,__res); \
}

#define __syscall_return(type, res) \
do { \
if ((unsigned long)(res) >= (unsigned long)(-125)) { \
errno = -(res); \
res = -1; \
} \
return (type) (res); \
} while (0)

这里给大伙说明下,Linux 中的系统调用是用寄存器来传递参数的,这些参数需要按照从左到右的顺序依次存入到不同的通用寄存器(除 esp)中。其中,寄存器 eax 用来保存子功能号,ebx 保存第 1 个参数,ecx 保存第 2 个参数,edx 保存第 3 个参数,esi 保存第 4 个参数,edi 保存第 5 个参数。传递参数还可以用栈(内存);
不知道您想过没有,为什么 Linux 用寄存器来传递参数,而不用栈?用寄存器快?肯定是这样的,没有哪个操作系统愿意更慢。不过这个“快”可不是出于存储介质方面的考虑,而是用寄存器传参的步骤少一些,听我慢慢道来。用户进程执行 int 0x80 时还处于用户态,编译器根据 c 调用约定,系统调用所用的参数会被压到用户栈中,这是 3 特权级栈。当 int 0x80 执行后,任务陷入内核态,此时进入了 0 特权级,因此需要用到 0 特权级栈,但系统调用的参数还在 3 特权级的栈中,为了获取用户栈地址,还得在 0 特权级栈中获取处理器自动压入的用户栈的 SS 和 esp 寄存器的值,然后再次从用户栈中获取参数。您看,光传递参数就涉及到了多次内存访问的情况,内存比寄存器要慢,而且步骤很麻烦

宏_syscall 和库函数 syscall 相比,syscall 实现更灵活,对用户来说任何参数个数的系统调用都统一用一种形式,用户只要记住 syscall 就可以了,而宏_syscall 的实现比较死板,针对每种参数个数的系统调用都要有单独的形式,因此支持的参数数量必然有限,而且用户要记住 7 种形式_syscall[0-6],调用时除了输入实参外,还要输入实参的类型,确实有些麻烦,此外这个宏会引发安全漏洞(有兴趣可自行检索相关资料),故必然会被 syscall 取代。

C.2 系统调用的实现

这里仿照 宏_stscall来实现,下面是大体步骤:

(1)用中断门实现系统调用,效仿 Linux 用 0x80 号中断作为系统调用的入口
(2)在 IDT 中安装 0x80 号中断对应的描述符,在该描述符中注册系统调用对应的中断处理例程
(3)建立系统调用子功能表 syscall_table,利用 eax 寄存器中的子功能号在该表中索引相应的处理函数
(4)用宏实现用户空间系统调用接口_syscall,最大支持 3 个参数的系统调用,故只需要完成_syscall[0-3](其中寄存器传递参数,eax 为子功能号,ebx保存第 1 个参数,ecx 保存第 2 个参数,edx 保存第 3 个参数)

C2.1 增加0x80中断描述符

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.c

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

#define IDT_DESC_CNT 0x81 // 目前总共支持的中断数,这里写到0x81,支持0x00~0x80
extern uint32_t syscall_handler(void); //系统调用的中断函数

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

static void idt_desc_init(void)
{
int i,lastindex = IDT_DESC_CNT-1;
for(i=0;i<IDT_DESC_CNT;i++)
{
make_idt_desc(&idt[i],IDT_DESC_ATTR_DPL0,intr_entry_table[i]);
}

//单独处理系统调用,系统调用对应的中断门 dpl 为 3,否则用户在3级环境执行int指令会产生GP异常
//中断处理程序为单独的 syscall_handler */
make_idt_desc(&idt[lastindex], IDT_DESC_ATTR_DPL3,syscall_handler);

put_str("idt_desc_init done\n");
}

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

C2.2 实现系统调用接口

lib/user下创建文件syscall.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
// /home/mouse/OS_mouse/tool/bochs/mouse/lib/user/syscall.c

#include "syscall.h"

//无参数的系统调用
#define _syscall0(NUMBER)({ \
int retval; \
asm volatile( \
"int $0x80" \
: "=a" (retval) \
: "a" (NUMBER) \
: "memory" \
); \
retval; \
})


//1个参数的系统调用
#define _syscall1(NUMBER, ARG1) ({ \
int retval; \
asm volatile ( \
"int $0x80" \
: "=a" (retval) \
: "a" (NUMBER), "b" (ARG1) \
: "memory" \
); \
retval; \
})

//2个参数的系统调用
#define _syscall2(NUMBER, ARG1, ARG2) ({ \
int retval; \
asm volatile ( \
"int $0x80" \
: "=a" (retval) \
: "a" (NUMBER), "b" (ARG1), "c" (ARG2) \
: "memory" \
); \
retval; \
})

//3个参数的系统调用
#define _syscall3(NUMBER, ARG1, ARG2, ARG3) ({ \
int retval; \
asm volatile ( \
"int $0x80" \
: "=a" (retval) \
: "a" (NUMBER), "b" (ARG1), "c" (ARG2), "d" (ARG3) \
: "memory" \
); \
retval; \
})

C2.3 增加 0x80 号中断处理例程

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

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

;;;;;;;;;;;;;;;;;;;;;; 0x80 号中断 ;;;;;;;;;;;;;;;;;;;;;;
[bits 32]
extern syscall_table ;声明系统调用使用的数组
section .text
global syscall_handler ;声明外部可调用

syscall_handler:
;1.保存上下文环境
push 0 ;压入0,使栈中格式统一

push ds
push es
push fs
push gs
pushad ;压入32位寄存器,入栈顺序为EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

push 0x80 ;这里压入0x80也是为了保持统一的格式

;2. 为系统调用子功能传入参数
push edx ;第3个参数
push ecx ;第2个参数
push ebx ;第1个参数

;3. 调用子功能处理函数
call [syscall_table + eax*4]
add esp,12 ;跨过上面的三个参数

;4. 将call调用后的返回值存入当前内核栈的eax的位置
mov [esp +8*4],eax;
jmp intr_exit ;intr_exit返回,恢复上下文

然后修改thread.c/h,在结构体添加pid变量/锁,并且增加初始化和获取

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

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

//自定义的pid类型
typedef int16_t pid_t;
struct lock pid_lock; // 分配 pid 锁

/* 进程或线程的 pcb,程序控制块 */
struct task_struct {
uint32_t* self_kstack; // 各内核线程都用自己的内核栈,注意为首位
pid_t pid; // 任务的pid
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; //进程自己的虚拟地址

struct virtual_addr userprog_vaddr; //用户进程的虚拟地址

uint32_t stack_magic; // 栈的边界标记,用于检测栈的溢出,注意为尾部
};

//略............................
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/thread/thread.c

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

//分配pid
static pid_t allocate_pid(void)
{
lock_acquire(&pid_lock);
static pid_t next_pid = 0;
next_pid++;
lock_release(&pid_lock);
return next_pid;
}


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

list_init(&thread_ready_list);
list_init(&thread_all_list);
lock_init(&pid_lock);

//将当前的main函数创建为线程
make_main_thread();
put_str("thread_init done");
}

C2.4 初始化系统调用和实现 sys_getpid

下面在/mouse/userprog下添加syscall-init.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
// /home/mouse/OS_mouse/tool/bochs/mouse/userprog/syscall-init.c

#include "syscall-init.h"
#include "stdint.h"
#include "thread.h"
#include "syscall.h"
#include "print.h"

#define syscall_nr 32
typedef void* syscall;
syscall syscall_table[syscall_nr]; //存储系统调用的函数

/* 返回当前任务的 pid */
uint32_t sys_getpid(void)
{
return running_thread()->pid;
}

/* 初始化系统调用 */
void syscall_init(void)
{
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid;
put_str("syscall_init done\n");
}
1
2
3
4
5
6
7
8
9
10
11
// /home/mouse/OS_mouse/tool/bochs/mouse/userprog/syscall-init.h

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

uint32_t sys_getpid(void);
void syscall_init(void);

#endif

C2.5 添加系统调用 getpid

现在来完善前面写的syscall.c,同时添加syscall.h,来实现我们的第一个系统调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// /home/mouse/OS_mouse/tool/bochs/mouse/lib/user/syscall.hs

#ifndef __LIB_USER_SYSCALL_H
#define __LIB_USER_SYSCALL_H

#include "stdint.h"

enum SYSCALL_NR{ //存放系统调用子功能号
SYS_GETPID
};

uint32_t getpid(void);

#endif
1
2
3
4
5
6
7
8
9
10
11
// /home/mouse/OS_mouse/tool/bochs/mouse/lib/user/syscall.c

#include "syscall.h"


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

uint32_t getpid(void)
{
return _syscall0(SYS_GETPID);
}

C2.6 在用户进程中的系统调用

注意将之前的初始化添加到init.c文件中,同时更新相关子Makefile文件,这里选择在主函数中模拟验证系统调用结果,分别用户内核函数中调用获取pid的函数,然后通过内核线程输出:

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
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall.h"
#include "syscall-init.h"

void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int test_var_a = 0, test_var_b = 0;

int main(void) {
put_str("I am kernel\n");
init_all();

process_execute(u_prog_a, "user_prog_a");
process_execute(u_prog_b, "user_prog_b");

thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 31, k_thread_b, "argB ");

intr_enable();
while(1);
return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
char* para = arg;
while(1) {
console_put_str("ua:");
console_put_int(test_var_a);
console_put_str(" ka:");
console_put_int(sys_getpid());
console_put_str(" ");
}
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
char* para = arg;
while(1) {
console_put_str("ub:");
console_put_int(test_var_b);
console_put_str(" kb:");
console_put_int(sys_getpid());
console_put_str(" ");
}
}

/* 测试用户进程 */
void u_prog_a(void) {
while(1) {
test_var_a = getpid();
}
}

/* 测试用户进程 */
void u_prog_b(void) {
while(1) {
test_var_b = getpid();
}
}

到这里,如果屏幕上打印出四个任务(除了主函数)的pid的话,那么就成功啦!

C.3 让用户进程说话

函数占用的也是静态内存,因此也得提前告诉编译器自己占用的内存大小。为了在编译时获取函数调用时所需要的内存空间(这通常是在栈中分配内存单元),编译器要求提供函数声明,声明中描述了函数参数的个数及类型,编译器用它们来计算参数所占据的栈空间。因此编译器不关心函数声明中参数的名称,它只关心参数个数及类型(您懂的,函数声明中的参数可以不包括参数名,但必须包括类型),编译器用这两个信息才能确定为函数在栈中分配的内存大小。重点来了,函数并不是在堆中分配内存,因此它需要提前确定内存空间,这通常取决于参数的个数及类型大小,但编译器却允许函数的参数个数不固定(可变参数),怎么看上去显得那么“动态”?其实可变参数的这种“动态”只是一种幻想,本质上还是静态,这一切得益于编译器采用 C 调用约定来处理函数的传参方式。C调用约定规定:由调用者把参数以从右向左的顺序压入栈中,并且由调用者清理堆栈中的参数。我们拿格式化输出函数 printf(char* format, arg1, arg2,…)举例,其中的参数 format 就是大伙儿再熟悉不过的包含“%类型字符”的字符串

既然参数是由调用者压入的,调用者当然知道栈中压入了几个参数,参数占用了多少空间,因此无论函数的参数个数是否固定,采用 C 调用约定,调用者都能完好地回收栈空间,不必担心栈溢出等问题

如何知道栈中有多少个参数呢,如何找到它们呢?其实答案全在格式化字符串中,此字符串通常称为 format,在格式化字符串中的字符’%’便是在栈中寻找可变参数的依据,紧跟’%’后面的是类型字符,类型字符表示数据类型和进制相关的内容。格式化字符串中有多少’%’,就在栈中找多少次参数,尽管用户(程序员)输入’%’的数量可以和参数个数不一致,但那是用户自己的事,除非用户愿意搬起石头砸自己的脚,编译器也不会检查它们的数量是否匹配,因为参数处理与否是函数自己的行为,由函数体内的代码决定。总之正常情况下,用户传入可变参数的数量应与 format 中字符’%’的数量匹配,以 format 中的’%’作为参数的线索,每找到一个’%’,就到栈中去找一次参数

下面直接通过代码来了解什么是可变参数:

C3.1 实现系统调用write

printf 函数来完成的,它是标准 io 函数,printf 函数是“格式化”“输出”函数,但它只是个外壳,真正起到“格式化”作用的是vsprintf函数,真正起“输出”作用的是write系统调用

下面我们来实现一个简易版的write,因为标准实现还需要fd(文件描述符),这个要等到后面学习文件系统的时候才能实现

首先,现在syscall.h中添加新的子功能号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// /home/mouse/OS_mouse/tool/bochs/mouse/lib/user/syscall.hs

#ifndef __LIB_USER_SYSCALL_H
#define __LIB_USER_SYSCALL_H

#include "stdint.h"

enum SYSCALL_NR{ //存放系统调用子功能号
SYS_GETPID,
SYS_WRITE
};

uint32_t getpid(void);
uint32_t write(char* str);

#endif
1
2
3
4
5
6
7
8
// /home/mouse/OS_mouse/tool/bochs/mouse/lib/user/syscall.c

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

uint32_t write(char* str)
{
return _syscall1(SYS_WRITE,str);
}

同时在syscall_table中注册

1
2
3
4
5
6
7
8
9
10
11
12
// /home/mouse/OS_mouse/tool/bochs/mouse/userprog/syscall-init.c

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

/* 初始化系统调用 */
void syscall_init(void)
{
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid; //获取pid
syscall_table[SYS_WRITE] = sys_write; //write
put_str("syscall_init done\n");
}
1
2
3
4
5
6
7
8
9
10
11
// /home/mouse/OS_mouse/tool/bochs/mouse/userprog/syscall-init.h

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

uint32_t sys_getpid(void);
void syscall_init(void);

#endif

实现方法很简单,就是使用console_put_str输出str,然后通过strlen(str)返回长度

这里就不测试了,下面来实现printf;

C3.2 实现printf

int printf(const char *format, ...) 是我们 C 语言标准输出函数,前面说过,printf 是 vsprintf 和 write 的封装,下面我们还需要实现 vprintf 和对可变参数解析的3个宏以及转换函数itoa,本节的目标是使 printf 支持十六进制输出,即完成“%x”的功能

这里简单说一下我的理解:

printf或者可变字符的原理就是,编译器会通过将参数从右向左压入栈,然后我读取第一个参数format,然后取地址再强转成char*,就得到了栈的地址,然后偏移4个单位(32位的char*是这样的,如果想要更严谨,可以选择使用一个内存对齐的宏),得到第一个参数的位置,然后通过读取字符串中的%字符,判断%后面的内容(应该读取什么类型),来按顺序/变量大小读取刚刚获得的栈,然后再拼接到一个字符串中,就实现了”可变参数”,而参数的具体还得取决栈/缓冲区的大小,并不是真的无限可变

下面创建文件/lib/stdio.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
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
// /home/mouse/OS_mouse/tool/bochs/mouse/lib/stdio.c

#include "stdio.h"
#include "stdint.h"
#include "string.h"
#include "syscall.h"

//定义用于内存对齐的宏(char short int 对齐到 4,double 对齐到8)
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))

#define va_start(ap, v) (ap = (va_list)&v + _INTSIZEOF(v)) //将ap指向固定参数v
#define va_arg(ap, t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t))) //将ap返回值,然后指向下一个参数
#define va_end(ap) (ap = (va_list)0) //清除ap

//将整形转换为字符
//待转换的数字,转换后存储的缓冲区 转换的进制数
static void itoa(uint32_t value,char** buf_ptr_addr,uint8_t base)
{
uint32_t m = value % base; //取模,最先掉下来的是最低位
uint32_t i = value / base; //取整
if(i)
{
itoa(i,buf_ptr_addr,base);
}
if(m < 10) //如果余数是0~9
{
*((*buf_ptr_addr)++) = m + '0'; //数字0~9转换为字符'0'~'9'
}
else //否则余数是A~F
{
*((*buf_ptr_addr)++) = m - 10 + 'A'; //将数字A~F转换为字符A~F
}
}

// 将参数ap按照格式format输出到字符串str,并返回str长度
uint32_t vsprintf(char* str,const char* format,va_list ap)
{
char* buf_ptr = str;
const char* index_ptr = format ;
char index_char = *index_ptr;
int32_t arg_int;
char* arg_str;
while(index_char)
{
if(index_char != '%')
{
*(buf_ptr++) = index_char; //保存当前字符
index_char = *(++index_ptr); //获取下一个字符
continue;
}
index_char = *(++index_ptr); //获取%后面的字符
switch (index_char)
{
case 'x':
{
arg_int = va_arg(ap,int);
itoa(arg_int,&buf_ptr,16); //将数字转换为16进制并存储到buf_ptr
index_char = *(++index_ptr); //跳过格式字符并更新index_char
break;
}
case 'c':
{
*(buf_ptr++) = va_arg(ap,char);
index_char = *(++index_ptr);
break;
}
case 's':
{
arg_str = va_arg(ap,char*);
strcpy(buf_ptr,arg_str); //将字符串拷贝到buf中,拷贝并不会修改指针位置
buf_ptr += strlen(arg_str); //buf移动到下个需要写入的地址
index_char = *(++index_ptr); //跳过格式字符并更新index_char
break;
}
case 'd':
{
arg_int = va_arg(ap,int);
//若为负数,转为正数之后,在正数之前添加一个符号'-'
if(arg_int < 0)
{
arg_int = 0 - arg_int;
*buf_ptr++ = '-';
}
itoa(arg_int,&buf_ptr,10);
index_char = *(++index_ptr); //跳过格式字符并更新index_char
break;
}
}
}
return strlen(buf_ptr);
}

//格式化输出字符format
uint32_t printf(const char* format,...)
{
va_list args;
va_start(args,format); //使args指向format的栈地址
char buf[1024] = {0}; //用于存储拼接后的字符串
vsprintf(buf,format,args); //转换可变参
va_end(args); //清理原指针
return write(buf); //通过write输出到终端
}

uint32_t sprintf(char* buf, const char* format, ...)
{
va_list args;
uint32_t retval; //保存返回值
va_start(args,format); //使args指向format的栈地址
retval = vsprintf(buf,format,args); //转换可变参到buf中
va_end(args); //清理原指针
return retval; //返回字符串长度
}
1
2
3
4
5
6
7
8
9
10
11
12
// /home/mouse/OS_mouse/tool/bochs/mouse/lib/stdio.h

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

typedef char* va_list;
uint32_t printf(const char* str,...);
uint32_t vsprintf(char* str,const char* format,va_list ap);
uint32_t sprintf(char* buf, const char* format, ...);

#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
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall.h"
#include "syscall-init.h"
#include "stdio.h"

void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int test_var_a = 0, test_var_b = 0;

int main(void) {
put_str("I am kernel\n");
init_all();

// process_execute(u_prog_a, "user_prog_a");
// process_execute(u_prog_b, "user_prog_b");

// thread_start("k_thread_a", 31, k_thread_a, "argA ");
// thread_start("k_thread_b", 31, k_thread_b, "argB ");
int a = -2;
char b = 'c';
char* eee = "mouse";
printf("Hello,Word %x %d %c %s\n",getpid(),a,b,eee);
intr_enable();
while(1);
return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
char* para = arg;
while(1) {
console_put_str("ua:");
console_put_int(test_var_a);
console_put_str(" ka:");
console_put_int(sys_getpid());
console_put_str(" ");
}
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
char* para = arg;
while(1) {
console_put_str("ub:");
console_put_int(test_var_b);
console_put_str(" kb:");
console_put_int(sys_getpid());
console_put_str(" ");
}
}

/* 测试用户进程 */
void u_prog_a(void) {
while(1) {
test_var_a = getpid();
}
}

/* 测试用户进程 */
void u_prog_b(void) {
while(1) {
test_var_b = getpid();
}
}

注意别忘了在Makefile中添加新增的文件,然后make mouse,运行之后就可以看到屏幕上正常显示便是成功啦,但注意我们现在还不支持浮点数


C.4 完善堆内存管理

完善堆内存管理也就是来实现malloc的接口以及底层实现

C4.1 malloc 底层原理

这里要引用一个新的名词:“arena”,这里还是引用书中部分句子

arena 是由“一大块内存”被划分成无数“小内存块”的内存仓库,我们在原有内存管理系统的基础上实现 arena,大伙儿知道,原有系统只能分配 4KB 粒度的内存页框,因此 arena 的这“一大块内存”也是通过 malloc_page 获得的以 4KB 为粒度的内存,根据请求的内存量的大小,arena 的大小也许是 1 个页框,也许是多个页框,随后再将它们平均拆分成多个小内存块。按内存块的大小,可以划分出多种不同规格的 arena,比如一种 arena 中全是 16 字节大小的内存块,故它只响应 16 字节以内的内存分配,另一种arena 中全是 32 字节的内存块,故它只响应 32 字节以内的内存分配。我们平时调用 malloc 申请内存时,操作系统返回的地址其实就是某个内存块的起始地址,操作系统会根据 malloc 申请的内存大小来选择不同规格的内存块。因此,为支持多种容量内存块的分配,我们要提前建立好多种不同容量内存块的 arena

arena 是个提供内存分配的数据结构,它分为两部分,一部分是元信息,用来描述自己内存池中空闲内存块数量,这其中包括内存块描述符指针(后面介绍),通过它可以间接获知本 arena 所包含内存块的规格大小,此部分占用的空间是固定的,约为 12 字节。另一部分就是内存池区域,这里面有无数的内存块,此部分占用 arena 大量的空间。我们把每个内存块命名为 mem_block,它们是内存分配粒度更细的资源,最终为用户分配的就是这其中的一个内存块。在咱们的实现中,针对小内存块的 arena 占用 1 页框内存,除了元信息外的剩下的内存被平均分成多个小内存块

尽管 arena 用小内存块来满足小内存量的分配,但实际上,arena 为内存分配提供了统一的入口,无论申请的内存量是多大,都可以用同一个 arena 来分配内存。小内存块的容量虽然有几种规格,但毕竟是为满足“小”内存量分配的,最大内存块容量不会超过 1024 字节,如果申请的内存量较大,超过 1024 字节,单独的一个小内存块无法满足需求时,这时候您可能想,将多个内存块组合到一起,肯定能满足需求,团结力量大嘛。方法虽具有可行性,但还是太麻烦了,动态维护内存块的信息会增加编程复杂性,这似乎有些像 Linux 的 buddy 系统啦。其实咱们的应用很简单,根本用不着那么麻烦,处理大内存请求时也会创建个 arena,但不会再将它拆分成小内存块,而是直接将整块大内存分配出去,确实有些简单粗暴,但很有效。故此类 arena 没有对应的内存块描述符,元信息中的内存块描述符指针为空

当申请的内存大于 1024 字节时,因此我们对大内存的定义就是大于1024 字节。为什么要以 1024 为界限呢?有这样一个提前,就是用于处理小内存块时,我们为 arena 分配 1 页框也就是 4KB 大小的内存,我们已经介绍过了,每个 arena 都分为两部分,一部分是占用空间很少的元信息,除元信息外的剩余部分才用于内存块的划分,因此,真正用于内存块的部分不足 4KB。内存块是平均划分的,所以最大的内存块肯定要小于 2KB,这里我们以 2 为底的指数方程来划分内存块,因此最大的内存块是1024 字节,也就是说对内存块规格为 1024 字节的 arena 来说,它只有 3 个内存块,每个都是 1024 字节,剩余的部分就浪费了。咱们这里的内存块以 16 字节为起始,向上依次是 32 字节、64 字节、128 字节、256字节、512 字节、1024 字节,总共 7 种规格,因此,内存块描述符也就这 7 种。再次强调一下,每种 arena中只有一种规格的内存块,并不是同时包含多种规格,比如要么该 arena 中全是 16 字节大小的内存块,要么全是 512 字节的内存块。对于小内存块来说,系统为 arena 分配的内存总共为 4KB,因此,不同规格arena 中的内存块数量也是不同的,举例来说,假设 arena 元信息大小为 12 字节,对于内存块规格 16 字节的 arena,其包括的内存块数量是(4096-12)/16,对于内存块规格 128 字节的 arena,其包括的内存块数量是(4096-12)/128


下面还是在代码中理解:

C4.2 底层初始化

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

#include "list.h"

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

/* 内存块 */
struct mem_block{
struct list_elem free_elem;
};

/* 内存块描述符 */
struct mem_block_desc{
uint32_t block_size; //内存块大小
uint32_t blocks_per_arena; //本arena可容纳此mem_block的数量
struct list free_list; //目前可用的mem_block链表
};

// 16、32、64、128、256、512、1024 字节
// 当申请的内存大小超过 1024 时就直接返回一个页框,不再从 arena 中划分
#define DESC_CNT 7 //内存块描述符个数


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

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

//内存仓库
struct arena{
struct mem_block_desc* desc; //此arena关联的mem_block_desc 描述符
//large为true时,cnt表示的是叶框数
//否则表示空闲mem_block的数量
uint32_t cnt;
bool large;
};

struct mem_block_desc k_block_descs[DESC_CNT]; //内核内存块描述符数组
struct pool kernel_pool, user_pool; // 生成内核内存池和用户内存池
struct virtual_addr kernel_vaddr; // 此结构是用来给内核分配虚拟地址

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


//为malloc做准备 -- 注意在头文件声明,等会其它文件中需要调用
void block_desc_init(struct mem_block_desc* desc_array)
{
uint16_t desc_idx,block_size = 16;
//初始化每个mem_block_desc结构
for(desc_idx = 0;desc_idx < DESC_CNT;desc_idx++)
{
desc_array[desc_idx].block_size = block_size;

//初始化arena中的内存块数量
desc_array[desc_idx].blocks_per_arena = (PG_SIZE - sizeof(struct arean))/block_size;
list_init(&desc_array[desc_idx].free_list);
block_size *=2; //更新为下一个内存块规格
}
}

/* 内存管理部分初始化入口 */
void mem_init()
{
put_str("mem_init start\n");
uint32_t mem_bytes_total = (*(uint32_t*)(0xb00));
lock_init(&kernel_pool.lock); //初始化锁
lock_init(&user_pool.lock);
mem_pool_init(mem_bytes_total); //初始化内存池
block_desc_init(k_block_descs); //初始化mem_block_desc数组
put_str("mem_init done\n");
}

C4.3 实现sts_malloc

内存资源需要在需要的时候由系统动态创建,创建它的函数就是sys_malloc,下面要在thread.h中对pcb略微改动一下

为了实现用户进程的堆内存管理,在 pcb 中增加了内存块描述符数组 u_block_desc[DESC_CNT],同理,它也要初始化,所以要在process.c中的process_execute中进行初始化工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*略................*/

/* 进程或线程的 pcb,程序控制块 */
struct task_struct {
uint32_t* self_kstack; // 各内核线程都用自己的内核栈,注意为首位
pid_t pid; // 任务的pid
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; //进程自己的虚拟地址

struct virtual_addr userprog_vaddr; //用户进程的虚拟地址
struct mem_block_desc u_block_desc[DESC_CNT]; //用户进程内存块描述符

uint32_t stack_magic; // 栈的边界标记,用于检测栈的溢出,注意为尾部
};

/*略................*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*略................*/

//创建用户进程
void process_execute(void* filename,char*name)
{
//pcb内核的数据结构,由内核来维护进程信息,因此要在内核内存池中申请
struct task_struct* thread = get_kernel_pages(1);
init_thread(thread,name,default_prio); //默认优先级31
create_user_vaddr_bitmap(thread); //创建位图
thread_create(thread,start_process,filename); //通过start_process函数退出中断进入用户进程
thread->pgdir = create_page_dir(); //创建页目录表
block_desc_init(thread->u_block_desc); //初始化内存块描述符

enum intr_status old_status = intr_disable();

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

intr_set_status(old_status);
}
/*略................*/

然后是对内存管理方面的修改memory.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
//返回arena中第idx个内存块的地址
static struct mem_block* arena2block(struct arena* a,uint32_t idx)
{
return (struct mem_block*) ((uint32_t)a + sizeof(struct arena) + idx * a->desc->block_size);
}

//返回内存块b所在的arena地址
//由于此类内存块所在的 arena 占据 1 个完整的自然页框,所以 arena 中的内存块都属于这
//页框之内,因此函数原理很简单,内存块的高 20 位地址便是 arena 所在的地址
static struct arena* block2arena(struct mem_block* b)
{
return (struct arena*)((uint32_t)b & 0xfffff000);
}

//在堆中申请size字节内存
void* sys_malloc(uint32_t size)
{
enum pool_flags PF;
struct pool* mem_pool; //内存池结构
uint32_t pool_size;
struct mem_block_desc* descs;
struct task_struct* cur_thread = running_thread(); //获取当前pcb

//判断是哪个内存池
if(cur_thread->pgdir == NULL) //若为内核线程
{
PF = PF_KERNEL; //内核线程
pool_size = kernel_pool.pool_size;
mem_pool = &kernel_pool;
descs = k_block_descs;
}
else //若为用户进程pcb,pgdir不为空
{
PF = PF_USER;
pool_size = user_pool.pool_size;
mem_pool = &user_pool;
descs = cur_thread->u_block_desc;
}

//若申请的内存不在内存池的容量范围内,则直接返回NULL
if(!(size > 0 && size < pool_size))
{
return NULL;
}
struct arena* a;
struct mem_block* b;
lock_acquire(&mem_pool->lock); //上锁

if(size>1024) //如果大于1024,就直接分配页框
{
uint32_t page_cnt = DIV_ROUND_UP(size + sizeof(struct arena),PG_SIZE); //直接向上取整

a = malloc_page(PF,page_cnt); //获取页框

if(a!= NULL)
{
memset(a,0,page_cnt * PG_SIZE); //将分配的内存清0

//对于分配的大块页框,将desc置NULL,cnt就是页框数,large置true
a->desc = NULL;
a->cnt = page_cnt;
a->large = true;
lock_release(&mem_pool->lock); //解锁
return (void*)(a+1); //跨过arena大小,把剩下的内存返回
}
else
{
lock_release(&mem_pool->lock); //解锁
return NULL;
}
}
else //小于1024,则通过不同的mem_block_desc去适配
{
uint8_t desc_idx;
//从内存块描述符中找到合适的内存块规格
for(desc_idx = 0; desc_idx < DESC_CNT ; desc_idx++)
{
if(size <= descs[desc_idx].block_size)
{//从小到大寻找
break;
}
}
//若 mem_block_desc 的 free_list 中已经没有可用的 mem_block,就创建新的 arena 提供 mem_block
if(list_empty(&descs[desc_idx].free_list))
{
a = malloc_page(PF,1); //分配1页框作为arena
if(a == NULL)
{
lock_release(&mem_pool->lock);
return NULL;
}
memset(a,0,PG_SIZE); //将内存块清零

//对于分配的小块内存,将desc置为相应的内存块描述符
//cnt为此arena可用的内存块数,large置为flase
a->desc = &descs[desc_idx];
a->large = false;
a->cnt = descs[desc_idx].blocks_per_arena;
uint32_t block_idx;

enum intr_status old_status = intr_disable(); //关中断
//开始将arena拆分为内存块,并添加到内存块描述符的free_list中
for(block_idx = 0;block_idx < descs[desc_idx].blocks_per_arena;block_idx++)
{
b = arena2block(a,block_idx); // 寻找第block_idx个内存块初始地址
ASSERT(!elem_find(&a->desc->free_list, &b->free_elem)); //保证free_list中没有
list_append(&a->desc->free_list,&b->free_elem); //添加到链表中
}
intr_set_status(old_status);
}

//开始分配内存块
b = elem2entry(struct mem_block,free_elem, list_pop(&(descs[desc_idx].free_list))); //通过elem找到mem_block的地址,即之前内存块拆分的时候b的地址,这就是最终分配的地址
memset(b,0,descs[desc_idx].block_size); //置0
a = block2arena(b); //获取b所在的arena;
a->cnt--; //此arena中的空闲内存块数减1
lock_release(&mem_pool->lock); //关锁
return (void*)b;
}
}

然后我们在头文件添加对应的声明,最后就是对这个函数进行测试了

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
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall.h"
#include "syscall-init.h"
#include "stdio.h"

void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);
int test_var_a = 0, test_var_b = 0;

int main(void) {
put_str("I am kernel\n");
init_all();

thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 31, k_thread_b, "argB ");

intr_enable();
while(1);
return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
char* para = arg;
void* addr = sys_malloc(33);
console_put_str(" I am thread_a, sys_malloc(33), addr is 0x");
console_put_int((int)addr);
console_put_char('\n');
while(1);
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
char* para = arg;
void* addr = sys_malloc(63);
console_put_str(" I am thread_b, sys_malloc(63), addr is 0x");
console_put_int((int)addr);
console_put_char('\n');
while(1);
}

/* 测试用户进程 */
void u_prog_a(void) {
while(1) {
test_var_a = getpid();
}
}

/* 测试用户进程 */
void u_prog_b(void) {
while(1) {
test_var_b = getpid();
}
}

在两个内核线程中分别申请33,63字节的内存,按照我们写的逻辑,都会选择分配64字节的内存,因此 sys_malloc 会创建规格为 64 字节的 arena,然后把它拆分成 64 字节的内存块,由于是第 1 次申请内存且只申请了一种内存块,故系统中只存在这一个 arena,那么我们的结果如果相差16进制的4,也就是十进制的64字节,说明它给分配的就是64字节,符合预期

I am thread_b, sys_malloc(63), addr is OxC010204C
I am thread_a, sys_malloc(33), addr is OxC010200C


C4.4 内存的释放

内存管理系统不仅能分配内存,还应该能回收内存,这是最基本的内存管理机制

回忆一下:内存的使用情况都是通过位图来管理的,因此,无论内存的分配或释放,本质上都是在设置相关位图中的相应位,都是在读写位图。回收物理地址就是将物理内存池位图中的相应位清 0,无需将该 4KB 物理页框逐字节清 0。回收虚拟地址就是将虚拟内存池位图中的相应位清 0。分配则是相反的,也就是将位图中相应位置为 1 即可

在之前,我们申请的内存是通过malloc_page中,分别经历在虚拟地址池和物理地址池中分配地址(位图同理),在页表中完成虚拟地址和物理地址的映射

而释放内存刚好和这个相反,我们首先要在物理地址池释放物理页地址(位图同理)在页表中去掉虚拟地址的映射(将虚拟地址对应 pte 的 P 位置 0 page_table_ pte_remove),最后在虚拟地址池中释放虚拟地址(vaddr_remove)
下面我们将这几个步骤封装到函数mfree_page中

开始搓代码:

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

//将物理地址pg_phy_addr 回收到物理内存池-- 回收一个物理页
void pfree(uint32_t pg_phy_addr)
{
struct pool* mem_pool;
uint32_t bit_idx = 0;
if(pg_phy_addr >= user_pool.phy_addr_start) //用户物理内存池
{
mem_pool = &user_pool;
bit_idx = (pg_phy_addr - user_pool.phy_addr_start)/PG_SIZE;
}
else //内核物理内存池
{
mem_pool = &kernel_pool;
bit_idx = (pg_phy_addr - kernel_pool.phy_addr_start)/PG_SIZE;
}
bitmap_set(&mem_pool->pool_bitmap,bit_idx,0); //将位图清零
}

//去掉页表中虚拟地址 vaddr 的映射,只去掉 vaddr 对应的 pte
static void page_table_pte_remove(uint32_t vaddr)
{
uint32_t* pte = pte_ptr(vaddr);
*pte &= ~PG_P_1; // 将页表项 pte 的 P 位置 0
asm volatile ("invlpg %0"::"m" (vaddr):"memory"); //更新 tlb 快表
}

//在虚拟地址池中释放以_vaddr 起始的连续 pg_cnt 个虚拟页地址
static void vaddr_remove(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt)
{
uint32_t bit_idx_start = 0, vaddr = (uint32_t)_vaddr, cnt = 0;

if (pf == PF_KERNEL) // 内核虚拟内存池
{
bit_idx_start = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;
while(cnt < pg_cnt)
{
bitmap_set(&kernel_vaddr.vaddr_bitmap,bit_idx_start + cnt++, 0);
}
}
else // 用户虚拟内存池
{
struct task_struct* cur_thread = running_thread();
bit_idx_start =(vaddr - cur_thread->userprog_vaddr.vaddr_start) / PG_SIZE;
while(cnt < pg_cnt)
{
bitmap_set(&cur_thread->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);
}
}
}

//释放以虚拟地址 _vaddr 为起始的 pg_cnt 个物理页框
void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt)
{
uint32_t pg_phy_addr;
uint32_t vaddr = (int32_t)_vaddr,page_cnt = 0;
ASSERT(pg_cnt>=1 && vaddr%PG_SIZE == 0);
pg_phy_addr = addr_v2p(vaddr); //获取虚拟地址vaddr对应的物理地址

//确保待释放的物理内存在低端1MB+1kb大小的页目录——1kb大小的页表地址范围外
ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= 0x102000);

//判断 pg_phy_addr 属于用户物理内存池还是内核物理内存池
if(pg_phy_addr >= user_pool.phy_addr_start) //user
{
vaddr -= PG_SIZE;
while (page_cnt < pg_cnt)
{
vaddr += PG_SIZE;
pg_phy_addr = addr_v2p(vaddr);
//确保物理地址属于用户物理内存池
ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= user_pool.phy_addr_start);
//先将对应的物理页框归还到内存池
pfree(pg_phy_addr);
//再从页表中清除此虚拟地址所在的页表项 pte
page_table_pte_remove(vaddr);
page_cnt++;
}
//清空虚拟地址的位图中的相应位
vaddr_remove(pf, _vaddr, pg_cnt);
}
else //kernel
{
vaddr -= PG_SIZE;
while (page_cnt < pg_cnt)
{
vaddr += PG_SIZE;
pg_phy_addr = addr_v2p(vaddr);
//确保待释放的物理内存只属于内核物理内存池
ASSERT((pg_phy_addr % PG_SIZE) == 0 && pg_phy_addr >= kernel_pool.phy_addr_start && pg_phy_addr < user_pool.phy_addr_start);
//先将对应的物理页框归还到内存池
pfree(pg_phy_addr);
//再从页表中清除此虚拟地址所在的页表项 pte
page_table_pte_remove(vaddr);
page_cnt++;
}
//清空虚拟地址的位图中的相应位
vaddr_remove(pf, _vaddr, pg_cnt);
}
}

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

C4.5 实现sys_free

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
// 回收内存 ptr
void sys_free(void* ptr)
{
ASSERT(ptr != NULL);
if (ptr != NULL)
{
enum pool_flags PF;
struct pool* mem_pool;

//判断是线程,还是进程
if (running_thread()->pgdir == NULL) //内核
{
ASSERT((uint32_t)ptr >= K_HEAP_START);
PF = PF_KERNEL;
mem_pool = &kernel_pool;
}
else //用户
{
PF = PF_USER;
mem_pool = &user_pool;
}

lock_acquire(&mem_pool->lock); //上锁
struct mem_block* b = ptr;
struct arena* a = block2arena(b);
//把 mem_block 转换成 arena,获取元信息
ASSERT(a->large == 0 || a->large == 1);
if (a->desc == NULL && a->large == true) // 大于 1024 的内存
{
mfree_page(PF, a, a->cnt);
}
else // 小于等于 1024 的内存块
{
//先将内存块回收到 free_list
list_append(&a->desc->free_list, &b->free_elem);

//再判断此 arena 中的内存块是否都是空闲,如果是就释放 arena
if (++a->cnt == a->desc->blocks_per_arena)
{
uint32_t block_idx;
for (block_idx = 0;block_idx < a->desc->blocks_per_arena; block_idx++)
{
struct mem_block* b = arena2block(a, block_idx);
ASSERT(elem_find(&a->desc->free_list, &b->free_elem));
list_remove(&b->free_elem);
}
mfree_page(PF, a, 1);
}
}
lock_release(&mem_pool->lock); //释放锁
}
}

下面还是在主函数中进行测试,这里在主函数的两个内核线程中进程1000次的获取和释放内存:

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

void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);

int main(void) {
put_str("I am kernel\n");
init_all();
intr_enable();
thread_start("k_thread_a", 8, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 8, k_thread_b, "I am thread_b ");
while(1);
return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
char* para = arg;
void* addr1;
void* addr2;
void* addr3;
void* addr4;
void* addr5;
void* addr6;
void* addr7;
console_put_str(" thread_a start\n");
int max = 1000;
while (max-- > 0) {
printf("a: %d\n",1000-max);
int size = 128;
addr1 = sys_malloc(size);
size *= 2;
addr2 = sys_malloc(size);
size *= 2;
addr3 = sys_malloc(size);
sys_free(addr1);
addr4 = sys_malloc(size);
size *= 2; size *= 2; size *= 2; size *= 2;
size *= 2; size *= 2; size *= 2;
addr5 = sys_malloc(size);
addr6 = sys_malloc(size);
sys_free(addr5);
size *= 2;
addr7 = sys_malloc(size);
sys_free(addr6);
sys_free(addr7);
sys_free(addr2);
sys_free(addr3);
sys_free(addr4);
}
console_put_str(" thread_a end\n");
while(1);
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
char* para = arg;
void* addr1;
void* addr2;
void* addr3;
void* addr4;
void* addr5;
void* addr6;
void* addr7;
void* addr8;
void* addr9;
int max = 1000;
console_put_str(" thread_b start\n");
while (max-- > 0) {
printf("b: %d\n",1000-max);
int size = 9;
addr1 = sys_malloc(size);
size *= 2;
addr2 = sys_malloc(size);
size *= 2;
sys_free(addr2);
addr3 = sys_malloc(size);
sys_free(addr1);
addr4 = sys_malloc(size);
addr5 = sys_malloc(size);
addr6 = sys_malloc(size);
sys_free(addr5);
size *= 2;
addr7 = sys_malloc(size);
sys_free(addr6);
sys_free(addr7);
sys_free(addr3);
sys_free(addr4);

size *= 2; size *= 2; size *= 2;
addr1 = sys_malloc(size);
addr2 = sys_malloc(size);
addr3 = sys_malloc(size);
addr4 = sys_malloc(size);
addr5 = sys_malloc(size);
addr6 = sys_malloc(size);
addr7 = sys_malloc(size);
addr8 = sys_malloc(size);
addr9 = sys_malloc(size);
sys_free(addr1);
sys_free(addr2);
sys_free(addr3);
sys_free(addr4);
sys_free(addr5);
sys_free(addr6);
sys_free(addr7);
sys_free(addr8);
sys_free(addr9);
}
console_put_str(" thread_b end\n");
while(1);
}

/* 测试用户进程 */
void u_prog_a(void) {
char* name = "prog_a";
printf(" I am %s, my pid:%d%c", name, getpid(),'\n');
while(1);
}

/* 测试用户进程 */
void u_prog_b(void) {
char* name = "prog_b";
printf(" I am %s, my pid:%d%c", name, getpid(), '\n');
while(1);
}

我们通过查看内存池位图来验证,在k_thread_a位置打断点

1
2
nm build/kernel/kernel.bin |grep -P 'k_thread_a'    #获取k_thread_a地址
# c000155f 是我得到的地址
1
2
3
lb 0xc000155f   #打断点

x/3 0xc009a000 #查看位图

我们可以在第一次进入断点查看一次位图,然后再过程中和结束后都查看一下位图,如果结束之后,位图的值一样,就算是获取释放成功啦

1
2
3
4
5
6
7
8
<bochs:1> lb 0xc000155f
<bochs:2> c
(0) Breakpoint 1, 0xc000155f in ?? ()
Next at t=18231903
(0) [0x00000000155f] 0008:c000155f (unk. ctxt): push ebp ; 55
<bochs:3> x/3 0xc009a000
[bochs]:
0xc009a000 <bogus+ 0>: 0x00000003 0x00000000 0x00000000

这里的0x00000003,0x3 的二进制是 11,这表示用了 2 个页框,原因是创建两个线程时各用了 1 个页框做其 pcb,因此位图的使用情况与预期相符,等运行结束,也就是输出thread_a/b end的时候,应该和这个数字一样,并且中途如果暂停,位图的值应该会有不同的值

下面就来实现系统调用吧…….


C4.6 实现系统调用malloc && free

这里我们和书中一样,将这两个接口放到syscall.h文件中(Linux是放在stdlib.h中)

下面直接上代码,和之前的系统调用差不多

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/lib/user/syscall.hs

#ifndef __LIB_USER_SYSCALL_H
#define __LIB_USER_SYSCALL_H

#include "stdint.h"

enum SYSCALL_NR{ //存放系统调用子功能号
SYS_GETPID,
SYS_WRITE,
SYS_MALLOC,
SYS_FREE
};

uint32_t getpid(void);
uint32_t write(char* str);
void* malloc(uint32_t size);
void free(void* ptr);

#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
// /home/mouse/OS_mouse/tool/bochs/mouse/lib/user/syscall.c

#include "syscall.h"

//无参数的系统调用
#define _syscall0(NUMBER)({ \
int retval; \
asm volatile( \
"int $0x80" \
: "=a" (retval) \
: "a" (NUMBER) \
: "memory" \
); \
retval; \
})


//1个参数的系统调用
#define _syscall1(NUMBER, ARG1) ({ \
int retval; \
asm volatile ( \
"int $0x80" \
: "=a" (retval) \
: "a" (NUMBER), "b" (ARG1) \
: "memory" \
); \
retval; \
})

//2个参数的系统调用
#define _syscall2(NUMBER, ARG1, ARG2) ({ \
int retval; \
asm volatile ( \
"int $0x80" \
: "=a" (retval) \
: "a" (NUMBER), "b" (ARG1), "c" (ARG2) \
: "memory" \
); \
retval; \
})

//3个参数的系统调用
#define _syscall3(NUMBER, ARG1, ARG2, ARG3) ({ \
int retval; \
asm volatile ( \
"int $0x80" \
: "=a" (retval) \
: "a" (NUMBER), "b" (ARG1), "c" (ARG2), "d" (ARG3) \
: "memory" \
); \
retval; \
})

uint32_t getpid(void)
{
return _syscall0(SYS_GETPID);
}

uint32_t write(char* str)
{
return _syscall1(SYS_WRITE,str);
}

void* malloc(uint32_t size)
{
return (void*)_syscall1(SYS_MALLOC,size);
}

void free(void* ptr)
{
_syscall1(SYS_FREE,ptr);
}

然后是更新数组syscall_table

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
// /home/mouse/OS_mouse/tool/bochs/mouse/userprog/syscall-init.c

#include "syscall-init.h"
#include "stdint.h"
#include "thread.h"
#include "syscall.h"
#include "print.h"
#include "console.h"
#include "string.h"

#define syscall_nr 32
typedef void* syscall;
syscall syscall_table[syscall_nr]; //存储系统调用的函数

/* 返回当前任务的 pid */
uint32_t sys_getpid(void)
{
return running_thread()->pid;
}

uint32_t sys_write(char* str)
{
console_put_str(str);
return strlen(str);
}

/* 初始化系统调用 */
void syscall_init(void)
{
put_str("syscall_init start\n");
syscall_table[SYS_GETPID] = sys_getpid; //获取pid
syscall_table[SYS_WRITE] = sys_write; //write
syscall_table[SYS_MALLOC] = sys_malloc; //write
syscall_table[SYS_FREE] = sys_free; //write
put_str("syscall_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
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
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"
#include "memory.h"

void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);

int main(void) {
put_str("I am kernel\n");
init_all();
intr_enable();
process_execute(u_prog_a, "u_prog_a");
process_execute(u_prog_b, "u_prog_b");
thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
while(1);
return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
void* addr1 = sys_malloc(256);
void* addr2 = sys_malloc(255);
void* addr3 = sys_malloc(254);
console_put_str(" thread_a malloc addr:0x");
console_put_int((int)addr1);
console_put_char(',');
console_put_int((int)addr2);
console_put_char(',');
console_put_int((int)addr3);
console_put_char('\n');

int cpu_delay = 100000;
while(cpu_delay-- > 0);
sys_free(addr1);
sys_free(addr2);
sys_free(addr3);
while(1);
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
void* addr1 = sys_malloc(256);
void* addr2 = sys_malloc(255);
void* addr3 = sys_malloc(254);
console_put_str(" thread_b malloc addr:0x");
console_put_int((int)addr1);
console_put_char(',');
console_put_int((int)addr2);
console_put_char(',');
console_put_int((int)addr3);
console_put_char('\n');

int cpu_delay = 100000;
while(cpu_delay-- > 0);
sys_free(addr1);
sys_free(addr2);
sys_free(addr3);
while(1);
}

/* 测试用户进程 */
void u_prog_a(void) {
void* addr1 = malloc(256);
void* addr2 = malloc(255);
void* addr3 = malloc(254);
printf(" prog_a malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);

int cpu_delay = 100000;
while(cpu_delay-- > 0);
free(addr1);
free(addr2);
free(addr3);
while(1);
}

/* 测试用户进程 */
void u_prog_b(void) {
void* addr1 = malloc(256);
void* addr2 = malloc(255);
void* addr3 = malloc(254);
printf(" prog_b malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);

int cpu_delay = 100000;
while(cpu_delay-- > 0);
free(addr1);
free(addr2);
free(addr3);
while(1);
}

本次 main.c 中启了 4 个任务,分别是 2 个用户进程和 2 个内核线程。它们分别都申请了 256、255、254 字节大小的内存,因此它们对应的内存块规格都应该是 256 字节。所有内核线程共享内存空间,因此线程函数 k_thread_a 和 k_thread_b 所申请的内存应该会有地址累加的情况。用户进程拥有独立的内存空间,因此在申请内存时,都会从自己的堆空间从头算起,并不会产生地址累加的情况。256 的十六进制形式是0x100,比较方便查看地址累加的情况,这就是我们选择规格为 256 字节内存块的原因

下面是正确运行结果:

1
2
3
4
prog_a mal1oc addr:0x804800C,Ox804810C,Ox804820C
prog_b malloc addr:Ox804800C,0x804810C,0x804820C
thread_a malloc addr:0xC013400C,C013410C,C013420C
thread_b malloc addr:0xC013430C,C013440C,C013450C

终于,内存分配到此结束~


D 编写硬盘驱动程序

之前我们写的东西一直都在第一课配环境创建的硬盘中,我们和书中一样,把那个作为启动盘,仅仅存放内核,现在我们再创建一个80M的硬盘来存储文件系统


D.1 硬盘及分区表

创建硬盘还是利用bochs的命令bximage,此命令在bochs安装目/bin/

D1.1 创建从盘及获取安装的磁盘数

下面给出步骤(原书中图片–画横线的位置是要输入的内容–回车用中文表示)

然后会得到一串内容ata0-master: type=disk, path="hd80M.img", mode=flat, cylinders=162, heads=16, spt=63,我们需要添加到配置文件中,同时要把ata0-master(主盘)修改为ata0-slave(从盘),其他的参数不变

修改后的文件如下:

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
###############################################
# Configuration file for Bochs
###############################################

# 第一步,首先设置 Bochs 在运行过程中能够使用的内存,本例为 32MB。
# 关键字为:megs
megs: 32

# 第二步,设置对应真实机器的 BIOS 和 VGA BIOS。
# 对应两个关键字为:romimage 和 vgaromimage
romimage: file=/home/mouse/OS_mouse/tool/bochs/share/bochs/BIOS-bochs-latest
vgaromimage: file=/home/mouse/OS_mouse/tool/bochs/share/bochs/VGABIOS-lgpl-latest

# 第三步,设置 Bochs 所使用的磁盘,软盘的关键字为 floppy。
# 若只有一个软盘,则使用 floppya 即可,若有多个,则为 floppya,floppyb…
# floppya: 1_44=a.img, status=inserted

# 第四步,选择启动盘符。
#boot: floppy #默认从软盘启动,将其注释
boot: disk #改为从硬盘启动。我们的任何代码都将直接写在硬盘上,所以不会再有读写软盘的操作。

# 第五步,设置日志文件的输出。
log: bochs.out

# 第六步,开启或关闭某些功能。
# 下面是关闭鼠标,并打开键盘。
mouse: enabled=0
keyboard_mapping: enabled=1, map=/home/mouse/OS_mouse/tool/bochs/share/bochs/keymaps/x11-pc-us.map

# 硬盘设置
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata0-master: type=disk, path="hd60M.img", mode=flat, cylinders=121, heads=16, spt=63
ata0-slave: type=disk, path="hd80M.img", mode=flat, cylinders=162, heads=16, spt=63

# 下面的是增加的 bochs 对 gdb 的支持,这样 gdb 便可以远程连接到此机器的 1234 端口调试了
# gdbstub: enabled=1, port=1234, text_base=0, data_base=0, bss_base=0
################### 配置文件结束 #####################

在修改上面文件之前,我们可以通过指令来验证是否添加成功,因为硬盘是通过BIOS来识别的

添加之前,通过指令xp/b 0x475来读取磁盘的数量,再修改文件之前,输出的结果应该是0x01,添加之后如果数字变成离开0x02,就说明添加成功啦

ps:这是通过BIOS来识别写入的,所以要先c,运行BIOS程序之后,才能通过指令读取,否则读出来肯定全都是0x00


D1.2 创建磁盘分区表及简介

文件系统是运行在操作系统中的软件模块,是操作系统提供的一套管理磁盘文件读写的方法和数据组织、存储形式,因此,文件系统=数据结构+算法,哈哈,所以它是程序。它的管理对象是文件,管辖范围是分区,因此它建立在分区的基础上,每个分区都可以有不同的文件系统

本节的任务是将刚刚创建的磁盘进行分区,使用fdisk工具

(1)硬盘容量=单片容量×磁头数。
(2)单片容量=每磁道扇区数×磁道数×512 字节。
磁道数又等于柱面数,因此将公式 2 代入公式 1 后:
硬盘容量=每磁道扇区数×柱面数×512 字节×磁头数
ata0-slave: type=disk, path=”hd80M.img”, mode=flat, cylinders=162(柱面数), heads=16(磁头), spt=63(每磁道扇区数)

书中这里介绍了磁盘的具体组成和分区的详细内容,这里就只简略写几句

分区是逻辑上划分磁盘空间的方式,归根结底是人为地将硬盘上的柱面扇区划分成不同的分组,每个分组都是单独的分区。各分区都有“描述符”来描述分区本身所在硬盘上的起止界限等信息,在硬盘的MBR 中有个 64 字节“固定大小”的数据结构,这就是著名的分区表,分区表中的每个表项就是一个分区的“描述符”,表项大小是 16 字节,因此 64 字节的分区表总共可容纳 4 个表项,这就是为什么硬盘仅支持 4 个分区的原因(当初硬盘制造者认为,一台机器上顶多安装 4 个操作系统,每个操作系统各占 1 个分区,所以硬盘支持 4 个分区足矣)……….

下面来实操一下分区:

因为fdisk的版本有所不同,所以这里需要指定兼容模式,并且打开柱面设置才可以与书中展示的一致,整体命令如下:

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
fdisk -c="dos" -u="cylinders"  ./hd80M.img  #以兼容模式进入

x #进入专家模式
c #设置柱面
162 #设置为162
h #设置磁头
16 #设置为16
r #返回上一级菜单

n #创建分区
p #创建主分区
1 #指定主分区号
1 #起始柱面,默认值1
32 #末尾柱面

n #创建分区
e #创建拓展分区
4 #指定拓展分区号
33 #起始柱面,默认值33
162 #末尾柱面,默认值162

p #查看当前分区

#设备 启动 Start 末尾 柱面 Size Id 类型
#./hd80M.img1 1 32 32 15.7M 83 Linux
#./hd80M.img4 33 162 131 64M 5 扩展

n #创建分区
l #分区满了,默认创建逻辑分区(如果没有选项就不用输入这个指令)
33 #逻辑分区起始柱面
50 #逻辑分区末尾柱面

#.....后面同理,分别创建51~75 76~90 91~120 121~162四个逻辑分区
n
51
75
n
76
90
n
91
120
n
121
162

p #查看分区

#设备 启动 Start 末尾 柱面 Size Id 类型
#./hd80M.img1 1 32 32 15.7M 83 Linux
#./hd80M.img4 33 162 131 64M 5 扩展
#./hd80M.img5 33 50 18 8.8M 83 Linux
#./hd80M.img6 51 75 25 12.3M 83 Linux
#./hd80M.img7 76 90 15 7.4M 83 Linux
#./hd80M.img8 91 120 30 14.8M 83 Linux
#./hd80M.img9 121 162 42 20.7M 83 Linux

l #查看已知的文件系统

#......略

t #设置分区id
5 #选择id
66 #自己定义的类型
6 #选择id
66 #自己定义的类型
7 #选择id
66 #自己定义的类型
8 #选择id
66 #自己定义的类型
9 #选择id
66 #自己定义的类型

p #打印分区表

#Disk ./hd80M.img: 79.8 MiB, 83607552 bytes, 163296 sectors
#Geometry: 16 heads, 63 sectors/track, 162 cylinders
#Units: cylinders of 1008 * 512 = 516096 bytes
#Sector size (logical/physical): 512 bytes / 512 bytes
#I/O size (minimum/optimal): 512 bytes / 512 bytes
#Disklabel type: dos
#Disk identifier: 0xfbedafc1
#
#设备 启动 Start 末尾 柱面 Size Id 类型
#./hd80M.img1 1 32 32 15.7M 83 Linux
#./hd80M.img4 33 162 131 64M 5 扩展
#./hd80M.img5 33 50 18 8.8M 66 未知
#./hd80M.img6 51 75 25 12.3M 66 未知
#./hd80M.img7 76 90 15 7.4M 66 未知
#./hd80M.img8 91 120 30 14.8M 66 未知
#./hd80M.img9 121 162 42 20.7M 66 未知

w #将分区表写入磁盘并退出fdisk

fdisk -l hd80M.img #查看分区,结果应该和上面的一致

磁盘分区表(Disk Partition Table)简称 DPT,是由多个分区元信息汇成的表,表中每一个表项都对应一个分区,主要记录各分区的起始扇区地址,大小界限等

分区表是由分区工具如 fdisk 创建的,但却是给操作系统使用的。听我这么一说,大伙儿不要误以为操作系统很“弱”,其实操作系统也可以创建分区表,它有底层硬件的一切操作权限,无所不能,只是在通常情况下操作系统直接安装在某个分区上,所以分区表要在内核安装之前建立好,因此分区工具通常独立于操作系统。有了分区表,操作系统(的文件系统)可以根据各表项中的信息对硬盘进行分区管理,只要按照表项中的信息访问磁盘,就不会出现分区越界的情况。分区表既然称为“表”,这表示各个表项的数据结构一致,因此磁盘分区表就是个数组,此数组长度固定为 4,数组元素是分区元信息的结构

最初的磁盘分区表位于 MBR 引导扇区中,下面是它的结构:

(1)主引导记录 MBR –(偏移 0~0x1BD 的空间,共计 446 字节大小)
(2)磁盘分区表 DPT –(偏移 0x1BE~0x1FD 的空间,共64个字节)
(3)结束魔数 55AA,表示此扇区为主引导扇区,里面包含控制程序

  • 拓展分区

来解决原来所支持的分区数太少的问题。大伙儿知道,“分区表的长度固定”才是问题的症结所在。按理来说,扩展分区中包含多少个逻辑分区,扩展分区的分区表中就该有多少表项,可是任何时候新事物的发展都要把“向上兼容”当成头等大事,它既要兼容此固定长度为 4 个分区的分区表,又要突破固定分区数的限制,这似乎有点为难,该怎样设计扩展分区的分区表呢?一个两全其美的方案是视这个扩展分区为总扩展分区,将它划分成多个子扩展分区,每个子扩展分区“在逻辑上”相当于硬盘,因此每个子扩展分区都可以有 1 个分区表。这样一来,各个分区表的长度依然固定为 4,但是允许有无限多个分区表,分区表项多了,自然支持的分区数就多了。如何将这些分区表组织到一起呢?扩展分区表采用链式结构,将所有子扩展分区的分区表串在一起,形成可容纳无限个分区表的单向链表。链表是要有结点的,这里的每个分区表就是结点,一般的链表结点除了包括数据外,还必须要包括下一个结点的地址,分区表也采用了这种结构,其表项就分为两部分,一部分是描述逻辑分区的信息,另一部分是描述下一个子扩展分区的地址

  • 逻辑分区

要想使用分区,就离不开分区表,逻辑分区也是分区,为了使用它,也需要有元信息来描述它的范围、边界、类型等信息,因此在子扩展分区中也要有分区表来描述这些逻辑分区。分区表本身也要在子扩展分区中占磁盘空间,因此实际情况是每个子扩展分区的空间并不是只有逻辑分区,在个子扩展分区中最开始的扇区(剧透一下,此扇区称为 EBR 引导扇区,马上要介绍它)用于存储此子扩展分区中的分区表,此扇区中的内容也是前 446 字节是引导程序,中间 64 字节是分区表,后 2 字节是 0x55 和 0xAA,您看,它同 MBR 引导扇区的结构相同。紧随其后的是空闲的一部分扇区,其余剩下的大部分扇区才被用作存储数据的分区,即逻辑分区。因此,子扩展分区包含逻辑分区

扩展分区被划分出多个子扩展分区,每个子扩展分区都有自己的分区表,所以子扩展分区在逻辑上相当于单独的硬盘,各分区表在各个子扩展分区最开始的扇区中,该扇区同MBR 引导扇区结构相同,由于是经扩展分区划分出来的,所以它们称为 EBR,即扩展引导记录

EBR 中分区表的第一分区表项用来描述所包含的逻辑分区的元信息第二分区表项用来描述下一个子扩展分区的地址第三、四表项未用到。位于 EBR 中的分区表相当于链表中的结点,第一个分区表项存的是分区数据,第二个分区表项存的是后继分区的指针
值得一提的是这两个分区表项都是指向一个分区的起始,起始地址都是个扇区地址,只不过第一个分区表项指向的是该逻辑分区最开始的扇区,此扇区称为操作系统引导扇区,即 OBR 引导扇区。第二个分区表项指向下一个子扩展分区的 EBR 引导扇区

子扩展分区是在总扩展分区中创建的,子扩展分区的偏移扇区理应以总扩展分区的绝对扇区 LBA 地址为基准,因此,“子扩展分区的绝对扇区 LBA 地址=总扩展分区绝对扇区 LBA 地址+子扩展分区的偏移扇区”。

逻辑分区是在子扩展分区中创建的,逻辑分区的偏移扇区理应以子扩展分区的绝对扇区 LBA 地址为基准,因此,“逻辑分区的绝对扇区 LBA 地址=子扩展分区绝对扇区 LBA 地址+逻辑分区偏移扇区”。这里的子扩展分区就是当前子扩展分区

书中有配图,可以更好的理解,这里就不多说了,理论部分到此为止,写写代码吧:


D.2 硬盘初始化

首先是打开硬盘的中断信号(8259A从片的IRQ14上),主盘和从盘共享同一个中断接口,通过设置硬盘控制器的 device 寄存器中第 4 位的 dev 位指定是哪个触发

第 2 个 ata 通道接在 8259A 从片的 IRQ15 上,该 ata 通道上可支持两个硬盘。来自 8259A从片的中断是由 8259A 主片帮忙向处理器传达的,8259A 从片是级联在 8259A 主片的 IRQ2 接口的,因此为了让处理器也响应来自 8259A 从片的中断,屏蔽中断寄存器必须也把 IRQ2 打开

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
// /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


/* IRQ2 用于级联从片,必须打开,否则无法响应从片上的中断
* 主片上打开的中断有 IRQ0 的时钟,IRQ1 的键盘和级联从片的 IRQ2,
* 其他全部关闭
*/
outb (PIC_M_DATA, 0xf8);
// 打开从片上的 IRQ14,此引脚接收硬盘控制器的中断
outb (PIC_S_DATA, 0xbf);

put_str(" pic_init done\n");
}

//略.......

然后为了更方便实现调试输出,所以这里实现内核使用的printf,也就是printk,我们将它定义在lib/kernel/stdio-kernel.c中,同时,这里将va_end等宏移动到stdio.h文件中,方便这个文件调用,否则编译会有问题

1
2
3
4
5
6
7
8
9
10
11
// ;/home/mouse/OS_mouse/tool/bochs/mouse/lib/kernel/stdio-kernel.h

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

//格式化输出字符format
void printk(const char* format,...);

#endif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ;/home/mouse/OS_mouse/tool/bochs/mouse/lib/kernel/stdio-kernel.c

#include "stdio-kernel.h"
#include "stdio.h"
#include "console.h"

//格式化输出字符format
void printk(const char* format,...)
{
va_list args;
va_start(args,format); //使args指向format的栈地址
char buf[1024] = {0}; //用于存储拼接后的字符串
vsprintf(buf,format,args); //转换可变参
va_end(args); //清理原指针
console_put_str(buf); //通过中断输出
}

下面介绍和硬盘相关的数据结构了,它定义在device/ide.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
// /home/mouse/OS_mouse/tool/bochs/mouse/device/ide.h
#ifndef __DEVICE_IDE_H
#define __DEVICE_IDE_H
#include "stdint.h"
#include "sync.h"
#include "bitmap.h"

/* 分区结构 */
struct partition {
uint32_t start_lba; // 起始扇区
uint32_t sec_cnt; // 扇区数
struct disk* my_disk; // 分区所属的硬盘
struct list_elem part_tag; // 用于队列中的标记
char name[8]; // 分区名称
struct super_block* sb; // 本分区的超级块
struct bitmap block_bitmap; // 块位图
struct bitmap inode_bitmap; // i结点位图
struct list open_inodes; // 本分区打开的i结点队列
};

/* 硬盘结构 */
struct disk {
char name[8]; // 本硬盘的名称,如sda等
struct ide_channel* my_channel; // 此块硬盘归属于哪个ide通道
uint8_t dev_no; // 本硬盘是主0还是从1
struct partition prim_parts[4]; // 主分区顶多是4个
struct partition logic_parts[8]; // 逻辑分区数量无限,但总得有个支持的上限,那就支持8个
};

/* ata通道结构 */
struct ide_channel {
char name[8]; // 本ata通道名称
uint16_t port_base; // 本通道的起始端口号
uint8_t irq_no; // 本通道所用的中断号
struct lock lock; // 通道锁
bool expecting_intr; // 表示等待硬盘的中断
struct semaphore disk_done; // 用于阻塞、唤醒驱动程序
struct disk devices[2]; // 一个通道上连接两个硬盘,一主一从
};

void ide_init(void);
extern uint8_t channel_cnt;
extern struct ide_channel channels[];
#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
// /home/mouse/OS_mouse/tool/bochs/mouse/device/ide.c

#include "ide.h"
#include "sync.h"
#include "stdio.h"
#include "stdio-kernel.h"
#include "interrupt.h"
#include "memory.h"
#include "debug.h"
#include "string.h"

/* 定义硬盘各寄存器的端口号 */
#define reg_data(channel) (channel->port_base + 0)
#define reg_error(channel) (channel->port_base + 1)
#define reg_sect_cnt(channel) (channel->port_base + 2)
#define reg_lba_l(channel) (channel->port_base + 3)
#define reg_lba_m(channel) (channel->port_base + 4)
#define reg_lba_h(channel) (channel->port_base + 5)
#define reg_dev(channel) (channel->port_base + 6)
#define reg_status(channel) (channel->port_base + 7)
#define reg_cmd(channel) (reg_status(channel))
#define reg_alt_status(channel) (channel->port_base + 0x206)
#define reg_ctl(channel) reg_alt_status(channel)

/* reg_alt_status寄存器的一些关键位 */
#define BIT_STAT_BSY 0x80 // 硬盘忙
#define BIT_STAT_DRDY 0x40 // 驱动器准备好
#define BIT_STAT_DRQ 0x8 // 数据传输准备好了

/* device寄存器的一些关键位 */
#define BIT_DEV_MBS 0xa0 // 第7位和第5位固定为1
#define BIT_DEV_LBA 0x40
#define BIT_DEV_DEV 0x10

/* 一些硬盘操作的指令 */
#define CMD_IDENTIFY 0xec // identify指令
#define CMD_READ_SECTOR 0x20 // 读扇区指令
#define CMD_WRITE_SECTOR 0x30 // 写扇区指令

/* 定义可读写的最大扇区数,调试用的 */
#define max_lba ((80*1024*1024/512) - 1) // 只支持80MB硬盘

uint8_t channel_cnt; // 按硬盘数计算的通道数
struct ide_channel channels[2]; // 有两个ide通道

/* 硬盘数据结构初始化 */
void ide_init()
{
printk("ide_init start\n");
uint8_t hd_cnt = *((uint8_t*)(0x475)); // 获取硬盘的数量(BIOS中0x475存储)
ASSERT(hd_cnt > 0);
channel_cnt = DIV_ROUND_UP(hd_cnt, 2); // 一个ide通道上有两个硬盘,根据硬盘数量反推有几个ide通道
struct ide_channel* channel;
uint8_t channel_no = 0;

/* 处理每个通道上的硬盘 */
while (channel_no < channel_cnt)
{
channel = &channels[channel_no];
sprintf(channel->name, "ide%d", channel_no);

/* 为每个ide通道初始化端口基址及中断向量 */
switch (channel_no)
{
case 0:
channel->port_base = 0x1f0; // ide0通道的起始端口号是0x1f0
channel->irq_no = 0x20 + 14; // 从片8259a上倒数第二的中断引脚,温盘,也就是ide0通道的的中断向量号
break;
case 1:
channel->port_base = 0x170; // ide1通道的起始端口号是0x170
channel->irq_no = 0x20 + 15; // 从8259A上的最后一个中断引脚,我们用来响应ide1通道上的硬盘中断
break;
}

channel->expecting_intr = false; // 未向硬盘写入指令时不期待硬盘的中断
lock_init(&channel->lock);

/* 初始化为0,目的是向硬盘控制器请求数据后,硬盘驱动sema_down此信号量会阻塞线程,
直到硬盘完成后通过发中断,由中断处理程序将此信号量sema_up,唤醒线程. */
sema_init(&channel->disk_done, 0);
channel_no++; // 下一个channel
}
printk("ide_init done\n");
}

基础部分的数据结构到这里就初始化完了


D.3 实现 thread_yield 和 idle 线程

这里还要实现一些其他的基础部件,比如thread_yield

thread_yield 定义在 thread.c 中,它的功能是主动把 CPU 使用权让出来,它与 thread_block 的区别是thread_yield 执行后任务的状态是 TASK_READY,即让出 CPU 后,它会被加入到就绪队列中,下次还能继续被调度器调度执行,而 thread_block 执行后任务的状态是 TASK_BLOCKED,需要被唤醒后才能加入到就绪队列,

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/thread/thread.c

//略..........
#include "global"

struct task_struct* idle_thread; //idle 线程

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

// 系统空闲时运行的线程
static void idle(void* arg UNUSED)
{
while(1)
{
thread_block(TASK_BLOCKED);
//执行 hlt 时必须要保证目前处在开中断的情况下,下面是让cpu休息的汇编指令
asm volatile ("sti; hlt" : : : "memory");
}
}

// 主动让出 cpu,换其他线程运行(当前任务添加到就绪队列)
void thread_yield(void)
{
struct task_struct* cur = running_thread();
enum intr_status old_status = intr_disable();
ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
list_append(&thread_ready_list, &cur->general_tag);
cur->status = TASK_READY;
schedule();
intr_set_status(old_status);
}

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

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

list_init(&thread_ready_list);
list_init(&thread_all_list);
lock_init(&pid_lock);

//将当前的main函数创建为线程
make_main_thread();

//创建 idle 线程 (当就绪队列没有任务的时候运行)
idle_thread = thread_start("idle", 10, idle, NULL);

put_str("thread_init done");
}

添加的idle线程中有个指令hlt,这个指令的功能让处理器停止执行指令,也就是将处理器挂起(并不是类似“jmp $”那样空兜 CPU,CPU 利用率 100%),使处理器得到休息,CPU 利用率一下子就掉下来了,在那一小段时间 CPU 利用率为 0。处理器已经停止运行,因此并不会再产生内部异常,唯一能唤醒处理器的就是外部中断,当外部发生后,处理器恢复执行后面的指令。处理器需要被唤醒,必须要保证在开中断的情况下执行 hlt,因此内联汇编代码中,先执行 sti,再执行 hlt。顺便说一下,idle_thread 在第一次创建时会被加入到就绪队列,因此会执行一次,然后阻塞

同时,这里用了一个宏,定义在global.h文件中,用来防止编译器警告的(未使用的变量)

1
#define UNUSED __attribute__ ((unused))

D.4 实现简单的休眠

这里还是基础部件,实现简单的休眠

硬盘和 CPU 是相互独立的个体,它们各自并行执行,但由于硬盘是低速设备,其在处理请求时往往消耗很长的时间(不过手册上说最慢的情况也能在 31 秒之内完成),为避免浪费 CPU 资源,在等待硬盘操作的过程中最好把 CPU 主动让出来,让 CPU 去执行其他任务,为实现这种“明智”的行为,我们在 timer.c中定义休眠函数,当然这只是简易版,精度不是很高,能达到目的就可以了

这里的休眠实现就是通过不断的调用thread_yield,将自己添加到就绪列表中,直到达到休眠的时间再继续于运行后面的代码

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
// /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 //控制字寄存器的端口

#define mil_seconds_per_intr (1000 / IRQ0_FREQUENCY) //这里计算出来就是10ms一次中断

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

//以 tick 为单位的 sleep,任何时间形式的 sleep 会转换此 ticks 形式
static void ticks_to_sleep(uint32_t sleep_ticks)
{
uint32_t start_tick = ticks; //开始休眠的ticks

//若间隔的 ticks 数不够便让出 cpu
while (ticks - start_tick < sleep_ticks)
{
thread_yield();
}
}

//以毫秒为单位的 sleep 1 秒= 1000 毫秒
void mtime_sleep(uint32_t m_seconds)
{
uint32_t sleep_ticks = DIV_ROUND_UP(m_seconds, mil_seconds_per_intr);
ASSERT(sleep_ticks > 0);
ticks_to_sleep(sleep_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");
}
1
2
3
4
5
6
7
8
9
10
11
12
// /home/mouse/OS_mouse/tool/bochs/mouse/device/timer.h

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

//定时器PIT8253的初始化
void timer_init();
//以毫秒为单位的 sleep 1 秒= 1000 毫秒
void mtime_sleep(uint32_t m_seconds);

#endif

D.5 完善硬盘驱动程序 & 获取硬盘信息,扫描分区表

下面是驱动的完整部分,因为涉及大量书中的概念等,这里只给出代码和测试结果:

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
// /home/mouse/OS_mouse/tool/bochs/mouse/device/ide.h
#ifndef __DEVICE_IDE_H
#define __DEVICE_IDE_H
#include "stdint.h"
#include "sync.h"
#include "list.h"
#include "bitmap.h"

/* 分区结构 */
struct partition {
uint32_t start_lba; // 起始扇区
uint32_t sec_cnt; // 扇区数
struct disk* my_disk; // 分区所属的硬盘
struct list_elem part_tag; // 用于队列中的标记
char name[8]; // 分区名称
struct super_block* sb; // 本分区的超级块
struct bitmap block_bitmap; // 块位图
struct bitmap inode_bitmap; // i结点位图
struct list open_inodes; // 本分区打开的i结点队列
};

/* 硬盘结构 */
struct disk {
char name[8]; // 本硬盘的名称,如sda等
struct ide_channel* my_channel; // 此块硬盘归属于哪个ide通道
uint8_t dev_no; // 本硬盘是主0还是从1
struct partition prim_parts[4]; // 主分区顶多是4个
struct partition logic_parts[8]; // 逻辑分区数量无限,但总得有个支持的上限,那就支持8个
};

/* ata通道结构 */
struct ide_channel {
char name[8]; // 本ata通道名称, 如ata0,也被叫做ide0. 可以参考bochs配置文件中关于硬盘的配置。
uint16_t port_base; // 本通道的起始端口号
uint8_t irq_no; // 本通道所用的中断号
struct lock lock;
bool expecting_intr; // 向硬盘发完命令后等待来自硬盘的中断
struct semaphore disk_done; // 硬盘处理完成.线程用这个信号量来阻塞自己,由硬盘完成后产生的中断将线程唤醒
struct disk devices[2]; // 一个通道上连接两个硬盘,一主一从
};

void intr_hd_handler(uint8_t irq_no);
void ide_init(void);
extern uint8_t channel_cnt;
extern struct ide_channel channels[];
extern struct list partition_list;
void ide_read(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt);
void ide_write(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt);
#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
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
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
// /home/mouse/OS_mouse/tool/bochs/mouse/device/ide.c

#include "ide.h"
#include "sync.h"
#include "io.h"
#include "stdio.h"
#include "stdio-kernel.h"
#include "interrupt.h"
#include "memory.h"
#include "debug.h"
#include "console.h"
#include "timer.h"
#include "string.h"
#include "list.h"

/* 定义硬盘各寄存器的端口号 */
#define reg_data(channel) (channel->port_base + 0)
#define reg_error(channel) (channel->port_base + 1)
#define reg_sect_cnt(channel) (channel->port_base + 2)
#define reg_lba_l(channel) (channel->port_base + 3)
#define reg_lba_m(channel) (channel->port_base + 4)
#define reg_lba_h(channel) (channel->port_base + 5)
#define reg_dev(channel) (channel->port_base + 6)
#define reg_status(channel) (channel->port_base + 7)
#define reg_cmd(channel) (reg_status(channel))
#define reg_alt_status(channel) (channel->port_base + 0x206)
#define reg_ctl(channel) reg_alt_status(channel)

/* reg_status寄存器的一些关键位 */
#define BIT_STAT_BSY 0x80 // 硬盘忙
#define BIT_STAT_DRDY 0x40 // 驱动器准备好
#define BIT_STAT_DRQ 0x8 // 数据传输准备好了

/* device寄存器的一些关键位 */
#define BIT_DEV_MBS 0xa0 // 第7位和第5位固定为1
#define BIT_DEV_LBA 0x40
#define BIT_DEV_DEV 0x10

/* 一些硬盘操作的指令 */
#define CMD_IDENTIFY 0xec // identify指令
#define CMD_READ_SECTOR 0x20 // 读扇区指令
#define CMD_WRITE_SECTOR 0x30 // 写扇区指令

/* 定义可读写的最大扇区数,调试用的 */
#define max_lba ((80*1024*1024/512) - 1) // 只支持80MB硬盘

uint8_t channel_cnt; // 按硬盘数计算的通道数
struct ide_channel channels[2]; // 有两个ide通道

/* 用于记录总扩展分区的起始lba,初始为0,partition_scan时以此为标记 */
int32_t ext_lba_base = 0;

uint8_t p_no = 0, l_no = 0; // 用来记录硬盘主分区和逻辑分区的下标

struct list partition_list; // 分区队列

/* 构建一个16字节大小的结构体,用来存分区表项 */
struct partition_table_entry {
uint8_t bootable; // 是否可引导
uint8_t start_head; // 起始磁头号
uint8_t start_sec; // 起始扇区号
uint8_t start_chs; // 起始柱面号
uint8_t fs_type; // 分区类型
uint8_t end_head; // 结束磁头号
uint8_t end_sec; // 结束扇区号
uint8_t end_chs; // 结束柱面号
/* 更需要关注的是下面这两项 */
uint32_t start_lba; // 本分区起始扇区的lba地址
uint32_t sec_cnt; // 本分区的扇区数目
} __attribute__ ((packed)); // 保证此结构是16字节大小

/* 引导扇区,mbr或ebr所在的扇区 */
struct boot_sector {
uint8_t other[446]; // 引导代码
struct partition_table_entry partition_table[4]; // 分区表中有4项,共64字节
uint16_t signature; // 启动扇区的结束标志是0x55,0xaa,
} __attribute__ ((packed));

/* 选择读写的硬盘 */
static void select_disk(struct disk* hd) {
uint8_t reg_device = BIT_DEV_MBS | BIT_DEV_LBA;
if (hd->dev_no == 1) { // 若是从盘就置DEV位为1
reg_device |= BIT_DEV_DEV;
}
outb(reg_dev(hd->my_channel), reg_device);
}

/* 向硬盘控制器写入起始扇区地址及要读写的扇区数 */
static void select_sector(struct disk* hd, uint32_t lba, uint8_t sec_cnt) {
ASSERT(lba <= max_lba);
struct ide_channel* channel = hd->my_channel;

/* 写入要读写的扇区数*/
outb(reg_sect_cnt(channel), sec_cnt); // 如果sec_cnt为0,则表示写入256个扇区

/* 写入lba地址(即扇区号) */
outb(reg_lba_l(channel), lba); // lba地址的低8位,不用单独取出低8位.outb函数中的汇编指令outb %b0, %w1会只用al。
outb(reg_lba_m(channel), lba >> 8); // lba地址的8~15位
outb(reg_lba_h(channel), lba >> 16); // lba地址的16~23位

/* 因为lba地址的24~27位要存储在device寄存器的0~3位,
* 无法单独写入这4位,所以在此处把device寄存器再重新写入一次*/
outb(reg_dev(channel), BIT_DEV_MBS | BIT_DEV_LBA | (hd->dev_no == 1 ? BIT_DEV_DEV : 0) | lba >> 24);
}

/* 向通道channel发命令cmd */
static void cmd_out(struct ide_channel* channel, uint8_t cmd) {
/* 只要向硬盘发出了命令便将此标记置为true,硬盘中断处理程序需要根据它来判断 */
channel->expecting_intr = true;
outb(reg_cmd(channel), cmd);
}

/* 硬盘读入sec_cnt个扇区的数据到buf */
static void read_from_sector(struct disk* hd, void* buf, uint8_t sec_cnt) {
uint32_t size_in_byte;
if (sec_cnt == 0) {
/* 因为sec_cnt是8位变量,由主调函数将其赋值时,若为256则会将最高位的1丢掉变为0 */
size_in_byte = 256 * 512;
} else {
size_in_byte = sec_cnt * 512;
}
insw(reg_data(hd->my_channel), buf, size_in_byte / 2);
}

/* 将buf中sec_cnt扇区的数据写入硬盘 */
static void write2sector(struct disk* hd, void* buf, uint8_t sec_cnt) {
uint32_t size_in_byte;
if (sec_cnt == 0) {
/* 因为sec_cnt是8位变量,由主调函数将其赋值时,若为256则会将最高位的1丢掉变为0 */
size_in_byte = 256 * 512;
} else {
size_in_byte = sec_cnt * 512;
}
outsw(reg_data(hd->my_channel), buf, size_in_byte / 2);
}

/* 等待30秒 */
static bool busy_wait(struct disk* hd) {
struct ide_channel* channel = hd->my_channel;
uint16_t time_limit = 30 * 1000; // 可以等待30000毫秒
while (time_limit -= 10 >= 0) {
if (!(inb(reg_status(channel)) & BIT_STAT_BSY)) {
return (inb(reg_status(channel)) & BIT_STAT_DRQ);
} else {
mtime_sleep(10); // 睡眠10毫秒
}
}
return false;
}

/* 从硬盘读取sec_cnt个扇区到buf */
void ide_read(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt) { // 此处的sec_cnt为32位大小
ASSERT(lba <= max_lba);
ASSERT(sec_cnt > 0);
lock_acquire (&hd->my_channel->lock);

/* 1 先选择操作的硬盘 */
select_disk(hd);

uint32_t secs_op; // 每次操作的扇区数
uint32_t secs_done = 0; // 已完成的扇区数
while(secs_done < sec_cnt) {
if ((secs_done + 256) <= sec_cnt) {
secs_op = 256;
} else {
secs_op = sec_cnt - secs_done;
}

/* 2 写入待读入的扇区数和起始扇区号 */
select_sector(hd, lba + secs_done, secs_op);

/* 3 执行的命令写入reg_cmd寄存器 */
cmd_out(hd->my_channel, CMD_READ_SECTOR); // 准备开始读数据

/********************* 阻塞自己的时机 ***********************
在硬盘已经开始工作(开始在内部读数据或写数据)后才能阻塞自己,现在硬盘已经开始忙了,
将自己阻塞,等待硬盘完成读操作后通过中断处理程序唤醒自己*/
sema_down(&hd->my_channel->disk_done);
/*************************************************************/

/* 4 检测硬盘状态是否可读 */
/* 醒来后开始执行下面代码*/
if (!busy_wait(hd)) { // 若失败
char error[64];
sprintf(error, "%s read sector %d failed!!!!!!\n", hd->name, lba);
PANIC(error);
}

/* 5 把数据从硬盘的缓冲区中读出 */
read_from_sector(hd, (void*)((uint32_t)buf + secs_done * 512), secs_op);
secs_done += secs_op;
}
lock_release(&hd->my_channel->lock);
}

/* 将buf中sec_cnt扇区数据写入硬盘 */
void ide_write(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt) {
ASSERT(lba <= max_lba);
ASSERT(sec_cnt > 0);
lock_acquire (&hd->my_channel->lock);

/* 1 先选择操作的硬盘 */
select_disk(hd);

uint32_t secs_op; // 每次操作的扇区数
uint32_t secs_done = 0; // 已完成的扇区数
while(secs_done < sec_cnt) {
if ((secs_done + 256) <= sec_cnt) {
secs_op = 256;
} else {
secs_op = sec_cnt - secs_done;
}

/* 2 写入待写入的扇区数和起始扇区号 */
select_sector(hd, lba + secs_done, secs_op); // 先将待读的块号lba地址和待读入的扇区数写入lba寄存器

/* 3 执行的命令写入reg_cmd寄存器 */
cmd_out(hd->my_channel, CMD_WRITE_SECTOR); // 准备开始写数据

/* 4 检测硬盘状态是否可读 */
if (!busy_wait(hd)) { // 若失败
char error[64];
sprintf(error, "%s write sector %d failed!!!!!!\n", hd->name, lba);
PANIC(error);
}

/* 5 将数据写入硬盘 */
write2sector(hd, (void*)((uint32_t)buf + secs_done * 512), secs_op);

/* 在硬盘响应期间阻塞自己 */
sema_down(&hd->my_channel->disk_done);
secs_done += secs_op;
}
/* 醒来后开始释放锁*/
lock_release(&hd->my_channel->lock);
}

/* 将dst中len个相邻字节交换位置后存入buf */
static void swap_pairs_bytes(const char* dst, char* buf, uint32_t len) {
uint8_t idx;
for (idx = 0; idx < len; idx += 2) {
/* buf中存储dst中两相邻元素交换位置后的字符串*/
buf[idx + 1] = *dst++;
buf[idx] = *dst++;
}
buf[idx] = '\0';
}

/* 获得硬盘参数信息 */
static void identify_disk(struct disk* hd) {
char id_info[512];
select_disk(hd);
cmd_out(hd->my_channel, CMD_IDENTIFY);
/* 向硬盘发送指令后便通过信号量阻塞自己,
* 待硬盘处理完成后,通过中断处理程序将自己唤醒 */
sema_down(&hd->my_channel->disk_done);

/* 醒来后开始执行下面代码*/
if (!busy_wait(hd)) { // 若失败
char error[64];
sprintf(error, "%s identify failed!!!!!!\n", hd->name);
PANIC(error);
}
read_from_sector(hd, id_info, 1);

char buf[64];
uint8_t sn_start = 10 * 2, sn_len = 20, md_start = 27 * 2, md_len = 40;
swap_pairs_bytes(&id_info[sn_start], buf, sn_len);
printk(" disk %s info:\n SN: %s\n", hd->name, buf);
memset(buf, 0, sizeof(buf));
swap_pairs_bytes(&id_info[md_start], buf, md_len);
printk(" MODULE: %s\n", buf);
uint32_t sectors = *(uint32_t*)&id_info[60 * 2];
printk(" SECTORS: %d\n", sectors);
printk(" CAPACITY: %dMB\n", sectors * 512 / 1024 / 1024);
}

/* 扫描硬盘hd中地址为ext_lba的扇区中的所有分区 */
static void partition_scan(struct disk* hd, uint32_t ext_lba) {
struct boot_sector* bs = sys_malloc(sizeof(struct boot_sector));
ide_read(hd, ext_lba, bs, 1);
uint8_t part_idx = 0;
struct partition_table_entry* p = bs->partition_table;

/* 遍历分区表4个分区表项 */
while (part_idx++ < 4) {
if (p->fs_type == 0x5) { // 若为扩展分区
if (ext_lba_base != 0) {
/* 子扩展分区的start_lba是相对于主引导扇区中的总扩展分区地址 */
partition_scan(hd, p->start_lba + ext_lba_base);
} else { // ext_lba_base为0表示是第一次读取引导块,也就是主引导记录所在的扇区
/* 记录下扩展分区的起始lba地址,后面所有的扩展分区地址都相对于此 */
ext_lba_base = p->start_lba;
partition_scan(hd, p->start_lba);
}
} else if (p->fs_type != 0) { // 若是有效的分区类型
if (ext_lba == 0) { // 此时全是主分区
hd->prim_parts[p_no].start_lba = ext_lba + p->start_lba;
hd->prim_parts[p_no].sec_cnt = p->sec_cnt;
hd->prim_parts[p_no].my_disk = hd;
list_append(&partition_list, &hd->prim_parts[p_no].part_tag);
sprintf(hd->prim_parts[p_no].name, "%s%d", hd->name, p_no + 1);
p_no++;
ASSERT(p_no < 4); // 0,1,2,3
} else {
hd->logic_parts[l_no].start_lba = ext_lba + p->start_lba;
hd->logic_parts[l_no].sec_cnt = p->sec_cnt;
hd->logic_parts[l_no].my_disk = hd;
list_append(&partition_list, &hd->logic_parts[l_no].part_tag);
sprintf(hd->logic_parts[l_no].name, "%s%d", hd->name, l_no + 5); // 逻辑分区数字是从5开始,主分区是1~4.
l_no++;
if (l_no >= 8) // 只支持8个逻辑分区,避免数组越界
return;
}
}
p++;
}
sys_free(bs);
}

/* 打印分区信息 */
static bool partition_info(struct list_elem* pelem, int arg UNUSED) {
struct partition* part = elem2entry(struct partition, part_tag, pelem);
printk(" %s start_lba:0x%x, sec_cnt:0x%x\n",part->name, part->start_lba, part->sec_cnt);

/* 在此处return false与函数本身功能无关,
* 只是为了让主调函数list_traversal继续向下遍历元素 */
return false;
}

/* 硬盘中断处理程序 */
void intr_hd_handler(uint8_t irq_no) {
ASSERT(irq_no == 0x2e || irq_no == 0x2f);
uint8_t ch_no = irq_no - 0x2e;
struct ide_channel* channel = &channels[ch_no];
ASSERT(channel->irq_no == irq_no);
/* 不必担心此中断是否对应的是这一次的expecting_intr,
* 每次读写硬盘时会申请锁,从而保证了同步一致性 */
if (channel->expecting_intr) {
channel->expecting_intr = false;
sema_up(&channel->disk_done);

/* 读取状态寄存器使硬盘控制器认为此次的中断已被处理,
* 从而硬盘可以继续执行新的读写 */
inb(reg_status(channel));
}
}

/* 硬盘数据结构初始化 */
void ide_init() {
printk("ide_init start\n");
uint8_t hd_cnt = *((uint8_t*)(0x475)); // 获取硬盘的数量
ASSERT(hd_cnt > 0);
list_init(&partition_list);
channel_cnt = DIV_ROUND_UP(hd_cnt, 2); // 一个ide通道上有两个硬盘,根据硬盘数量反推有几个ide通道
struct ide_channel* channel;
uint8_t channel_no = 0, dev_no = 0;

/* 处理每个通道上的硬盘 */
while (channel_no < channel_cnt) {
channel = &channels[channel_no];
sprintf(channel->name, "ide%d", channel_no);

/* 为每个ide通道初始化端口基址及中断向量 */
switch (channel_no) {
case 0:
channel->port_base = 0x1f0; // ide0通道的起始端口号是0x1f0
channel->irq_no = 0x20 + 14; // 从片8259a上倒数第二的中断引脚,温盘,也就是ide0通道的的中断向量号
break;
case 1:
channel->port_base = 0x170; // ide1通道的起始端口号是0x170
channel->irq_no = 0x20 + 15; // 从8259A上的最后一个中断引脚,我们用来响应ide1通道上的硬盘中断
break;
}

channel->expecting_intr = false; // 未向硬盘写入指令时不期待硬盘的中断
lock_init(&channel->lock);

/* 初始化为0,目的是向硬盘控制器请求数据后,硬盘驱动sema_down此信号量会阻塞线程,
直到硬盘完成后通过发中断,由中断处理程序将此信号量sema_up,唤醒线程. */
sema_init(&channel->disk_done, 0);

register_handler(channel->irq_no, intr_hd_handler);

/* 分别获取两个硬盘的参数及分区信息 */
while (dev_no < 2) {
struct disk* hd = &channel->devices[dev_no];
hd->my_channel = channel;
hd->dev_no = dev_no;
sprintf(hd->name, "sd%c", 'a' + channel_no * 2 + dev_no);
identify_disk(hd); // 获取硬盘参数
if (dev_no != 0) { // 内核本身的裸硬盘(hd60M.img)不处理
partition_scan(hd, 0); // 扫描该硬盘上的分区
}
p_no = 0, l_no = 0;
dev_no++;
}
dev_no = 0; // 将硬盘驱动器号置0,为下一个channel的两个硬盘初始化。
channel_no++; // 下一个channel
}

printk("\n all partition info\n");
/* 打印所有分区信息 */
list_traversal(&partition_list, partition_info, (int)NULL);
printk("ide_init done\n");
}

最后将ide_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
// /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"
#include "console.h"
#include "tss.h"
#include "syscall-init.h"
#include "keyboard.h"
#include "stdio-kernel.h"
#include "ide.h"

//初始化所有模块
void init_all()
{
put_str("init_all start\n");
idt_init(); //中断
mem_init(); //内存管理初始化
thread_init(); //线程初始化
timer_init(); //定时器设备 PIT
keyboard_init(); //键盘
tss_init(); //初始化tss
syscall_init(); //初始化系统调用
console_init(); //初始化控制台,最好放在打开中断前
intr_enable(); // 后面的ide_init需要打开中断
ide_init(); // 初始化硬盘
printk("init_all done\n"); //不用put_str是因为当时测试printk用的,懒得改
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ide_init start
disk sda info:
SN:BXHDOO011
MODULE: Generic 1234
SECTORS: 121968
CAPACITY: 59MB
disk sdb info:
SN: BXHD00012
MODULE: Generic 1234
SECTORS: 163296
CAPACITY: 79MB
all partition info
sdb1 start_lba:Ox3F, sec_cnt:Ox7DC1
sdb5 start_Iba:Ox7E3F, sec_cnt:Ox46A1
sdb6 start_Iba:OxC51F, sec_cnt:0x6231
sdb7 start_Iba:0x1278F, seC_cnt:Ox3AD1
sdb8 start_Iba:Ox1629F, sec_cnt:Ox75E1
sdb9 start_Iba:Ox1D8BF, sec_cnt:OxA521
ide_init done
init_all done

到此结束,这里涉及大量的硬盘相关概念知识在书中,大家可以自己阅读


E 文件系统前的小结

ok呀,也是只剩下两章就要结束这本书了,学校不让我去实习我就只能窝在这里学操作系统了,太难受了……………..

参考文献

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

留言

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

  1. 在下方评论区留言
  2. 邮箱留言
  • Title: 真象还原 --用户进程/完善内核/硬盘驱动 study(4)
  • Author: H_Haozi
  • Created at : 2025-11-03 20:35:11
  • Updated at : 2025-11-10 14:59:23
  • Link: https://redefine.ohevan.com/2025/11/03/os_elephant_four/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments