《操作系统 真相还原》第八章 内存管理系统

断言

在我们的系统中,我们一共需要实现两种断言,一种是内核系统使用的ASSERT,还有一种是用户进程使用的assert。由于我们目前还在实现内核阶段,所以我们只实现内核断言就好了。
首先我们需要知道两个前提,那就是一旦内核出现错误,那基本上就是很严重的错误,也就没有运行下去的必要了。第二就是我们在打印我们的错误的时候不希望有其他的进程来干扰我们,所以我们还要实现一个手动的开关中断的过程。

获取中断状态

这里我们把上一章的interrupt.c修改即可

#define EFLAGS_IF 0x00000200        //eflags寄存器中的if位为1
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; pop %0" : "=g" : (EFLAG_VAR))   //pushfl是指将eflags寄存器值压入栈顶

/* 开中断并返回开中断前的状态 */
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_OFF;
    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 ;
}

上面的代码跟上一章的没有太大的区别,就是修改了eflags的值罢了,这里我们用enum关键字来定义我们的中断状态,这是因为有两种状态,并且0或1的表示的比太直观,因此采用enum。
修改interrupt.c的同时,我们也需要修改一下我们的头文件interrupt.h

#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
typedef void* intr_handler;
void idt_init(void);    //初始化idt描述表

/* 定义两种中断的状态
 * INTR_OFF值为0表示关中断
 * INTR_ON值为1表示开中断
 */
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);

#endif

ASSERT

刚才我们只是实现了开关中断以及获取状态,这里我们开始实现assert,我们的assert是用来辅助程序进行调试的,通常在开发阶段,我们实现的ASSERT相当于是一个“哨兵”的定义,我们将程序运行到此该存在的状态传递给他,让他帮我们监督程序,若程序运行到这儿不符合我们之前构建好的状态,那么他就会报错,我们利用上一章的关闭中断函数intr_disable()来实现。
首先我们在kernel目录下定义一个debug.h文件

#ifndef __KERNEL_DEBUG_H
#define __KERNEL_DEBUG_H
void panic_spin(char* filename, int line, const char* func, const char* condition);

/***************** __VA_ARGS__  *******************
 * __VA_ARGS__是预处理器支持的专用标识符
 * 代表所有与省略号想对应的参数
 * "..."表示定义的宏参数可变 */
#define PANIC(...) panic_spin(__FILE__, __LINE__, __func__, __VA_ARGS__)    //表示把panic函数定义为宏,这里里面的__FILE__等宏是gcc自动解析的
/**************************************************/

#ifdef NDEBUG
  #define ASSERT(CONDITION) ((void)0)   //这里若定义了NDEBUG,则ASSERT宏会等于空值,也就是取消这个宏
#else                                   //否则就说明是调试模式,所以下面才是真正的定义断言
#define ASSERT(CONDITION) \
if(CONDITION){}else{    \               //如果条件满足,则什么也不做,直接过
  /* 符号#让编译器将宏的参数转化为字符串面量 */ \       //但是若条件不满足,则悬停程序
  PANIC(#CONDITION); \                  //调用panic_spin(),这里是可变参数
}

#endif /*__NDEBUG*/
#endif /*__KERNEL_DEBUG_H*/

这里就是定义了一个ASSERT函数,然后我们继续定义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();                   //这里先关中断,免得别的中断打扰
  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("condition:");
  put_str(condition);
  put_str("\n");
  while(1);
}

我们之前定义的panic_spin也就仅仅打印了这几个条件以及行号而已,然会就是while循环。之后我们到main函数中使用以下,我们先修改以下main.c

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

这里由于我们定义的断言ASSERT(1==2),这样程序运行到这里就会检查1是否等于2,发现不等于,就认为这里出现了错误。
之后我们继续编译链接,下面我们给出本次的Makefile文件

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 = -Wall $(LIB) -c -fno-builtin -no-pie -fno-pic -m32 -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/interrupt.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 $@

############### 链接所有目标文件 ###############3
$(BUILD_DIR)/kernel.bin : $(OBJS)
        $(LD) $(LDFLAGS) $^ -o $@         #这里$^代表所有依赖文件

.PHONY : mk_dir hd clean all                            #定义伪目标

mk_dir:
        if [[ ! -d $(BUILD_DIR) ]];then mkdir $(BUILD_DIR);fi         #若没有这个目录,则创建

hd:
        dd if=$(BUILD_DIR)/kernel.bin \
                of=/home/dawn/repos/OS_learning/bochs/hd60M.img \
                bs=512 count=200 seek=9 conv=notrunc

