8.1 makefile 简介
这部分可参考阮一峰的讲解:https://www.ruanyifeng.com/blog/2015/02/make.html
8.1.1 makefile 是什么
makefile 是 Linux 下编译大型程序的工具,是 make 程序的搭档,两者的主要功能就是发现某个文件更新后,只编译该文件和受该文件影响的相关文件,从而提高了编译效率。
make 和 makefile 并不是用来编译程序的,它只负责找出哪些文件有变化, 并 且根据依赖关系找出受影响的文件,然后执行事先在 makefile 中定义好的命令规则。因为 make 就是在 shell 下执行的,所以在 makefile 中,位于命令规则里的那些命令, 都是 shell 命令。
8.1.2 makefile 基本语法
makefile 基本语法包括三部分,这三部分加在一起称为一组规则:
- 目标文件是指此规则中想要生成的文件,可以是.o 结尾的目标文件,也可以是可执行文件或伪目标。
- 依赖文件是指要生成此规则中的目标文件,需要哪些文件。通常依赖文件不是 1 个,所以此处是个依赖文件的列表。
- 命令是指此规则中要执行的动作,这些动作是指各种 shell 命令。命令可以有多个,但一个命令要单独占一行,行首必须以 Tab 开头。
Linux 中文件分为属性和数据两部分,每个文件有三种时间:
- atime,即 access time,表示访问文件数据部分时间,每次读取文件数据部分时就会更新 atime。
- ctime,即 change time,表示文件属性或数据的改变时间。
- mtime,即 modify time,表示文件数据部分的修改时间。
make 程序分别获取依赖文件和目标文件的 mtime,对比依赖文件是否比目标文件的 mtime 新,从而得知是否要执行规则中的命令。
举例说明:
1 2
| file1:file2 @echo "makefile test ok"
|
文件的含义是:若 file2 比 file1 新,则输出“makefile test ok”。“@”是关闭打印命令本身。
makefile 的文件名不固定,可以在执行 make 时用-f 参数执行。如果未用-f 指定,默认 make 会按照GNUmakefile
、makefile
、Makefile
的顺序找 makefile 文件。
8.1.3 跳到目标处执行
如果 makefile 有多个目标,可以用make target_name
来执行,如果不指定目标,则默认执行第一个目标。
8.1.4 伪目标
当规则中不存在依赖文件时,这个目标文件名就是伪目标。
可以使用关键字.PHONY
来明确声明伪目标,例如:
1 2 3
| .PHONY:clean clean: rm ./build/*.o
|
8.1.5 make: 递归式推导目标
举例说明:
1 2 3 4 5 6 7 8
| test1.o:test1.c gcc -c -o test1.o test1.c test2.o:test2.c gcc -c -o test2.o test2.c test.bin:test1.o test2.o gcc -o test.bin test1.o test2.o all:test.bin @echo "compile done"
|
执行make all
时,make 会递归执行 all,依赖文件不存在或不是最新就去找该依赖文件名命名的规则执行,执行完后再找下一个依赖文件。故最终执行顺序是:test1.o、test2.o、test.bin、all。
8.1.6 自定义变量与系统变量
自定义变量仅支持字符串类型,即使是数字也被当做字符串来处理,变量引用格式:$(变量名)
,举例说明:
1 2 3 4 5 6 7
| test0.o:test0.c gcc -c -o test0.o test0.c test1.o:test1.c gcc -c -o test1.o test1.c objfiles = test0.c test1.c all:$(objfiles) @echo "compile done"
|
还有一些系统级的变量:
在命令相关的系统变量是有默认值的,一般参数相关的变量没有默认值。
8.1.7 隐含规则
- 行尾添加反斜杠字符’',下一行内容会被认为是同一行;
- 可以用’#'进行单行注释;
- 语言程序的隐含规则:
- C 程序:“x.o"的生成依赖于"x.c”,生成 x.o 的命令为:
$(CC) -c $(CPPFLAGS) $(CFLAGS)
- C++程序:“x.o"的生成依赖于"x.cc”,生成 x.o 的命令为:
$(CXX) -c $(CPPFGLAGS) $(CFLAGS)
- Pascal 程序:“x.o"的生成依赖于"x.p”,生成 x.o 的命令为:
$(PC) -c $(PFLAGS)
8.1.8 自动化变量
$@
,表示规则中的目标文件名集合。
$<
,表示规则中依赖文件中的第 1 个文件。
$^
,表示规则中所有依赖文件的集合。
$?
,表示规则中所有比目标文件 mtime 更新的依赖文件集合。
1 2 3 4 5 6 7 8
| test1.o:test1.c gcc -c -o test1.o test1.c test2.o:test2.c gcc -c -o test2.o test2.c test.bin:test1.o test2.o gcc -o $@ $^ all:test.bin @echo "compile done"
|
8.1.9 模式规则
%用来匹配任意多个非空字符,如果是匹配文件,默认在当前路径下匹配。
1 2 3 4 5 6 7
| %.o:%.c gcc -c -o $@ $^ objfiles = test1.o test2.o test.bin:$(objfiles) gcc -o $@ $^ all:test.bin @echo "compile done"
|
8.2 实现 assert 断言
8.2.1 实现开、关中断的函数
在相应文件添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| #define EFLAGS_IF 0x00000200 #define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl %0" : "=g" (EFLAG_VAR))
enum intr_status intr_enable() { if (INTR_ON == intr_get_status()) { return INTR_ON; } else { asm volatile("sti"); return INTR_OFF; } }
enum intr_status intr_disable() { if (INTR_ON == intr_get_status()) { asm volatile("cli" : : : "memory"); return INTR_ON; } else { return INTR_OFF; } }
enum intr_status intr_set_status(enum intr_status status) { return status & INTR_ON ? intr_enable() : intr_disable(); }
enum intr_status intr_get_status() { uint32_t eflags = 0; GET_EFLAGS(eflags); return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF; }
|
1 2 3 4 5 6 7 8 9 10 11 12
|
enum intr_status { INTR_OFF, INTR_ON };
enum intr_status intr_get_status(void); enum intr_status intr_set_status (enum intr_status); enum intr_status intr_enable (void); enum intr_status intr_disable (void);
|
8.2.2 实现 ASSERT
ASSERT 的原理是判断传给 ASSERT 的表达式是否成立,若表达式成立则什么都不做,否则打印出错信息并停止执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #ifndef __KERNEL_DEBUG_H #define __KERNEL_DEBUG_H
void panic_spin(char *filename, int line, const char *func, const char *condition);
#define PANIC(...) panic_spin (__FILE__, __LINE__, __func__, __VA_ARGS__)
#ifdef NDEBUG #define ASSERT(CONDITION) ((void)0) #else #define ASSERT(CONDITION) \ if (CONDITION) {} else { \ \ PANIC(#CONDITION); \ } #endif #endif
|
第 10 行中的宏:
__FILE__
表示被编译的文件名;
__LINE__
表示编译文件中的行号;
__func__
表示被编译的函数名;
__VA_ARGS__
是可变参数...
的表示符,它代表所有与省略号相对应的参数。
第 19 行的#CONDITION
,其中字符#
的作用是让预处理器把 CONDITION 转换成字符串常量,比如 CONDITION 为var!=0
,#CONDITION 则为字符串"var!=0"
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include "debug.h" #include "print.h" #include "interrupt.h"
void panic_spin(char* filename, int line, const char* func, const char* condition) { intr_disable(); put_str("\n\n\n!!!!! error !!!!!\n"); put_str("filename:");put_str(filename);put_str("\n"); put_str("line:0x");put_int(line);put_str("\n"); put_str("function:");put_str((char*)func);put_str("\n"); put_str("condition:");put_str((char*)condition);put_str("\n"); while(1); }
|
然后在 main.c 中测试一下:
1 2 3 4 5 6 7 8 9 10 11
| #include "print.h" #include "init.h" #include "debug.h"
int main(void) { put_str("I am kernel\n"); init_all(); ASSERT(1 == 2); while (1); return 0; }
|
上次把中断打开是为了演示中断,现在暂时给关了,等时机成熟了再打开。
8.2.3 通过 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
| BUILD_DIR = ./build DISK_IMG = hd60M.img ENTRY_POINT = 0xc0001500 AS = nasm CC = gcc LD = ld LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ ASFLAGS = -f elf ASBINLIB = -I boot/include/ CFLAGS = -m32 -Wall $(LIB) -c -fno-builtin -W -Wstrict-prototypes \ -Wmissing-prototypes -fno-stack-protector LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \ $(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \ $(BUILD_DIR)/debug.o
$(BUILD_DIR)/mbr.bin: boot/mbr.S $(AS) $(ASBINLIB) $< -o $@
$(BUILD_DIR)/loader.bin: boot/loader.S $(AS) $(ASBINLIB) $< -o $@
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \ lib/stdint.h kernel/init.h $(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \ lib/stdint.h kernel/interrupt.h device/timer.h $(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \ lib/stdint.h kernel/global.h lib/kernel/io.h lib/kernel/print.h $(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/stdint.h\ lib/kernel/io.h lib/kernel/print.h $(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \ lib/kernel/print.h lib/stdint.h kernel/interrupt.h $(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/kernel.o: kernel/kernel.S $(AS) $(ASFLAGS) $< -o $@ $(BUILD_DIR)/print.o: lib/kernel/print.S $(AS) $(ASFLAGS) $< -o $@
$(BUILD_DIR)/kernel.bin: $(OBJS) $(LD) $(LDFLAGS) $^ -o $@
.PHONY : mk_dir mk_img mbr2hd loader2hd kernel2hd clean all
mk_dir: if [ ! -d $(BUILD_DIR) ];then mkdir $(BUILD_DIR);fi
mk_img: if [ ! -e $(DISK_IMG) ];then /usr/bin/bximage -hd -mode="flat" -size=60 -q $(DISK_IMG);fi
mbr2hd: $(AS) $(ASBINLIB) -o $(BUILD_DIR)/mbr.bin boot/mbr.S dd if=$(BUILD_DIR)/mbr.bin of=$(DISK_IMG) bs=512 count=1 conv=notrunc
loader2hd: $(AS) $(ASBINLIB) -o $(BUILD_DIR)/loader.bin boot/loader.S dd if=$(BUILD_DIR)/loader.bin of=$(DISK_IMG) bs=512 count=4 seek=2 conv=notrunc kernel2hd: dd if=$(BUILD_DIR)/kernel.bin \ of=$(DISK_IMG) \ bs=512 count=200 seek=9 conv=notrunc
clean: cd $(BUILD_DIR) && rm -f ./*
build: $(BUILD_DIR)/kernel.bin
all: mk_dir build kernel2hd
|
执行make all
:
8.3 实现字符串操作函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #ifndef __LIB_STRING_H #define __LIB_STRING_H
#include "stdint.h" #define NULL 0 void memset(void* dst_, uint8_t value, uint32_t size); void memcpy(void* dst_, const void* src_, uint32_t size); int memcmp(const void* a_, const void* b_, uint32_t size); char* strcpy(char* dst_, const char* src_); uint32_t strlen(const char* str); int8_t strcmp(const char* a, const char* b); char* strchr(const char* str, const uint8_t ch); char* strrchr(const char* str, const uint8_t ch); char* strcat(char* dst_, const char* src_); uint32_t strchrs(const char* str, uint8_t ch); #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
| #include "string.h" #include "global.h" #include "debug.h"
void memset(void* dst_, uint8_t value, uint32_t size) { ASSERT(dst_ != NULL); uint8_t* dst = (uint8_t*) dst_; while (size-- > 0) *dst++ = value; }
void memcpy(void* dst_, const void* src_, uint32_t size) { ASSERT(dst_ != NULL && src_ != NULL); uint8_t* dst = dst_; const uint8_t* src = src_; while (size-- > 0) *dst++ = *src++; }
int memcmp(const void* a_, const void* b_, uint32_t size) { const char* a = a_; const char* b = b_; ASSERT(a != NULL || b != NULL); while (size-- > 0) { if (*a != *b) { return *a > *b ? 1 : -1; } a++; b++; } return 0; }
char* strcpy(char* dst_, const char* src_) { ASSERT(dst_ != NULL && src_ != NULL); char* r = dst_; while ((*dst_++ = *src_++)); return r; }
uint32_t strlen(const char* str) { ASSERT(str != NULL); const char* p = str; while (*p++); return (p - str - 1); }
int8_t strcmp(const char* a, const char* b) { ASSERT(a != NULL && b != NULL); while (*a != 0 && *a == *b) { a++; b++; }
return *a < *b ? -1 : *a > *b; }
char* strchr(const char* str, const uint8_t ch) { ASSERT(str != NULL); while (*str != 0) { if (*str == ch) { return (char*) str; } str++; } return NULL; }
char* strrchr(const char* str, const uint8_t ch) { ASSERT(str != NULL); const char* last_char = NULL; while (*str != 0) { if (*str == ch) { last_char = str; } str++; } return (char*) last_char; }
char* strcat(char* dst_, const char* src_) { ASSERT(dst_ != NULL && src_ != NULL); char* str = dst_; while (*str++); --str; while ((*str++ = *src_++)); return dst_; }
uint32_t strchrs(const char* str, uint8_t ch) { ASSERT(str != NULL); uint32_t ch_cnt = 0; const char* p = str; while (*p != 0) { if (*p == ch) { ch_cnt++; } p++; } return ch_cnt; }
|
8.4 位图 bitmap 及其函数实现
8.4.1 位图简介
位图就是用字节中的 1 位来映射其他单位大小的资源,按位与资源之间是一对一的对应关系。位图相当于一组资源的映射,故位图主要用于管理容量较大的资源。
8.4.2 位图的定义与实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #ifndef __LIB_KERNEL_BITMAP_H #define __LIB_KERNEL_BITMAP_H #include "global.h" #define BITMAP_MASK 1 struct bitmap { uint32_t btmp_bytes_len;
uint8_t* bits; };
void bitmap_init(struct bitmap* btmp); int bitmap_scan_test(struct bitmap* btmp, uint32_t bit_idx); int bitmap_scan(struct bitmap* btmp, uint32_t cnt); void bitmap_set(struct bitmap* btmp, uint32_t bit_idx, int8_t value); #endif
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| #include "bitmap.h" #include "stdint.h" #include "string.h" #include "print.h" #include "interrupt.h" #include "debug.h"
void bitmap_init(struct bitmap* btmp) { memset(btmp->bits, 0, btmp->btmp_bytes_len); }
int bitmap_scan_test(struct bitmap* btmp, uint32_t bit_idx) { uint32_t byte_idx = bit_idx / 8; uint32_t bit_odd = bit_idx % 8; return (btmp->bits[byte_idx] & (BITMAP_MASK << bit_odd)); }
int bitmap_scan(struct bitmap* btmp, uint32_t cnt) { uint32_t idx_byte = 0;
while (( 0xff == btmp->bits[idx_byte]) && (idx_byte < btmp->btmp_bytes_len)) {
idx_byte++; }
ASSERT(idx_byte < btmp->btmp_bytes_len); if (idx_byte == btmp->btmp_bytes_len) { return -1; }
int idx_bit = 0; while ((uint8_t)(BITMAP_MASK << idx_bit) & btmp->bits[idx_byte]) { idx_bit++; }
int bit_idx_start = idx_byte * 8 + idx_bit; if (cnt == 1) { return bit_idx_start; }
uint32_t bit_left = (btmp->btmp_bytes_len * 8 - bit_idx_start); uint32_t next_bit = bit_idx_start + 1; uint32_t count = 1;
bit_idx_start = -1; while (bit_left-- > 0) { if (!(bitmap_scan_test(btmp, next_bit))) { count++; } else { count = 0; } if (count == cnt) { bit_idx_start = next_bit - cnt + 1; break; } next_bit++; } return bit_idx_start; }
void bitmap_set(struct bitmap* btmp, uint32_t bit_idx, int8_t value) { ASSERT((value == 0) || (value == 1)); uint32_t byte_idx = bit_idx / 8; uint32_t bit_odd = bit_idx % 8;
if (value) { btmp->bits[byte_idx] |= (BITMAP_MASK << bit_odd); } else { btmp->bits[byte_idx] &= ~(BITMAP_MASK << bit_odd); } }
|
置 1 用或操作,置 0 用与操作。
8.5 内存管理系统
本节将实现内存管理系统,并将实现 malloc 和 free 函数。
8.5.1 内存池规划
内存地址池的概念是将可用的内存地址集中放到一个“池子”中,需要的时候直接从里面取出, 完后再放回去。由于在分页机制下有了虚拟地址和物理地址,为了有效地管理它们,我们需要创建虚拟内存地址池和物理内存地址池。
物理内存地址池分为两部分:
- 内核物理内存池,只用来运行内核
- 用户物理内存池,只用来运行用户进程
为了方便实现,这里把这两个内存池的大小设为一致,各占一半的物理内存。
对于虚拟内存地址池:
- 内核进程从内核虚拟地址池中分配地址,再从内核物理内存池中分配物理内存,然后在内核自己的页表中将两个地址建立好映射关系。
- 用户进程从用户进程虚拟地址池中分配空闲虚拟地址,然后再从用户物理内存池中分配空闲的物理内存,然后在该用户进程自己的页表将这两种地址建立好映射关系。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #ifndef __KERNEL_MEMORY_H #define __KERNEL_MEMORY_H
#include "stdint.h" #include "bitmap.h"
struct virtual_addr { struct bitmap vaddr_bitmap; uint32_t vaddr_start; };
extern struct pool kernel_pool, user_pool; void mem_init(void); #endif
|
virtualaddr 是虚拟地址池,用于虚拟地址管理。为了演示分页机制是如何将这两种独立不相关的地址关联到一起的,特意加了该结构体用于管理虚拟地址。
struct virtual addr 包含两个成员,一个是 vaddr_bitmap,用来以页位单位管理虚拟地址的分配情况。虽然多个进程可以有相同的虚拟地址,但其实是因为这些虚拟地址对应的物理地址是不同的,在同一进程内的虚拟地址必然是唯一的,这是由链接器为其分配,由链接器负责虚拟地址的唯一性。另一个是 vaddr_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 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
| #include "memory.h" #include "stdint.h" #include "print.h"
#define PG_SIZE 4096
#define MEM_BITMAP_BASE 0xc009a000 #define K_HEAP_START 0xc0100000
struct pool { struct bitmap pool_bitmap; uint32_t phy_addr_start; uint32_t pool_size; };
struct pool kernel_pool, user_pool; struct virtual_addr kernel_vaddr;
static void mem_pool_init(uint32_t all_mem) { put_str(" mem_pool_init start\n"); uint32_t page_table_size = PG_SIZE * 256; uint32_t used_mem = page_table_size + 0x100000; uint32_t free_mem = all_mem - used_mem; uint16_t all_free_pages = free_mem / PG_SIZE; uint16_t kernel_free_pages = all_free_pages / 2; uint16_t user_free_pages = all_free_pages - kernel_free_pages;
uint32_t kbm_length = kernel_free_pages / 8; uint32_t ubm_length = user_free_pages / 8;
uint32_t kp_start = used_mem; uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;
kernel_pool.phy_addr_start = kp_start; user_pool.phy_addr_start = up_start;
kernel_pool.pool_size = kernel_free_pages * PG_SIZE; user_pool.pool_size = user_free_pages * PG_SIZE;
kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length; user_pool.pool_bitmap.btmp_bytes_len = ubm_length;
kernel_pool.pool_bitmap.bits = (void*) MEM_BITMAP_BASE;
user_pool.pool_bitmap.bits = (void*) (MEM_BITMAP_BASE + kbm_length); put_str(" kernel_pool_bitmap_start:"); put_int((int) kernel_pool.pool_bitmap.bits); put_str("\n"); put_str(" kernel_pool_bitmap_end:"); put_int((int) kernel_pool.pool_bitmap.bits + kernel_pool.pool_bitmap.btmp_bytes_len); put_str("\n"); put_str(" kernel_pool_phy_addr_start:"); put_int(kernel_pool.phy_addr_start); put_str("\n"); put_str(" kernel_pool_phy_addr_end:"); put_int(kernel_pool.phy_addr_start + kernel_pool.pool_size); put_str("\n"); put_str(" user_pool_bitmap_start:");
put_int((int) user_pool.pool_bitmap.bits); put_str("\n"); put_str(" user_pool_bitmap_end:"); put_int((int) user_pool.pool_bitmap.bits + user_pool.pool_bitmap.btmp_bytes_len); put_str("\n"); put_str(" user_pool_phy_addr_start:"); put_int(user_pool.phy_addr_start); put_str("\n"); put_str(" user_pool_phy_addr_end:"); put_int(user_pool.phy_addr_start + user_pool.pool_size); put_str("\n");
bitmap_init(&kernel_pool.pool_bitmap); bitmap_init(&user_pool.pool_bitmap);
kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length;
kernel_vaddr.vaddr_bitmap.bits = (void*) (MEM_BITMAP_BASE + kbm_length + ubm_length); kernel_vaddr.vaddr_start = K_HEAP_START; put_str(" kernel_vaddr.vaddr_bitmap.start:"); put_int((int) kernel_vaddr.vaddr_bitmap.bits); put_str("\n"); put_str(" kernel_vaddr.vaddr_bitmap.end:"); put_int((int) kernel_vaddr.vaddr_bitmap.bits + kernel_vaddr.vaddr_bitmap.btmp_bytes_len); put_str("\n");
bitmap_init(&kernel_vaddr.vaddr_bitmap); put_str(" mem_pool_init done\n"); }
void mem_init() { put_str("mem_init start\n"); uint32_t mem_bytes_total = (*(uint32_t * )(0xb00)); mem_pool_init(mem_bytes_total); put_str("mem_init done\n"); }
|
MEM_BITMAP_BASE
用于表示内存位图的基址,其值为0xc009a000
。由于 PCB 占用 4KB,内核主线程栈顶0xc009f000
,故 PCB 地址为0xc009e00
,这里打算支持 4 页位图的内存,故再减去 0x4000,即为所得。位图 1bit 位管理 4KB 内存,一页 4KB 的位图可以管理 128MB 的内存,即最大可管理 512MB 的物理内存。
K_HEAP_START
是堆的起始地址,0xc0000000
是内核从虚拟地址 3G 起,0x100000
意指跨过低端 1M 内存,使虚拟地址在逻辑上连续,故此值为0xc0100000
。
- 函数
mem_pool_init
是根据内存容量大小初始化物理内存池的结构。
- 变量
page_table_size
是指目前分页机制已经占用了多少内存。这里包括一页页目录表+255 个内核空间页表(其中包括第 0 页表项和第 768 页表项指向的同一个页表)。
- 为了简化位图处理,用 kernel_free_pages 除以 8 获得位图的长度,余数不做处理,故内核内存池和用户内存池各会丢 1-7 页的内存。
- 在函数
mem_init
中,第 123 行,使用(*(uint32_t * )(0xb00))
获取内存容量,是因为在 loader 中我们将获取到的内存容量保存在total_mem_bytes
变量中,其物理地址是 0xb00
。
要注意的是,位图是全局数据结构,全局或静态的数组需要在编译时知道其长度,而位图的长度取决于具体要管理的内存也数量,因此是无法预计的。所以这里对位图的初始化改为指定一块内存来生成位图,这样就不需要固定长度了。
在 makefile 中添加 bitmap 和 memory 的编译:
1 2 3 4 5 6 7 8 9 10 11 12 13
| OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \ $(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \ $(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o $(BUILD_DIR)/bitmap.o
$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h \ kernel/global.h lib/stdint.h lib/string.h lib/stdint.h \ lib/kernel/print.h kernel/interrupt.h kernel/debug.h $(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h lib/stdint.h lib/kernel/bitmap.h \ kernel/global.h kernel/global.h kernel/debug.h lib/kernel/print.h \ lib/kernel/io.h kernel/interrupt.h lib/string.h lib/stdint.h $(CC) $(CFLAGS) $< -o $@
|
运行结果:
已知物理机 32MB 内存,由图所示,内核物理内存池可分配 15MB,用户物理内存池可分配 15MB,内核物理内存池位图、用户物理内存池位图以及内核虚拟地址位图各占用 480 字节。
8.5.2 内存管理系统第一步,分配页内存
在上一小节的基础上,对 memory.h 和 memory.c 进行改进:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| enum pool_flags { PF_KERNEL = 1, PF_USER = 2 };
#define PG_P_1 1 #define PG_P_0 0 #define PG_RW_R 0 #define PG_RW_W 2 #define PG_US_S 0 #define PG_US_U 4
void* get_kernel_pages(uint32_t pg_cnt); void* malloc_page(enum pool_flags pf, uint32_t pg_cnt); void malloc_init(void); uint32_t* pte_ptr(uint32_t vaddr); uint32_t* pde_ptr(uint32_t vaddr);
|
C 语言中申请内存时,由于我们是用户程序,操作系统直接会在用户内存池中分配内存,但这对应对应到内核中具体的操作时,必须要“显式”指定在哪个内存池中申请,故在 memory.h 中新增了枚举结构 pool_flags 来区分这两个内存池。
在内存管理中必不可少要修改页表,故这里又定义了一些页表项或页目录项的属性。
要注意的是,页表的作用是将虚拟地址转换成物理地址,其转换锅中涉及访问的页目录表、页目录项以及页表项都是通过真是物理地址访问的,若用虚拟地址访问的话会陷入转换的死循环中。
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 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
| #include "bitmap.h" #include "global.h" #include "debug.h" #include "string.h"
#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22) #define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)
static void* vaddr_get(enum pool_flags pf, uint32_t pg_cnt) { int vaddr_start = 0, bit_idx_start = -1; uint32_t cnt = 0; if (pf == PF_KERNEL) { bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt); if (bit_idx_start == -1) { return NULL; } while(cnt < pg_cnt) { bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1); } vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE; } else { } return (void*)vaddr_start; }
uint32_t* pte_ptr(uint32_t vaddr) {
uint32_t* pte = (uint32_t*)(0xffc00000 + \ ((vaddr & 0xffc00000) >> 10) + \ PTE_IDX(vaddr) * 4); return pte; }
uint32_t* pde_ptr(uint32_t vaddr) { uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr) * 4); return pde; }
static void* palloc(struct pool* m_pool) { int bit_idx = bitmap_scan(&m_pool->pool_bitmap, 1); if (bit_idx == -1 ) { return NULL; } bitmap_set(&m_pool->pool_bitmap, bit_idx, 1); uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start); return (void*)page_phyaddr; }
static void page_table_add(void* _vaddr, void* _page_phyaddr) { uint32_t vaddr = (uint32_t)_vaddr, page_phyaddr = (uint32_t)_page_phyaddr; uint32_t* pde = pde_ptr(vaddr); uint32_t* pte = pte_ptr(vaddr);
if (*pde & 0x00000001) { ASSERT(!(*pte & 0x00000001));
if (!(*pte & 0x00000001)) { *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); } else { PANIC("pte repeat"); *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); } } else { uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);
*pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
memset((void*)((int)pte & 0xfffff000), 0, PG_SIZE);
ASSERT(!(*pte & 0x00000001)); *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); } }
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt) { ASSERT(pg_cnt > 0 && pg_cnt < 3840);
void* vaddr_start = vaddr_get(pf, pg_cnt); if (vaddr_start == NULL) { return NULL; }
uint32_t vaddr = (uint32_t)vaddr_start, cnt = pg_cnt; struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
while (cnt-- > 0) { void* page_phyaddr = palloc(mem_pool); if (page_phyaddr == NULL) { return NULL; } page_table_add((void*)vaddr, page_phyaddr); vaddr += PG_SIZE; } return vaddr_start; }
void* get_kernel_pages(uint32_t pg_cnt) { void* vaddr = malloc_page(PF_KERNEL, pg_cnt); if (vaddr != NULL) { memset(vaddr, 0, pg_cnt * PG_SIZE); } return vaddr; }
|
需要说明的是,函数pte_ptr
和pde_ptr
都是将虚拟地址赋值给指针,从而触发 CPU 进行寻址操作,从而找到实际 pte 或 pde 的地址。
对于函数pte_ptr
,pte 指针的值存储在页目录表的指定页目录项中,故重新构造虚拟地址:
- 高 10 位用于定位页目录项。页目录地址保存在最后一个页目录项中,也就是页目录表的第 1023 个目录项地址
0xffc00000
。
- 中间 10 位用于定位页表项。存储 pte 的页表项其实就是页目录项,故需要用到 vaddr 的高 10 位,执行
(vaddr & 0xffc00000) >> 10
。
- 低 12 位用于定位 pte 地址。在第 2 步中我们定位到存储该 pte 的页表地址,该步需要构造寻址 pte 的页表索引。页表索引即为虚拟地址的中间 10 位,故可直接使用
PTE_IDX(vaddr)
来获得页表索引,但内存地址低 12 位 CPU 不会自动乘 4,故这里需要手动乘 4 从而得到地址。
函数pde_ptr
同理。
注意:直接将低 12 位置 0 取得的是 pte 的值,也就是页表的地址,不是 pte 的地址。pde 同理。
函数palloc
表示物理内存申请,vaddr_get
表示虚拟内存申请,page_table_add
表示添加虚拟地址和物理地址映射,malloc_page
就按照上述顺序执行操作。
函数page_table_add
不仅完成虚拟地址到物理地址的映射,同时还创建了页目录项、页表、页表项,这里使用页表项的P
位属性来判断该页是否在内存中存在。
函数malloc_page
中考虑到虚拟地址连续但物理地址可能不连续,故一次性申请完虚拟页之后循环申请物理页,每次申请一页,然后再将其关联。
最后进行代码测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include "print.h" #include "init.h" #include "debug.h"
int main(void) { put_str("I am kernel\n"); init_all();
void* addr = get_kernel_pages(3); put_str("\n get_kernel_page start vaddr is "); put_int((uint32_t)addr); put_str("\n");
while (1); return 0; }
|
编译后执行:
使用page 0xc0100000
命令可以看到虚拟内存映射情况:
虚拟地址范围为0xc0100000
-0xc0102fff
,其所映射的物理地址范围为0x200000
-0x202fff
。
本章结束。
本章残留问题:未实现内存回收,将在第 12 章时完善。