第8章-内存管理系统

这是一个网站有所有小节的代码实现,同时也包含了Bochs等文件

8.1 Makefile简介

8.1.1 Makefile是什么
8.1.2 makefile基本语法

在这里插入图片描述

make 给咱们提供了方法,可以在命令之前加个字符’@’,这样就不会输出命令本身信息了

8.1.3 跳到目标处执行

我们可以用目标名称作为 make 的参数,采用“ make 目标名称”的方式,单独执行目标名称处的规则.

8.1.4 伪目标

make 规定,当规则中不存在依赖文件时,这个目标文件名就称为一一伪目标。伪目标,顾名思义,也就是不产生真实的目标文件,所以当然也就不需要依赖文件了。于是,伪目标所在的规则就变成了纯粹地执行命令,只要给 make 指定该伪目标名做参数,就能让伪目标规则中的命令直接执行。

为了避免伪目标和真实目标文件同名的情况,可以用关键字“ .PHONY”来修饰伪目标,格式为“ .PHONY:伪目标名”,这样不管与伪目标同名的文件是否存在, make 照样执行伪目标处的命令。

在这里插入图片描述

8.1.5 make:递归式推导目标
8.1.6 自定义变量和系统变量

变量定义的格式:变量名=值(字符串),多个值之间用空格分开。 make 程序在处理时会用空格将值打散,然后遍历每一个值。另外,值仅支持字符串类型,即使是数字也被当作字符串来处理 。

变量引用的格式:$(变量名)。这样,每次引用变量时,变量名就会被其值(宇符串)替换。

在这里插入图片描述

8.1.8自动变量

$@:表示规则中的目标文件名集合,如果存在多个目标文件,$@则表示其中每一个文件名。

$<: 表示规则中依赖文件中的第 1 个文件。

$^:表示规则中所有依赖文件的集合,如果集合中有重复的文件,$^会自动去重。

$?:表示规则中,所有比目标文件 mtime 更新的依赖文件集合。
在这里插入图片描述

8.1.9模式规则

%:用来匹配任意多个非空字符。比如%.o 代表所有以.o为结尾的文件,g%s.o是以字符 g 开头的所有以.o。为结尾的文件, make 会拿这个字符串模式去文件系统上查找文件,默认为当前路径下。

在这里插入图片描述

8.2实现assert断言

8.2.1实现开、关中断的函数
#define EFLAGS_IF 0x00000200 //eflags寄存器中if位为1
#define GET_EFLAGS (EFLAG_VAR) asm volatile("pushfl; popl %0":"=g"(EFLAG_VAR)) //读取当前的标志寄存器 EFLAGS 的值,并将其存储到 C 语言中的变量 EFLAG_VAR 中

/*定义中断的两个状态*/
enum intr_status{
    INTR_OFF,
    INTR_ON
};