clean:
        cd $(BUILD) && rm -f ./*

build : $(BUILD_DIR)/kernel.bin

all : mk_dir build hd

然后我们make all命令来进行编译就好了。
在这里插入图片描述
可以看到我们实现了断言,打印了我们的文件名以及函数名还有行数。

实现字符串操作

上一部分我们编写main函数的时候发现,对于字符串的操作我门为0,这样操作实在有点不方便,这次我们完善下我们的字符串操作,我们在lib目录下实现我们的string.c,用来处理各种字符操作。
由于我们这里会使用很多c语言的同名函数,所以这里我们在编译的时候会添加-fno-builtin参数,这里就是防止编译器报错。

#ifndef __LIB_STRING_H
#define __LIB_STRING_H
#define NULL 0
#include "stdint.h"
void memset(void* dst_, uint8_t value, uint32_t size); //单字节复制,指定目的地
void memcpy(void* dst_, 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(cosnt char* str, const uint8_t ch);   //反向查找字符地址
char* strcat(char* dst_, const char* src_);         //连接字符串
uint32_t strchrs(const char* str, uint8_t ch);      //计算相同字符数量

#endif

这里给出的是lib目录下的string.h,这里没给出string.c是因为就是普通的业务代码。
我们在mian函数中定义两个字符串分别是hello和world,然后拼接。,在这里插入图片描述
可以看到在ASSERT之前我们成功拼接了hello和world字符串。

位图bitmap的实现

位图我们在之前的章节简单的提过,我们在学习特权级的时候说过IO位图,他一般是放在TSS的头顶上的,这里我们实现位图是因为要进行资源管理。
这里的位图也是一样的,它实际上就是一串二进制bit位,他的每一位都有两种状态那就是0和1,我们用分页来举个例子,我们的位图的一个bit就代表着一个页,若这个页被分配出去,那我们就将该bit位置1,否则为0.
所以位图就是这样来管理内存资源的。
在这里插入图片描述
看完了,我们来实现一下
先给出定义在kernel下的bitmap.h,这里先给出头文件,我们看看位图的数据结构是怎样的。

#ifndef __KERNEL_BITMAP_H
#define __KERNEL_BITMAP_H
#include "global.h"
#define BITMAP_MASK 1
typedef int bool;
struct bitmap {
  uint32_t btmp_bytes_len;      //位图的字节长度
  /* 在遍历位图的时候,整体以字节为单位,细节上是以位为单位,因此这里的指针为单字节 */
  uint8_t* bits;                //位图的指针
};

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

#endif

可以看到位图就是类似于一个字符串而已,不过他的颗粒度更细。下面是具体的实现代码,一样放在kernel目录下

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

/* 判断bit_idx位是否为1,若为1,则返回true,否则返回false */
bool bitmap_scan_test(struct bitmap* btmp, uint32_t bit_idx){
  uint32_t byte_idx = bit_idx/8;                //这里是求所在位所处的字节偏移
  uint32_t bit_odd = bit_idx%8;                //这里是求在字节中的位偏移
  return (btmp->bits[byte_idx] & (BITMAP_MASK << bit_odd));     //与1进行与操作,查看是否为1
}

