操作系统真象还原 学习笔记09--内存管理系统

selph
selph
发布于 2021-02-05 / 979 阅读
0
0

操作系统真象还原 学习笔记09--内存管理系统

本篇对应书籍第八章的内容

本篇内容介绍了使用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"

系统变量:

image-20210131163200954

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,运行:

image-20210131194608481

实现字符串操作函数

为了方便以后的各种工作,基础工作也得做

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

有了物理和虚拟内存池之后,他们的关系如图:

image-20210201014512485

具体是怎么实现的,还是得看代码:

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

编译运行:

image-20210201143022802

内存管理系统第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_ptrpde_ptr比较复杂,需要我们先来回顾一下二级页表的原理:

在这里插入图片描述

CPU 会自动将地址进行处理,转换的结果是物理地址,其中进行了三步骤:

  1. 取出前10位*4,作为索引,加到cr3寄存器中所保存的pde物理地址上,得到pte的物理地址
  2. 取出中10位*4,作为索引,加到刚刚获得的pte的物理地址上,得到实际的物理地址
  3. 取出后12位,作为偏移量,加到刚刚获得的实际物理地址,得到最终物理地址

这里获取pte地址的方法则是:

  1. 构造前10位为1023,也就是0xffc00000,用来索引第1023个页目录项,1023个页目录项存入的地址还是pde的地址
  2. 取出要获取的vaddr的前10位,右移10位,变成中间10位*4,作为索引,加到刚刚获取到的pde地址上,得到pte的物理地址
  3. 取出要获取的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:

运行:

image-20210205135649954

来看看映射情况:

image-20210205135718580

真的,抄代码的时候要抄的仔细一点!!!不然出问题够debug的了!!!

之前写页表的时候,应该是把页目录表的地址写到页目录项1023个位置,我给写成第1024了,坑!!!排查了1个多小时

然后后来我某一行代码里写成死循环了,又排错1个多小时,写代码几分钟,排错几小时,太真实了!!

参考资料


评论