/*开中断并返回开中断前的状态*/
enum inter_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 inter_status intr_disable(){
    enum intr_status old_status;
    if(INTR_ON==intr_get_status()){
        old_status = INTR_ON;
        asm volatile("cli": : :"memory");        //关闭中断,sti指令将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;
}

8.2.2实现ASSERT

在C语言中 ASSERT是用宏来定义的,其原理是判断传给 ASSERT 的表达式是否成立,若表达式成立则什么都不做,否则打印出错信息并停止执行。

__FILE__,__LINE__,__func__,这三个是预定义的宏,分别表示被编译的文件名、被编译文件中的行号、被编译的函数名。

/*
 * @Author: Adward-DYX 1654783946@qq.com
 * @Date: 2024-04-08 09:38:54
 * @LastEditors: Adward-DYX 1654783946@qq.com
 * @LastEditTime: 2024-04-08 10:26:38
 * @FilePath: /OS/chapter8/8.2/kernel/debug.c
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#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_spin,所以在这里先关闭中断
    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);
}
#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 NDEBUF
    #define ASSERT(CONDITION) ((void)0)
#else
    #define ASSERT(CONDITION) \
    if(CONDITION){ }else{   \
        /*符号#让编译器将宏的参数转化为字符串字面量,就是转化为字符串*/ \
        PANIC(#CONDITION); \
    }
#endif // NDEBUF

#endif //__KERNEL_DEBUG_H

8.3实现字符串操作函数

/*
 * @Author: Adward-DYX 1654783946@qq.com
 * @Date: 2024-04-08 11:22:49
 * @LastEditors: Adward-DYX 1654783946@qq.com
 * @LastEditTime: 2024-04-08 12:18:47
 * @FilePath: /OS/chapter8/8.3/lib/string.c
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#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 个字节,若相等则返回 O,若 a_大于 b_ ,返回 +1 ,否则返回-1 */
int memcmp(const void* a_, const void* b_, uint32_t size){
    const uint8_t* a = a_;
    const uint8_t* 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(cosnt char* a, const char* b){
    ASSERT(a!=NULL&&b!=NULL);
    while(*a!=0&&*a==*b){
        a++;
        b++;
    }
    return *a<*b ? -1 : *a>*b;
}

/*从左到右查找字符串str中首次出现字符ch的地址*/
char* strchr(const char* str, const uint8_t ch){
    ASSERT(str!=NULL);
    while(*str!=0){
        f(*str==ch)
            return (char*)str; //需要强制转化成和返回值类型一样否则编译器会报 const 属性丢失
        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;
    while((*str++=*src++)); //当str被赋值为0时也就是表达式不成立,正好添加了字符串结尾的。
    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;
}

8.4位图bitmap及其函数的实现

位图,也就是 bitmap,广泛用于资源管理,是一种管理资源的方式、手段。“资源”包括很多,比如内存或硬盘,对于此类大容量资源的管理一般都会采用位图的方式。

位图包含两个概念:位和图 。 位是指 bit,即字节中的位, 1 字节中有 8 个位。图是指 map, map 这个词在很久之前就介绍过啦,地图本质上就是映射的意思,映射,即对应关系。综合起来,位图就是用字节中的 1 位来映射其他单位大小的资源,按位与资源之间是一对一的对应关系。

位图中的每一位都将表示实际物理内存中的 4kb,也就是一页,即位图中的一位对应物理内存中的一页,如果某位为 0,表示该位对应的页未分配,可以使用,反之如果某位为 1 ,表示该位对应的页己经被分配出去了,在将该页回收之前不可再分配。

在这里插入图片描述

8.4.2位图的定义与实现
/*
 * @Author: Adward-DYX 1654783946@qq.com
 * @Date: 2024-04-09 10:16:53
 * @LastEditors: Adward-DYX 1654783946@qq.com
 * @LastEditTime: 2024-04-09 17:25:49
 * @FilePath: /OS/chapter8/8.4/lib/kernel/bitmap.h
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#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);
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

/*
 * @Author: Adward-DYX 1654783946@qq.com
 * @Date: 2024-04-09 10:31:56
 * @LastEditors: Adward-DYX 1654783946@qq.com
 * @LastEditTime: 2024-06-10 17:42:57
 * @FilePath: /OS/chapter8/8.5.1/lib/kernel/bitmap.c
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#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 */
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));
}

/* 在位图中申请连续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]) { 
	   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;	      // 先将其置为-1,若找不到连续的位就直接返回
   while (bit_left-- > 0) {
      if (!(bitmap_scan_test(btmp, next_bit))) {	 // 若next_bit为0
	      count++;
      } else {
	      count = 0;
      }
      if (count == cnt) {	    // 若找到连续的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;    // 取余用于索引数组内的位

/* 一般都会用个0x1这样的数对字节中的位操作,
 * 将1任意移动后再取反,或者先取反再移位,可用来对位置0操作。*/
   if (value) {		      // 如果value为1
      btmp->bits[byte_idx] |= (BITMAP_MASK << bit_odd);
   } else {		      // 若为0
      btmp->bits[byte_idx] &= ~(BITMAP_MASK << bit_odd);
   }
}


8.5内存管理系统

8.5.1内存池规划

由于在分页机制下有了虚拟地址和物理地址,为了有效地管理它们,我们需要创建虚拟内存地址池和物理内存地址池。

规划物理内存池:一种可行的方案是将物理内存划分成两部分,一部分只用来运行内核,另一部分只用来运行用户进程,将内存规划出不同的部分,专项专用。 我们把物理内存分成两个内存池, 一部分称为用户物理内存池,此内存池中的物理内存只用来分配给用户进程。另 一部分就是内核物理内存池,此内存池中的物理内存只给操作系统使用。

内存池中的内存也得按单位大小来获取,这个单位大小是4KB,称为页,故,内存池中管理的是一个个大小为 4KB的内存块,从内存池中获取的内存大小至少为 4KB或者为 4KB 的倍数

现在,我们就来进行内存管理的核心准备工作,初始化三个内存池:管理内核可用虚拟地址空间内存池管理内核可用物理地址空间内存池管理用户可用物理地址空间内存池用户可用虚拟地址空间内存池是要等到创建用户进程时才创立,现在不用初始化。