/* 在位图中申请连续cnt个位,成功则返回其起始位下标,否则返回-1 */
int bitmap_scan(struct bitmap* btmp, uint32_t cnt){
  uint32_t idx_byte = 0;                    //记录空闲位所在的字节
  /* 逐字节比较,蛮力法 */
  while((0xff == btmp->bits[idx_byte]) && (idx_byte < btmp->btmp_bytes_len)){
    /* 1表示已经分配,若为0xff则说明该字节内已经无空闲位,到下一字节再找 */
    idx_byte++;
  }

  ASSERT(idx_byte < btmp->btmp_bytes_len);
  if(idx_byte == btmp->btmp_bytes_len){     //这里若字节等于长度的话,那说明没有了剩余空间了
    return -1;
  }

  /* 这里若找到了空闲位,则在该字节内逐位比对,返回空闲位的索引 */
  int idx_bit = 0;
  /* 同btmp->bits[idx_byte]这个字节逐位对比 */
  while((uint8_t)(BITMAP_MASK << idx_bit) & btmp->bits[idx_byte]){ //注意这里&是按位与,所有位都为0才返回0,跳出循环
    idx_bit++;
  }

  int bit_idx_start = idx_byte * 8 + idx_bit;       //这里就是空闲位在位图中的坐标
  if(cnt == 1){
    return bit_idx_start;           //若咱们只申请数量为1
  }

  uint32_t bit_left = (btmp->btmp_bytes_len*8 - bit_idx_start);     //记录还剩下多少个位
  uint32_t next_bit = bit_idx_start + 1;
  uint32_t count = 1;               //用来记录找到空闲位的个数

  bit_idx_start = -1;               //将其置-1,若找不到连续的位就返回
  while(bit_left-- >0){
    if(!(bitmap_scan_test(btmp,next_bit))){
      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){                                //value为1
    btmp->bits[byte_idx] |= (BITMAP_MASK << bit_odd);
  }else{                                    //value为0
    btmp->bits[byte_idx] &= ~(BITMAP_MASK << bit_odd);
  }
}

内存管理系统

内存池

什么是内存池

内存池可以看成一个资源库,什么是资源呢?
其实顾名思义,资源当然就是内存了。
在前面的章节中,咱们实现了分页机制,所以现在地址分为了虚拟地址和物理地址,为了有效地分配他们,所以这里我们需要实现虚拟地址的内存池和物理地址的内存池。

物理地址内存池

我们的程序都是运行在物理内存中的,因此我们再次划分,将物理内存池再划分为用户物理内存池和内核物理内存池在这里插入图片描述
我们将物理内存池对半分,就是内核内存池和用户内存池。

虚拟地址内存池

用户程序的地址是在链接的过程中就定下来了,而由于使用的是虚拟地址,也就是说不同的进程之间的地址选择互不干涉,但程序在运行的过程中会有这动态内存的申请,比如malloc和free等,这样我们就必须向程序返回一个虚拟内存块,但是如何知道哪些内存是空闲可以分配的呢?所以说这里我们也需要有虚拟地址内存池。
而虽然说内核程序完全可以自己随便找地方村,但是这样一定会存在不可预料的错误,因此内核也需要通过内核管理系统申请内存,所以这里我们也需要有内核的虚拟内存池。当他们申请内存的时候,首先从虚拟地址池分配虚拟地址,再从物理地址池中分配物理地址,然后在内核将这两种地址建立好映射关系。
在这里插入图片描述

实现

先给出对应的头文件,我们定义为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

首先我们先规划一下,之前我们在loader.S中定义的栈地址吗?我们那时候将内核的栈地址定义在了虚拟地址20c009f000,是这样的,我们这里必须先解释一个简单的知识点,是我们之后遇到的PCB(程序控制块),而我们将来所实现的PCB都必须要占用1页内存,也就是4KB大小的内存空间,因此PCB的内容首地址必须是0xXXXXX000,末尾地址必须是0xXXXXXfff。
这里我们再来解释一下PCB的结构,首先首地址向上存放着一些进程或线程的信息,而高出0xXXXXXfff向下就是栈空间了,所以我们之前定义的栈首地址为0xc009f000就是为main主线程所预留的栈空间同时也为了PCB所预留的空间,因此我们这里就将PCB的首地址定义为0xc009e000.
说完PCB,我们还要讨论一下上面我们解释的位图,这里一个位图单位代表一页,因为我们目前分配的是512MB,可以知道我摸嗯一共分配有512MB/4KB=128K页,所以共需要我们的位图大小为128/8-16KB,所以我们需要16KB/4KB=4个页才能完整存放咱们的位图。因此我们将位图放在咱们的PCB之前,也就是0xc009a000,这里离PCB有4个页,刚好够。
我们立即来实现一下,也就是初始化一些代码

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

#define PG_SIZE 4096    //4K
/************************ 位图地址 ****************************/
#define MEM_BITMAP_BASE 0xc009a000
/**************************************************************/

/* 0xc0000000是内核从虚拟地址3G开始
 * 而1MB指的是跨过低端1MB内存
 * */

/* 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){    //这里的all_mem传递的参数是总共的物理内存
  put_str("     mem_poool_init_start \n ");
  uint32_t page_table_size = PG_SIZE * 256;     //这里只计算769~1022是因为这一部分是属于内核进程
  //页表大小 = 1页的页目录表 + 第0项和第768个页目录项指向同一个页表 + 第769~1022个页目录项共指向254个页表,共256个页框
  uint32_t used_mem = page_table_size + 0x100000;   //0x100000为低端1MB内存
  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;      //kernel_bitmap的长度,以字节为单位
  uint32_t ubm_length = user_free_pages / 8;        //user_bitmap的长度,以字节为单位

  uint32_t kp_start = used_mem;     //kernel_pool_start 内核物理内存池起始地址
  uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;   //user_pool_start 用户物理内存池起始地址

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

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

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

/******************* 内核内存池和用户内存池位图 *******************
 * 位图是全局的数据,长度不固定
 * 全局或静态的数组需要在编译时知道其长度
 * 而我们需要根据总内存大小算出需要多少字节,
 * 所以改为指定一块来生成位图
 * ****************************************************************/
//内核使用的最高地址是0xc009f000,这里是主线程的栈地址,这里咱们内核占了物理地址的低1MB,但是大概率用不了这么多
//咱们有512MB的内存,所以位图就需要4页
//所以内核内存池的位图定在MEM_BITMAP_BASE(0xc009a000)这里
  kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;
  //用户内存池的位图就当跟屁虫辣
  user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);

