本篇对应书籍第八章的内容
本篇内容介绍了使用makefile进行编译文件,位图在内存管理中的使用,已经内存管理系统中的内存池初始化和分配页内存
本篇难点:
- 理解位图的使用,理解内存池,理解获取pde和pte指针
本篇坑点:
- 之前页表章节写的代码有问题,导致这里排错排了半天找不到原因(已解决)
makefile
到目前为止,每次编译需要用 4 次 gcc,两次 nasm,和 ld 链接,操作十分麻烦,通过makefile可以简化这一块的操作。
makefile 是 Linux 下编译大型程序用的工具,Linux 使用 make 命令配合 makefile 使用;
make 和 makefile 并不是用来编译程序的,它只负责找出哪些文件有变化,并且根据依赖关系找出受影响的文件, 然后执行事先在makefile中定义好的命令规则。make 是在 shell 下执行的,所以 makefile 里的命令规则里的命令,都是 shell 命令。
而实际上,makefile 通常用来编译程序。
总的来说就是:依赖关系是定义在文件makefile中, make程序通过解析makefile文件, 自动找出变更的文件以及依赖此变更文件的相关文件, 然后对所有受影响的相关文件执行事先定义好的命令规则。
makefile 基本语法
目标文件:依赖文件
命令
- 目标文件:想要生成的文件
- 依赖文件:生成目标文件所需要的依赖文件
- 命令:依赖文件内容有变更时(mtime),执行的shell命令,每条命令都要单独在一行
这样的三部分在一起称为一组规则
使用举例:
main.o:main.c
@echo "hello world"
#这是注释
如果 main.c 有改变,就打印helloworld,@的作用就是在输出中不显示命令本身
make 命令默认依次寻找当前目录下的GNUmakefile
,makefile
,Makefile
文件,也可以用参数-f
指定 makefile 文件
如果 makefile 里有多个目标,如果只想执行其中一组命令,那就将目标名称作为参数来使用:
make main
makefile 伪目标
有时候,希望 makefile 可以不管 mtime,总是能执行一些规则,那就可以用 makefile 中的伪目标来实现
当规则中不存在依赖文件时,这个目标文件名就是伪目标。
为了防止伪目标和真实目标文件同名而失效,可以显式使用关键字.PHONY
来修饰伪目标,格式为:.PHONY:伪目标名称
makefile 中的伪目标名称虽然可以自己定,但是约定俗成这些名称做这些事情:
伪目标名称 | 功能描述 |
---|---|
all | 通常用来完成所有模块的编译工作 |
clean | 通常用于清空编译完成的所有目标文件,一般rm命令实现 |
dist | 通常用于将打包文件后的tar再压缩成gz文件 |
Install | 通常将编译好的程序复制到安装目录下,此目录是在执行configure脚本 |
printf | 通常用于打印已发生改变的文件 |
tar | 通常用于将文件打包成tar |
test | 用于测试makefile流程 |
makefile 递归式推导目标
目标文件需要依赖文件,就会在当前 makefile 中寻找这个依赖文件,找到之后,判断这个依赖文件是否存在,不存在或者不是最新的,那就直接执行对应规则的命令,执行完毕后,去判断下一个依赖文件
makefile 自定义变量和系统变量
自定义变量的值只能是字符串,且多个值需要用空格隔开,使用$(var)进行使用
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"
系统变量:
makefile 自动化变量
makefile 还支持一种自动化变量:
-
$@
:规则中目标文件的集合 -
$<
:规则中依赖文件的第1个文件 -
$^
:规则中依赖文件的集合 -
$?
:规则中所有比目标文件更新的依赖文件的集合
makefile 模式匹配
使用%
可以自动匹配字符串,就像 Linux 中的*
一样,匹配到的,且规则用不上的,依赖文件,make 命令会跳过他们不进行处理
实现 assert 断言
程序运行的时候难免会出现错误,为了更好的监视错误的发生,可以用ASSERT断言排查错误,在ASSERT排查出错误的时候,为了报错信息不被其他信息所干扰,最好能在关中断的情况下输出错误信息
这里实现的是专供内核使用的 ASSERT,给用户程序使用的 ASSERT 到时候再说
实现开、关中断的函数
这里要解决的问题是:如何在开中断的情况下关中断?
kernel/source/interrupt.c:
在原有的基础上,添加如下内容:
#define EFLAGS_IF 0x00000200 //eflags中的 IF 位为 1
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl%0": "=g"(EFLAG_VAR))
...;
/*开中断并返回开中断前的状态*/
enum intr_status intr_enable(){
enum intr_status old_status;
if(INTR_ON == intr_get_status()){
old_status = INTR_ON;
return old_status;
}else{
old_status = INTR_OFF;
asm volatile("sti"); //开中断,sti 将 IF 位置 1
return old_status;
}
}
/*关中断并返回关中断前的状态*/
enum intr_status intr_disable(){
enum intr_status old_status;
if(INTR_ON == intr_get_status()){
old_status = INTR_ON;
asm volatile("cli":::"memory"); //关中断,cli 将 IF 位置 0
return old_status;
}else{
old_status = INTR_OFF;
return old_status;
}
}
/*将中断状态设置位 status*/
enum intr_status intr_set_status(enum intr_status status){
return status & INTR_ON ? intr_enable():intr_disable();
}
/*获取当前中断状态*/
enum intr_status intr_get_status(){
uint32_t eflags = 0;
GET_EFLAGS(eflags);
return (EFLAGS_IF & eflags)?INTR_ON:INTR_OFF;
}
kernel/source/interrupt.h:
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;
void idt_init();
// 定义中断的两种状态
enum intr_status{
INTR_OFF, //值为 0,表示关闭中断
INTR_ON //值为 1,表示开启中断
};
enum intr_status intr_disable();
enum intr_status intr_enable();
enum intr_status intr_set_status(enum intr_status status);
enum intr_status intr_get_status();
#endif
实现 ASSERT
ASSERT 是用来辅助程序调试的,我们把程序该有的条件状态传递给他,他来监督,一旦条件不符合就报错并将程序挂起
kernel/source/debug.h:
#ifndef __KERNEL_DEBUG_H
#define __KERNEL_DEBUG_H
void panic_spin(char* filename, int line, const char* func, const char* condition);
/*__VA_ARGS__
* _ VA_ARGS_ 是预处理器所支持的专用标识符。
* 代表所有与省略号相对应的参数。
* ... 表示定义的宏其参数可变。 */
#define PANIC(...) panic_spin(__FILE__,__LINE__,__func__,__VA_ARGS__)
#ifdef NDEBUG //不用调试的时候,在程序里定义 NDEBUG 即可删除这里的处理逻辑
#define ASSERT(CONITION)((void)0)
#else
#define ASSERT(CONITION) \
if(CONITION){}else{ \
PANIC(#CONITION);} //#号让宏的参数转换成 字符串 常量
#endif /*__NDEBUF*/
#endif /*__KERNEL_DEBUG_H*/
宏是用来传递参数的,这里的关键在于PANIC要怎么处理了:
kernel/source/debug.c:
#include "debug.h"
#include "print.h"
#include "interrupt.h"
/*打印文件名、 行号、 函数名、 条件并使程序悬停*/
void panic_spin(char* filename, int line, const char* func, const char* condition){
intr_disable(); //因为有时候会单独调用 panic_span 所以在此处关中断
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);
}
这里就是关中断后,打印完相关信息让程序悬停在这里
接下来让ASSERT工作一下:
kernel/source/main.c:
#include "print.h"
#include "init.h"
#include "debug.h"
int main(){
put_str("\nI am kernel\n");
init_all();
//asm volatile("sti"); //开启中断
ASSERT(1==2);
while(1);
return 0;
}
上次把中断打开是为了演示中断,现在暂时给关了,等时机成熟了再打开
通过 makefile 进行编译
这里进行接下来的内容之前,先把当前乱糟糟的文件重新整理一下,以便后续编写 makefile;
整理后的文件目录(之后的文件位置均以本次整理后新的位置为准):
.
├── bochsrc.disk
├── boot
│ ├── include
│ │ └── boot.inc
│ ├── loader.bin
│ ├── loader.S
│ ├── mbr.bin
│ └── mbr.S
├── build
│ ├── init.o
│ ├── interrupt.o
│ ├── kernel.bin
│ ├── kernel.o
│ ├── main.o
│ ├── print.o
│ └── timer.o
├── device
│ ├── timer.c
│ └── timer.h
├── hd60M.img
├── kernel
│ ├── debug.c
│ ├── debug.h
│ ├── global.h
│ ├── init.c
│ ├── init.h
│ ├── interrupt.c
│ ├── interrupt.h
│ ├── kernel.S
│ └── main.c
├── lib
│ ├── kernel
│ │ ├── io.h
│ │ ├── print.h
│ │ ├── print.o
│ │ └── print.S
│ ├── stdint.h
│ └── user
└── makefile
-
目录boot:下是 MBR 和 Loader
-
目录build:存放目标文件和编译出来的可执行程序
-
目录device:存放和外设有关的源代码
-
目录kernel:存放内核代码
-
目录lib:存放封装的调用库
整理完了,接下来编写 makefile 了:
这里的 makefile 我已经改过了,书上原本的 makefile 在咱们 64 位系统环境下不能直接用。
BUILD_DIR = ./build
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
CFLAGS = -m32 -Wall $(LIB) -c -fno-builtin -fno-stack-protector -W #-Wstrict-prototypes -Wmissing-prototypes
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
############ C 代码编译 ##############
$(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 $@
############ ASM 代码编译 ##############
$(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 $@
strip --remove-section=.note.gnu.property $(BUILD_DIR)/kernel.bin
.PHONY: mk_dir hd clean all
mkdir:
if[[ ! -d $(BUILD_DIR) ]];then mkdir $(BUILD_DIR);fi
hd:
dd if=$(BUILD_DIR)/kernel.bin of=./hd60M.img bs=512 count=200 seek=9 conv=notrunc
clean:
cd $(BUILD_DIR) && rm -f ./*
build: $(BUILD_DIR)/kernel.bin
all: mk_dir build hd
运行 Bochs
编译:make all
,运行:
实现字符串操作函数
为了方便以后的各种工作,基础工作也得做
lib/string.c
#include "string.h"
#include "global.h"
#include "debug.h"
/*将 dst_ 起始的 size 个字节设置为 value*/
void memset(void* dst_, uint8_t value, uint32_t size){
ASSERT(dst_ != NULL);
uint8_t* dst = (uint8_t*)dst_;
while(size-- >0){
*dst++ = value;
}
}
/*将 src_ 起始的 size 个字节复制到 dst_*/
void memcpy(void* dst_, const void* src_, uint32_t size){
ASSERT(dst_ != NULL && src_ != NULL);
uint8_t* dst = dst_;
const uint8_t* src = src_;
while(size-- > 0){
*dst++ = *src++;
}
}
/*连续比较以地址 a_ 和地址 b_ 开头的 size 个字节,若相等则返回 0,若 a_ 大于 b_, 返回 +1,否则返回 -1 */
int memcmp(const void* a_, const void* b_, uint32_t size) {
const char* a = a_;
const char* b = b_;
ASSERT( *a != NULL || *b != NULL);
while(size-- >0){
if(*a != *b){
return *a > *b ? 1: -1;
}
a++;
b++;
}
return 0;
}
/* 将字符串从 src_ 复制到 dst_ */
char* strcpy(char* dst_, const char* src_) {
ASSERT(dst_ != NULL && src_ != NULL);
char* r = dst_;
while((*dst_++ = *src_++));
return r;
}
/*返回字符串长度*/
uint32_t strlen(const char* str) {
ASSERT(str != NULL);
const char* p = str;
while (*p++);
return (p - str - 1);
}
/*比较两个字符串,若 a_ 中的字符大于 b_ 中的字符返回 1,相等时返回0, 否则返回-1.*/
int8_t strcmp (const char* a, const char* b) {
ASSERT(a != NULL && b != NULL);
while (*a != 0 && *a == *b) {
a++;
b++;
}
return *a < *b ? -1 :*a > *b; //后面这个,如果a>b,则返回1,a=b则返回0,利用布尔表达式实现一个判断三种输出,这个厉害
}
/*从左到右查找字符串str中首次出现字符 ch 的地址*/
char* strchr(const char* str, const uint8_t ch) {
ASSERT(str != NULL);
while(*str != 0){
if(*str == ch){
return (char*)str;
}
str++;
}
return NULL;
}
/*从后往前查找字符串 str 中首次出现 字符ch的地址*/
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;
}
/*将字符串src_拼接到dst_后,返回拼接的串地址*/
char* strcat(char* dst_, const char* src_) {
ASSERT(dst_ != NULL && src_ != NULL);
char* str = dst_;
while(*str++); //把指针移动到str的最后
--str;
while((*str++ = *src_++));
return dst_;
}
/*在字符串str 中查找字符ch 出现的次数*/
uint32_t strchrs (const char* str, uint8_t ch){
ASSERT(str != NULL);
uint32_t ch_cnt = 0;
const char* p = str;
while(*p != 0) {
if(*p == ch){
ch_cnt++;
}
p++;
}
return ch_cnt;
}
lib/string.h
#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
位图 bitmap 及其函数的实现
位图简介
位图,bitmap,用于资源管理,主要用于管理大容量资源,如硬盘和内存
位图就是用1位来映射其他单位大小的资源,按位与资源一对一的对应关系
位图本质上就是一串二进制位,用字节型数组来存储比较方便
举个例子:如果用位图管理内存,则用一位对应4KB物理内存,也就是一页的大小:
- 如果某位为 0 则表示该页未分配,可以使用
- 如果某位为 1 则表示该页已分配,在回收之前不可再分配使用
位图的定义与实现
lib/kernel/bitmap.h
#ifndef __LIB_KERNEL_BITMAP_H
#define __LIB_KERNEL_BITMAP_H
#include "global.h"
#define BITMAP_MASK 1
struct bitmap {
uint32_t btmp_bytes_len;
/* 在遍历位图时, 整体上以字节为单位, 细节上是以位为单位,所以此处位图的指针必须是单字节 */
uint8_t* bits;
// 使用位图数组需要知道其长度,但是长度得以后才能知道,所以这里可以用指定地址来代替使用数组
};
void bitmap_init (struct bitmap* btmp);
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
位图长度取决于所管理资源的大小,其长度不固定, 只能在 struct bitmap 中提供位图的指针,就是uint8_t" bits。
用指针bits来记录位图的地址,真正的位图由上一级模块提供,并由上一级模块把位图的地址赋值给bits。
lib/kernel/bitmap.c
#include "bitmap.h"
#include "stdint.h"
#include "string.h"
#include "print.h"
#include "interrupt.h"
#include "debug.h"
/*将位图btmp初始化*/
void bitmap_init(struct bitmap* btmp) {
memset(btmp->bits, 0, btmp->btmp_bytes_len);
}
/*判断bit_idx位是否为 1 ,若为 1,则返回true, 否则返回false */
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));
}
/*在位图中申清连续 cnt 个位, 成功, 则返回其起始位下标,失败, 返回 -1 */
int bitmap_scan(struct bitmap* btmp, uint32_t cnt) {
uint32_t idx_byte = 0; //用于记录空闲位所在的字节
/*先逐字节比较,蛮力法 */
while ((0xff == btmp->bits [idx_byte]) && (idx_byte < btmp->btmp_bytes_len)) {
idx_byte++;
}
/*如果找不到可用空间就返回 -1*/
ASSERT(idx_byte < bymp->bymp_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))){
//若next_bit为0
count++;
}else{
count=0;
}
if(count == cnt){
bit_idx_start = next_bit - cnt +1;
break;
}
next_bit++;
}
return bit_idx_start;
}
/*将位图 btmp 的 bit_idx 位设置为 value*/
void bitmap_set(struct bitmap* btmp, uint32_t bit_idx, int8_t value) {
ASSERT ((value == 0) || (value == 1));
uint32_t byte_idx = bit_idx / 8;
uint32_t bit_odd = bit_idx % 8;
if(value){
btmp->bits[byte_idx] |= (BITMAP_MASK << bit_odd);
}else{
btmp->bits[byte_idx] &= ~(BITMAP_MASK << bit_odd);
}
}
代码不难,很容易看懂,4个函数:
-
void bitmap_init (struct bitmap* btmp);
将位图字节数组地址开始,以数组长度进行清零
-
bool bitmap_scan_test(struct bitmap* btmp, uint32_t bit_idx);
已知位图位索引,在数组中找到这个位,判断是1还是0
-
int bitmap_scan(struct bitmap* btmp, uint32_t cnt);
逐字节寻找可用位,找到了后判断需要几个位,如果是一个就可以返回位的位置了
如果是多个,则循环判断当前找到的可用位下一位是否可用,如果可用,计数+1,如果不可用,计数清零,当可用计数和需要的数量相同时,则返回这段可用位的开始位的位置,否则失败返回-1
-
void bitmap_set(struct bitmap* btmp, uint32_t bit_idx, int8_t value);
找到这个位,设置为1的话,就按位或,设置为0的话,就逆然后按位与
内存管理系统
用户进程所占用的空间是由操作系统分配的,内存如何分配并且分配多少?这就是本节要解决的问题。
内存池规划
在实模式下,内存地址就是物理内存地址
在保护模式下,引入了虚拟内存和分页机制,虚拟内存到物理内存之间会经过分页机制来映射,那么问题来了,如何分配不同进程之间虚拟内存到物理内存的映射呢?这里引入了内存池;
内存池也叫内存地址池,将可用的地址都装在一起,需要用的时候,从里面取出来用,用完了再放回去
由于在分页机制下出现了虚拟内存和物理内存,所以需要为这两个内存分别都创建对应的内存池
-
物理内存地址池
物理内存分为两部分:
- 内核物理内存池,只给内核用
- 用户物理内存池,只给用户用
方便起见,这里把内核物理内存池和用户物理内存池分配成一样大小
-
虚拟内存地址池
在分页模式下,每个程序都在自己的虚拟内存中运行,访问的是虚拟内存,访问时会自动经过硬件根据页转换成物理内存
每个程序都有自己的页表,程序使用的虚拟内存地址由链接器决定,之后就不再变了
- 对于
内核
进程,申请内存地址时,从内核的虚拟内存地址池中分配虚拟地址,再从内核
物理内存池中分配物理内存,然后在自己的页表中将两个地址建立好映射关系 - 对于
用户
进程,申请内存地址时,从内核的虚拟内存地址池中分配虚拟地址,再从用户
物理内存池中分配物理内存,然后在自己的页表中将两个地址建立好映射关系
- 对于
内存池分配中的内存的单位是页,也就是4KB
有了物理和虚拟内存池之后,他们的关系如图:
具体是怎么实现的,还是得看代码:
kernel/memory.h
#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H
#include "stdint.h"
#include "bitmap.h"
/*虚拟地址池, 用于虚拟地址管理*/
struct virtual_addr{
struct bitmap vaddr_bitmap; //虚拟地址用到的位图结构
uint32_t vaddr_start; //虚拟地址起始地址
};
extern struct pool kernel_pool, user_pool;
void mem_init(void);
#endif
虚拟地址也是需要分配的,链接器保证虚拟地址的唯一性,但是程序内申请的内存空间也需要保证唯一性,所以需要先知道虚拟地址的使用情况,所以创建一个结构用来保存虚拟地址池
kernel/memory.c
0xc009f000
是内核主线程栈顶,0xc009e000
是内核主线程的pcb
pcb是进程或线程的“身份证”,每个进程都要有,占一个自然页
一页大小的位图可以表示128MB内存,这里支持4页大小,即512MB,故减去4个页的大小,0xc009a000
处是位图的地址
把位图地址选在1M一下是为了方便内存管理,因为1M一下的内存几乎都被占用了,1M以上我们的页表占用了空间256个页的大小
这里代码主要内容是初始化内存池,创建一个内存池需要做一些准备,需要知道该内存池的物理起始地址,内存池字节容量,位图起始地址,位长度:
- 获取空闲的总页数,然后分成两份,一份给内核,一份给用户
- 物理起始地址就是我们1M之后加上页表所占空间之后的位置,内核在物理空间低部分,用户在高部分
- 主要就是建立内存池结构,设置起始位置和大小
内存池结构准备完成之后,将物理内存池位图清理,将内核虚拟内存池位图初始化
#include "memory.h"
#include "stdint.h"
#include "print.h"
#define PG_SIZE 4096
/* 位图地址 */
//因为 0xc009f000是内核主线程栈顶, 0xc009e000是内核主线程的pcb。
//一个页框大小的位图可表示128MB内存,位图位置安排在地址0xc009a000,这样本系统最大支持4个页框的位图, 即512MB
#define MEM_BITMAP_BASE 0xc009a000
//0xc0000000是内核从虚拟地址3G 起。0x100000意指跨过低端1MB内存,使虚拟地址在逻辑上连续
#define K_HEAP_START 0xc0100000
//内存池结构,生成两个实例用于管理内核内存池和用户内存池
struct pool{
struct bitmap pool_bitmap; //本内存池用到的位图结构, 用于管理物理内存
uint32_t phy_addr_start; //本内存池所管理物理内存的起始地址
uint32_t pool_size; //本内存池字节容量
};
struct pool kernel_pool, user_pool; //生成内核内存池和用户内存池
struct virtual_addr kernel_vaddr; //此结构用来给内核分配虚拟地址
//初始化内存池
static void mem_pool_init(uint32_t all_mem) {
put_str("mem_pool_init start\n");
uint32_t page_table_size = PG_SIZE * 256; //记录页目录表和页表占用的字节大小
//页表大小 = 1页的页目录表 +第 0 和第 768 个页目录项指向同一个页表,之前创建页表的时候,挨着页目录表创建了768-1022总共255个页表+上页目录的1页大小,就是256
//第 769~1022 个页目录项共指向 254 个页表,共 256 个页框
uint32_t used_mem = page_table_size + 0x100000; //当前已经使用的内存字节数,1M部分已经使用了,1M往上是页表所占用的空间
uint32_t free_mem = all_mem - used_mem; //剩余可用内存字节数
uint16_t all_free_pages = free_mem / PG_SIZE; //所有可用的页
// 1页为 4KB, 不管总内存是不是 4k 的倍数,对于以页为单位的内存分配策略, 不足 1 页的内存不用考虑了
uint16_t kernel_free_pages = all_free_pages / 2;
uint16_t user_free_pages = all_free_pages - kernel_free_pages;
//为简化位图操作,余数不处理,坏处是这样做会丢内存。好处是不用做内存的越界检查,因为位图表示的内存少于实际物理内存。
uint32_t kbm_length = kernel_free_pages / 8; //Kernel Bitmap的长度,位图中的一位表示一页,以字节为单位,也就是8页表示1字节的位图
uint32_t ubm_length = user_free_pages /8;
uint32_t kp_start = used_mem; //kernel pool start,内核内存池起始地址
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;
//内核内存池和用户内存池位图
//位图是全局的数据,长度不固定。
//全局或静态的数组需要在编译时知道其长度,而我们需要根据总内存 大小算出需要多少字节,所以改为指定一块内存来生成位图。
//内核使用的最高地址是Oxc009f000, 这是主线程的栈地址,32MB内存占用的位图是2KB
//内核内存池的位图先定在 MEM_BITMAP_BASE(Oxc009a000)处
kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;
user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);
//输出内存池信息
put_str(" kernel_pool_bitmap_start:");
put_int((int)kernel_pool.pool_bitmap.bits);
put_str(" kernel_pool_phy_addr_start: ");
put_int(kernel_pool.phy_addr_start);
put_str ("\n");
put_str (" user_pool_bitmap_start: ");
put_int ((int) user_pool.pool_bitmap.bits);
put_str (" user_pool_phy_addr_start: ");
put_int(user_pool.phy_addr_start);
put_str ("\n");
// 将位图置 0
bitmap_init(&kernel_pool.pool_bitmap);
bitmap_init(&user_pool.pool_bitmap);
//下面初始化内核虚拟地址的位图,按实际物理内存大小生成数组。
kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length;
//用干维护内核堆的虚拟地址,所以要和内核内存池大小一致
//位图的数组指向一块没用的内存,目前定位在内核内存池和用户内存池之外
kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);
kernel_vaddr.vaddr_start = K_HEAP_START;
bitmap_init(&kernel_vaddr.vaddr_bitmap);
put_str(" mem_pool_init done \n");
}
//内存管理部分初始化入口
void mem_init(){
put_str("mem_init start\n");
uint32_t mem_bytes_total = (*(uint32_t*)(0xb00));
mem_pool_init(mem_bytes_total);
put_str("mem_init done\n");
}
makefile
新增如下内容:
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)/memory.o $(BUILD_DIR)/string.o $(BUILD_DIR)/bitmap.o
...
$(BUILD_DIR)/string.o: lib/string.c lib/string.h \
kernel/global.h lib/stdint.h\
kernel/debug.h \
lib/kernel/print.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h \
lib/string.h kernel/debug.h \
lib/kernel/print.h lib/stdint.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \
lib/kernel/bitmap.h \
lib/kernel/print.h lib/stdint.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
kernel/init.c
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
/* 负责初始化所有模块 */
void init_all(){
put_str("init_all\n");
idt_init(); // 初始化 中断
timer_init(); // 初始化 PIT
mem_init(); // 初始化内存池
}
运行 Bochs
编译运行:
内存管理系统第1步--分配页内存
完成了内存池的划分之后,接下来还要继续做基础工作,现在距离malloc函数还挺远,当前要做的工作是按页分配内存
kernel/memory.h
内存管理中,必不可少的就是修改页表,需要一些页表相关的宏定义
页表的地址都是真实的物理地址,页表的工作是将虚拟地址转换成物理地址
/*内存池标记, 用于判断用哪个内存池*/
enum pool_flags{
PF_KERNEL = 1, //内核内存池
PF_USER = 2 //用户内存池
};
#define PG_P_1 1 //页表项或页目录项存在属性位
#define PG_P_0 0 //页表项或页目录项存在属性位
#define PG_RW_R 0 //R/W 属性位值,读/执行
#define PG_RW_W 2 //R/W 属性位值,读/写/执行
#define PG_US_U 0 //U/S 属性位值,系统级
#define PG_US_S 4 //U/S 属性位值,用户级
kernel/memory.c
添加如下内容:
#include "global.h"
#include "debug.h"
#include "string.h"
#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)
//在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); //将位起始值开始连续置1,直到设置完需要的页位置
}
vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
}else{
//用户内存池,将来实现用户进程再来补充
}
return (void*)vaddr_start;
}
//得到虚拟地址 vaddr 对应的 pte 指针
static uint32_t* pte_ptr(uint32_t vaddr){
// 先访问到页表自己
// 再用页目录项 pde(页目录内页表的索引)作为pte的索引访问到页表
// 再用pte的索引作为页内偏移
uint32_t* pte = (uint32_t*)(0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr) * 4);
//第一步:0xffc00000 是取出第1023个页目录项进行索引,其实就是页目录表的物理地址
//第二步:((vaddr & 0xffc00000) >> 10) 是将原来vaddr的前10位取出,放在中间10位的位置上,用来获取 pte 的
//第三步:PTE_IDX(vaddr) * 4 会被当作物理偏移直接加上,而不会像其前面10位会被cpu自动*4再加上,所以这里手动*4,获取PTE索引,得到PTE物理地址
return pte;
}
//得到虚拟地址vaddr对应的pde的指针
static uint32_t* pde_ptr(uint32_t vaddr){
//0xfffff 用来访问到页表本身所在的地址
//前10位是1023,是页目录表的物理地址
//中10位是1023,索引到的还是页目录表的物理地址
//后12位是addr的前10位*4,也就是页目录表的索引
uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr) * 4);
return pde;
}
//在m_pool指向的物理内存池中分配1个物理页,成功则返回页框的物理地址,失败则返回NULL
static void* palloc(struct pool* m_pool){
//扫描或设置位图要保证原子操作
int bit_idx = bitmap_scan(&m_pool->pool_bitmap, 1); //找一个物理页面,位图中1位表示实际1页地址
if(bit_idx == -1){
return NULL;
}
bitmap_set(&m_pool->pool_bitmap, bit_idx, 1); //将此位的bit_idx置1
uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start); //物理内存池起始地址 + 页偏移 = 页地址
return (void*)page_phyaddr;
}
这里的pte_ptr
和pde_ptr
比较复杂,需要我们先来回顾一下二级页表的原理:
CPU 会自动将地址进行处理,转换的结果是物理地址,其中进行了三步骤:
- 取出前10位*4,作为索引,加到cr3寄存器中所保存的pde物理地址上,得到pte的物理地址
- 取出中10位*4,作为索引,加到刚刚获得的pte的物理地址上,得到实际的物理地址
- 取出后12位,作为偏移量,加到刚刚获得的实际物理地址,得到最终物理地址
这里获取pte地址的方法则是:
- 构造前10位为1023,也就是0xffc00000,用来索引第1023个页目录项,1023个页目录项存入的地址还是pde的地址
- 取出要获取的vaddr的前10位,右移10位,变成中间10位*4,作为索引,加到刚刚获取到的pde地址上,得到pte的物理地址
- 取出要获取的vaddr的中10位,右移12位,变成后12位,手动*4,作为pte的偏移,自动加到pte地址上之后,得到实际上的pte地址
获取pde地址原理类似
此处的vaddr与它所在的pde,pte是否存在无关,仅仅是用构造的vaddr获取该地址的pte和pde地址
kernel/memory.c
添加如下内容:
//页表中添加虚拟地址_vaddr与物理地址_page_phyaddr的映射
static void page_table_add(void* _vaddr, void* _page_phyaddr){
uint32_t vaddr = (uint32_t)_vaddr;
uint32_t page_phyaddr = (uint32_t)_page_phyaddr;
uint32_t* pde = pde_ptr(vaddr);
uint32_t* pte = pte_ptr(vaddr);
//执行*pte,会访问到空的 pde。所以确保pde创建完成后才能执行*pte,否则 会引发page_fault。
//因此在 *pde为0时,pte只能出现在下面 else语句块中的*pde后面。
//总之,就是创建pte之前必须先创建好pde才行,不然没法通过pde访问到pte
//先在页目录内判断目录项的p位, 若为1, 则表示该表已存在
if(*pde & 0x00000001){
//页目录项和页表项的第0位为P, 此处判断目录项是否存在
ASSERT(!(*pte & 0x00000001)); //此时pte应该不存在
if(!(*pte & 0x00000001)){ //只要是创建页表,pte就应该不存在,多判断一下放心
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); //创建pte
}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);
//分配到的物理页地址pde_phyaddr对应的物理 内存清0
//避免里面的陈旧数据变成了页表项,从而让页表混乱。
//访间到 pde对应的物理地址,用 pte取高20位便可。
//因为pte基千该pde对应的物理地址内再寻址,把低12位置0便是该pde对应的物理页的起始。
memset((void*)((int)pte & 0xfffff000), 0, PG_SIZE);
ASSERT(!(*pte & 0x00000001));
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
}
}
// 分配 pg_cnt 个页空间, 成功则返回起始虚拟地址,失败时返回 NULL
static void* malloc_page(enum pool_flags pf, uint32_t pg_cnt){
ASSERT(pg_cnt > 0 && pg_cnt < 3840);
//malloc_page 的原理是三个动作的合成:
//1 通过 vaddr_get在虚拟内存池中申请虚拟地址
//2 通过 palloc在物理内存池中申请物理页
//3 通过 page_table_add 将以上得到的虚拟地址和物理地址在页表中完成映射
void* vaddr_start = vaddr_get(pf, pg_cnt);
if(vaddr_start == NULL){
return NULL;
}
uint32_t vaddr = (uint32_t)vaddr_start;
uint32_t 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;
}
// 从内核物理内存池中申请内存,成功返回虚拟地址,失败返回NULL
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;
}
// 就是上一个函数的封装
page_table_add函数是建立虚拟地址和物理地址的映射,所以需要修改pde和pte的内容,通常情况下,pte是不存在的,创建pte之前需要先创建好pde,通过palloc从物理内存池获取一个页的大小,存入pde,将pte所在位置那一页清空,然后写入pte。
虚拟地址用来获取页表地址,页表地址里存储的是对应物理地址的映射,所以要先通过虚拟地址获取页表地址,然后修改页表映射到物理地址,其中页表项的内存也是从物理内存池中申请的
后面两个函数很好看懂,就不解释了
kernel/main.c
修改main来测试一下:
#include "print.h"
#include "init.h"
#include "memory.h"
int main(){
put_str("\nI am kernel\n");
init_all();
//asm volatile("sti"); //开启中断
void* addr = get_kernel_pages(5);
put_str("\n get_kernel_page start vaddr is:");
put_int((uint32_t)addr);
put_str("\n");
while(1);
return 0;
}
这里就是申请一块内存空间
运行Bochs:
运行:
来看看映射情况:
真的,抄代码的时候要抄的仔细一点!!!不然出问题够debug的了!!!
之前写页表的时候,应该是把页目录表的地址写到页目录项1023个位置,我给写成第1024了,坑!!!排查了1个多小时
然后后来我某一行代码里写成死循环了,又排错1个多小时,写代码几分钟,排错几小时,太真实了!!
参考资料
- 《操作系统 真象还原》 Chapter 8
- 消除编译告警:https://www.cnblogs.com/yilipika/p/4981556.html