在这里插入图片描述

A、建立管理可用虚拟地址空间的数据结构虚拟内存池:virtual_addr,包含一个管理位图的数据结构、管理的可用虚拟地址空间的起始地址;建立管理可用物理地址空间的数据结构物理内存池:pool,包含一个管理位图的数据结构、管理的可用物理地址空间的起始地址、这个可用物理地址内存空间的大小;

B、通过A建立的数据结构,建立管理管理内核可用虚拟地址空间的内存池变量kernel_vaddr、管理内核可用物理地址空间的内存池变量kernel_pool、管理用户进程可用的物理地址空间内存池变量user_pool

C、根据作者设置与实际情况,初始化kernel_vaddr、user_pool、kernel_pool,就是初始化虚拟内存池内的位图数据结构、管理的地址空间起始地址,物理内存池内的位图数据结构、管理的地址空间起始地址、可用的物理地址空间大小

D、将C封装成一个函数mem_init(),并在init_all()中调用

/*
 * @Author: Adward-DYX 1654783946@qq.com
 * @Date: 2024-04-09 14:17:52
 * @LastEditors: Adward-DYX 1654783946@qq.com
 * @LastEditTime: 2024-04-09 16:45:14
 * @FilePath: /OS/chapter8/8.4/kernel/memory.h
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H
#include "stdint.h"
#include "bitmap.h"

/*虚拟地址池,用于虚拟地址管理*/
struct virtual_addr
{
    /* data */
    struct bitmap vaddr_bitmap; //虚拟地址用到的位图结构
    uint32_t vaddr_start;   //虚拟地址起始地址
};

extern struct pool kernel_pool, user_pool;
void mem_init(void);
#endif // !__KERNEL_MEMORY_H