/*********************** 输出内存池信息 ***************************/
  put_str("         kernel_pool_bitmap_start:");
  put_int((int)kernel_pool.pool_bitmap.bits);
  put_str(" kernel_pool_phy_addr_start:");
  put_int((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((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));     //这里是咱们之前loader.S中存放物理总内存的地址
  mem_pool_init(mem_bytes_total);
  put_str("mem_init done\n");
}

这里的初始化入口当然也是由咱们之前编写的kernel/init.c来进行调用啦,所以这里我们来看看执行情况
在这里插入图片描述
可以发现这里我们的内核物理内存池起始地址也确实是在咱们的页目录以及内核页表之后,也就是0x200000那儿。

分配页内存

我们已经成功构建了位图,内存池且将他们初始化了,这里我们继续进行下一步工作,得使用他们了,这里我先给出memory.h改进的代码

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

/* 内存池标记,用于判断用哪个内存池,这里采用enum枚举 */
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_R 2   //R/W属性位值,读/写/执行
#define PG_US_S 0   //U/S属性位值,系统级
#define PG_US_U 4   //U/S属性位值,用户级
/* 虚拟地址池,用于虚拟地址管理 */
struct virtual_addr {
  struct bitmap vaddr_bitmap;
  uint32_t vaddr_start;
};

extern struct pool kernel_pool, user_pool;
void mem_init(void);
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt);
void* get_kernel_pages(uint32_t pg_cnt);
#endif

我们添加了一些定义和两个函数,这两个函数的具体实现我们简单的说下,首先我们分配页表,我们需要做三件事:
1.首先在虚拟内存池中申请虚拟地址,然后在虚拟内存池的位图中将他们置为1
2.然后我们在物理内存池申请一定量的物理页框,同样的也要在对应的物理内存池的位图中将其置为1
3.然后我们再通过之前内存机制内篇学习的只是访问到对应的页目录项页目录表,然后修改其中的值以此来实现咱们的虚拟地址与物理地址的映射,这里我给出映射部分的代码,这部分很重要的。

/* 页表中添加虚拟地址_vaddr与物理地址_page_phyaddr的映射 */
static void page_table_add(void* _vaddr, void* _page_phyaddr){
  uint32_t vaddr = (uint32_t)_vaddr,page_phyaddr = (uint32_t)_page_phyaddr;
  uint32_t* pde = pde_ptr(vaddr);
  uint32_t* pte = pte_ptr(vaddr);

/******************************** 注意 **********************************
 * 执行*pte会访问到空的pde,所以确保pde创建完成后才能执行*pte,
 * 否则会引发page_fault。因此在*pde为0的时候,*pte只能出现在下面else语句块中的*pde后面
 * **********************************************************************/
  /* 先在页目录内判断目录项的P位,若为1则表示该表已经存在 */
  if(*pde & 0x00000001){
    //页目录项和页表项的第0位为p,这里是判断页目录项是否存在
    ASSERT(!(*pte & 0x00000001));   //这里若是说以前有已经装载的物理页框,则会报错
    if(!(*pte & 0x00000001)){
      *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
    }else{
      PANIC("pte repeat");      //ASSERT的内置函数
      *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);
  }
}

这里的pte是对应的页表项地址,pde是对应的页目录项地址,我们平时说的映射其实也就是操作这个页表而已了,其中若页目录项或页表项为空的时候我们必须先创建页目录然后在创建页表来实现映射。
然后我们再到main.c里面实现

#include "print.h"
#include "init.h"
#include "debug.h"
#include "string.h"
#include "memory.h"

int main(void){
  put_str("I am Kernel\n");
  init_all();
  void* addr = get_kernel_pages(3);
  put_str("\n get kernel pages start vaddr is:");
  put_int((uint32_t)addr);
  put_str("\n");
  while(1);
  return 0;
}

这里我们是在main函数里面申请了三个虚拟页,然后我们打印一下我们的申请的虚拟页首地址,我们进入调试界面查看是否有对应的映射
在这里插入图片描述
我们看到这里刚好就多了三个页大小的映射,这证明我们已经正确的进行了映射,我们已经实现了初步的内存管理了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值