/*
 * @Author: Adward-DYX 1654783946@qq.com
 * @Date: 2024-04-09 14:26:27
 * @LastEditors: Adward-DYX 1654783946@qq.com
 * @LastEditTime: 2024-04-09 17:33:33
 * @FilePath: /OS/chapter8/8.4/kernel/memory.c
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#include "memory.h"
#include "stdint.h"
#include "print.h"

#define PG_SIZE 4096

/***********************************位图地址************************************
 * 因为 Oxc009f000 是内核主线程栈顶, Oxc009e000 是内核主线程的pcb。
 * 一个页框大小的位图可表示 128MB 内存,位图位置安排在地址。0xc009a000
 * 这样本系统最大支持 4 个页框的位图,即 512MB
********************************************************************************/
#define MEM_BITMAP_BASE 0xc009a000
/* 0xcOOOOOOO 是内核从虚拟地址 3G 起
 * 0xcOOOOOOO 是内核从虚拟地址 3G起 OxlOOOOO 意指跨过低端 lMB 内存,使虚拟地址在逻辑上连续
*/
#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 个页目录项指向同一个页表+第 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; //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的长度,位图中的一位表示一页,以字节为单位
    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;

    /** 内核内存池和用户内存池位图
     * 位图是全局的数据,长度不固定。
     * 全局或静态的数组需要在编译时知道其长度,
     * 而我们需要根据总内存大小算出需要多少字节,
     * 所以改为指定一块内存来生成位图。
    */
   //内核使用的最高地址是 Oxc009f000,这是主线程的校地址
   //(内核的大小预计为 70KB 左右)
   //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);

   /*将位图置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_ini t 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");
}
8.5.2内存管理系统第一步,分配页内存

为减少学习阻力,先给大伙复习下 32 位虚拟地址的转换过程。

  1. 高 10 位是页目录项 pde 的索引,用于在页目录表中定位 pde,细节是处理器获取高 10 位后自动将其乘以 4,再加上页目录表的物理地址,这样便得到了 pde 索引对应的 pde 所在的物理地址,然后自动在该物理地址中,即该 pde 中,获取保存的页表物理地址(为了严谨,说的都有点拗口了)。
  2. 中间 10 位是页表项 pte 的 索引,用于在页表中定位 pte 。细节是处理器获取中间 10 位后自动将其乘以 4,再加上第一步中得到的页表的物理地址,这样便得到了 pte 索引对应的 pte 所在的物理地址,然后自动在该物理地址(该 pte )中获取保存的普通物理页的物理地址。
  3. 低 12 位是物理页内的偏移量,页大小是 4KB, 12 位可寻址的范围正好是 4阻,因此处理器便直接把低 12 位作为第二步中获取的物理页的偏移量,无需乘以 4c 用物理页的物理地址加上这低 12 位的和便是这 32 位虚拟地址最终落向的物理地址 。

注意啦,再提醒一次,页表的作用是将虚拟地址转换成物理地址,此工作表面虚幻,但内心真实,其转换过程中涉及访问的页目录表、页目录项及页表项,都是通过真实物理地址访问的,否则若用虚拟地址访问它们的话,会陷入转换的死循环中不可自拔。

1、代码功能

从内存池中分配地址,然后将分配到的物理地址与虚拟地址建立映射关系。

2、实现原理

物理内存池与虚拟内存池已经初始化完毕,我们自然就能够从这些内存池中申请到虚拟地址与物理地址。通过建立页表,完成虚拟地址到物理地址的映射。

3、代码逻辑

A、写函数完成申请地址空间,包括物理地址与虚拟地址

B、为二者建立映射关系

4、怎么写代码?

A、在memory.h中建立枚举类型结构体pool_flags用于选择从哪个虚拟内存池中分配内存,这样就可以实现用一个函数即能完成从用户虚拟地址空间分配地址,也能完成从内核虚拟地址空间分配地址;定义模块化的页表项字段宏,用于虚拟地址到物理地址的映射时的页表构建。

B、写函数vaddr_get,通过传入的poll_flags值完成对应的从对应的虚拟内存池中分配虚拟地址;写函数palloc,完成从传入的物理内存池中分配物理地址;

C、写宏PDE_IDX与PTE_IDX完成将从一个虚拟地址当中取出PDT与PTE的索引;写函数pde_ptr与pte_ptr将虚拟地址转换成访问虚拟地址对应的页目录表项的地址与页表表项的地址,这是为了当一个虚拟地址没有页表映射时,我们要动态建立映射,这就需要建立页目录表项与页表表项,自然得需要知道这两个的地址。

D、写出将申请得到的虚拟地址空间与物理地址空间,通过修改页表建立映射关系的函数page_table_add

1、页表存在,那么我们只需要将物理地址填入虚拟地址对应页表表项中即可

2、页表不存在(页目录表表项为空),我们需要先申请物理地址来存放页表,然后填入页目录表项这个页表的地址,然后初始化页表,最后将传入的物理地址填入虚拟地址对应页表表项中

E、写函数malloc_page根据传入的pool_flags的值决定是为内核空间还是用户空间分配连续的多个页面,包含从对应的虚拟内存池中分配虚拟地址(调用vaddr_get),从对应的物理内存池中分配物理地址(调用palloc),然后为虚拟地址与物理地址建立映射(调用page_talbe_add)。

F、写函数get_kernel_pages,快捷为内核申请地址空间(调用malloc_page)

/*
 * @Author: Adward-DYX 1654783946@qq.com
 * @Date: 2024-04-09 14:17:52
 * @LastEditors: Adward-DYX 1654783946@qq.com
 * @LastEditTime: 2024-04-10 10:47:04
 * @FilePath: /OS/chapter8/8.4/kernel/memory.h
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H
#include "stdint.h"
#include "bitmap.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_S 0    //U/S 属性位值,系统级
#define PG_US_U 4    //U/S 属性位值,用户级

 
/*虚拟地址池,用于虚拟地址管理*/
struct virtual_addr
{
    /* data */
    struct bitmap vaddr_bitmap; //虚拟地址用到的位图结构
    uint32_t vaddr_start;   //虚拟地址起始地址
};

extern struct pool kernel_pool, user_pool;
void mem_init(void);
#endif // !__KERNEL_MEMORY_H

/*
 * @Author: Adward-DYX 1654783946@qq.com
 * @Date: 2024-04-09 14:26:27
 * @LastEditors: Adward-DYX 1654783946@qq.com
 * @LastEditTime: 2024-06-10 18:59:08
 * @FilePath: /OS/chapter8/8.4/kernel/memory.c
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
#include "memory.h"
#include "bitmap.h"
#include "stdint.h"
#include "global.h"
#include "debug.h"
#include "string.h"
#include "print.h"

#define PG_SIZE 4096

/***********************************位图地址************************************
 * 因为 Oxc009f000 是内核主线程栈顶, Oxc009e000 是内核主线程的pcb。
 * 一个页框大小的位图可表示 128MB 内存,位图位置安排在地址。xc009a000
 * 这样本系统最大支持 4 个页框的位图,即 512MB
********************************************************************************/
#define MEM_BITMAP_BASE 0xc009a000
/* 0xcOOOOOOO 是内核从虚拟地址 3G 起
 * 0xcOOOOOOO 是内核从虚拟地址 3G起 OxlOOOOO 意指跨过低端 lMB 内存,使虚拟地址在逻辑上连续
*/
#define K_HEAP_START 0xc0100000

#define PDE_IDX(addr) ((addr&0xffc00000)>>22) //高10位
#define PTE_IDX(addr) ((addr&0x003ff000)>>12) //中10位

/*存池结构,生成两个实例用于管理内核内存池和用户内存池*/
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;    //此结构用来给内核分配虚拟地址


/*在 pf 表示的虚拟内存池中申请 pg_cnt 个虚拟页,成功则返回虚拟页的起始地址,失败则返回 NULL*/
static void* vaddr_get(enum pool_flags pf, uint32_t pg_cnt){
    int vaddr_start = 0, bit_idx_start = -1;
    uint32_t cnt = 0;
    if(pf==PF_KERNEL){
        bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap,pg_cnt); //  返回的是成功位置的下标需要*页大小
        if(bit_idx_start == -1) return NULL;
        while(cnt < pg_cnt){
            bitmap_set(&kernel_vaddr.vaddr_bitmap,bit_idx_start+cnt++,1);
        }
        vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
    }else{
        //用户内存池,将来实现用户进程在补充
    }
    return (void*)vaddr_start;
}

/*得看书205页*/
/*得到虚拟地址 vaddr 对应的 pte 指针*/
uint32_t* pte_ptr(uint32_t vaddr){
    /*访问到页表自己 再用页目录项 pde (页目录内页袤的索引)作为pte的索引访问到页表 再用pte的索引作为页内偏移*/
    uint32_t* pte = (uint32_t*)(0xffc00000 + ((vaddr & 0xffc00000)>>10) + PTE_IDX(vaddr) * 4); //这里得到的是新的虚拟地址 能够访问到它的页表物理地址
    return pte;
}

/*得到虚拟地址 vaddr 对应的 pde 的指针*/
uint32_t* pde_ptr (uint32_t vaddr){
    /* Oxfffff 用来访问到页表本身所在的地址 */
    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);  //找到一个物理页
    if(bit_idx == -1)
        return NULL;
    bitmap_set(&m_pool->pool_bitmap,bit_idx,1); //将这个位置1,表示以用
    uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start);
    return (void*)page_phyaddr;
}

/*表中添加虚拟地址_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 就应该不存在,多判断一下放心
        *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
    }else{  ///目前应该不会执行到这,因为上面的 ASSERT 会先执行
        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 对应的物理内存清 O,
     * 避免里面的陈旧数据变成了页表项,从而让页表混乱。
     * 访问到 pde 对应的物理地址,用 pte 取高 20 位便可。
     * 因为 pte 基于该 pde 对应的物理地址内再寻址,
     * 把低 12 位置0。便是该 pde 对应的物理页的起始*
    */
    memset((void*)((int)pte & 0xfffff000),0,PG_SIZE); //这里要的地址必须是pte的虚拟地址
    ASSERT(!(*pte & 0x00000001));
    *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
   }
}

/*分配 pg_cnt 个页空间,成功则返回起始虚拟地址,失败时返回 NULL*/
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, 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;
}

/*从内核物理内存池中申请 1 页内存,成功则返回其虚拟地址,失败则返回 NULL */
void* get_kernel_pages(uint32_t pg_cnt){
    void* vaddr = malloc_page(PF_KERNEL, pg_cnt);
    if(vaddr != NULL)   //若分配的地址不为空,将页框清0后返回
        memset(vaddr,0,pg_cnt*PG_SIZE);
    return 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 个页目录项指向同一个页表+第 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; //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的长度,位图中的一位表示一页,以字节为单位
    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;

    /** 内核内存池和用户内存池位图
     * 位图是全局的数据,长度不固定。
     * 全局或静态的数组需要在编译时知道其长度,
     * 而我们需要根据总内存大小算出需要多少字节,
     * 所以改为指定一块内存来生成位图。
    */
   //内核使用的最高地址是 Oxc009f000,这是主线程的校地址
   //(内核的大小预计为 70KB 左右)
   //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);

   /*将位图置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_ini t 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");
}

所以,申请内存这个行为反映到代码上的实质是,将一个4k空间起始的物理地址,填入4K虚拟空间起始地址对应的页表表项中

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部