操作系统真象还原 --- 8.内存管理系统

本文详细介绍了操作系统内存管理的基础,包括makefile的使用、ASSERT断言的实现、Bitmap位图在内存管理中的应用,以及内存管理系统的构建。通过对位图结构体的定义和相关操作函数的实现,展示了如何利用位图进行内存资源的跟踪和分配。同时,文章还阐述了内存池的初始化、内存的划分以及分配策略,包括内核和用户内存池的管理。整个过程揭示了操作系统内存管理的底层逻辑。
摘要由CSDN通过智能技术生成

这一章主要包括:

  1. makefile语法,方便后面编译文件
  2. ASSERT断言
  3. Bitmap位图实现
  4. 内存管理系统是实现(最复杂的部分)

一、makefile

基本语法

  1. 目标文件是指此规则下想要生成的文件,可以是.o结尾的目标文件,也可以是可执行文件,也可以是伪目标
  2. 依赖文件是指要生成此规则中的目标文件,需要哪些文件
  3. 命令是指要执行的动作,shell命令。每一条命令单独占用一行
目标文件:依赖文件
[Tab]命令

文件的3个时间

  • atime:access time,每次文件被访问都会更新
  • ctime:change time,文件属性或数据被改变时更新
  • mtime:modify time,文件属性被修改时更新

makefile默认参数优先级
GNUmakefile > makefile > Makefile

伪目标
设置伪目标".PHONY",无论是否同名都会执行

.PHONY:clean
clean:
    rm ./build/*.o
用法make clean

自变量
变量定义的格式:变量名=值(字符串)。引用变量 $(变量名)

系统变量
在这里插入图片描述
注释
使用#号进行注释

隐含规则
x.o文件依赖于x.c文件生成,默认命令为:

$(cc) -c $(CPPFLAGS) $(CFLAGS)

自动化变量

  • $@:表示规则中的目标文件名集合,如果存在多个目标文件,$@表示其中每个文件名
  • $<:表示规则中依赖文件中的第一个文件
  • $^:表示规则中所有依赖文件的集合
  • $?:表示规则中,所有比目标文件Mtim更新的依赖文件集合
    模式规则
    类似正则表达式
#表示所有的.o文件都依赖于对应的.c文件并执行下面的命令
%.o:%.c
    gcc -c -o $@ $^

二、实现ASSERT断言

为什么要断言?

  1. 判断允许到某处时数据是否和预想的一样,如果不同就退出,接下去运行也是错的
  2. 减少其他信息的干扰,输出错误信息

debug.h文件,定义了ASSERT,当未开启NDEBUG时,调用的是((void)0)就是什么都没做。开启时会将对条件进行判断,false输出信息

#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
    #define ASSERT(CONDITION) ((void)0)
#else
    #define ASSERT(CONDITION)\
        if(CONDITION){}else{\
            PANIC(#CONDITION);\
        }
#endif
#endif

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("func:");put_str((char*)func);put_str("\n");
    put_str("condition:");put_str((char*)condition);put_str("\n");
    while(1);
}

三、Bitmap位图实现

位图:bitmap,广泛用于资源管理,是一种管理资源的方式、手段。每一个bit和一个资源相联系。
在内存管理中,就是将每一位和4kb的一个页绑定,0表示页空闲,1表示页被使用了,方便后面的使用。

关键结构体bitmap,btmp_bytes_len表示位图大小,bits指向真实位图

struct bitmap{
    uint32_t btmp_bytes_len;
    uint8_t* bits;
};

bitmap的各种操作
bitmap_scan:获取连续的cnt个空位。(实现方法不错,反正我是想不出来)

#include "bitmap.h"
#include "stdint.h"
#include "string.h"
#include "print.h"
#include "interrupt.h"
#include "debug.h"

//初始化bitmap
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[bit_idx]&(BITMAP_MASK<<bit_odd));
}
//在位图中哦内连续申请cnt个位,成功则放回其下标
int bitmap_scan(struct bitmap* btmp,uint32_t cnt){
    uint32_t idx_byte = 0;
    while((0xff == btmp->bits[idx_byte]) && (idx_byte<btmp->btmp_bytes_len)){
        idx_byte++;//表示没有空位
    }
    ASSERT(idx_byte < btmp->btmp_bytes_len);
    if(idx_byte == btmp->btmp_bytes_len){
        return -1;//找不到空位
    }
    //若在位图内找到空闲为,返回空闲位的索引
    int idx_bit = 0;
    while((uint8_t)(BITMAP_MASK<<idx_bit)&btmp->bits[idx_byte]){
        idx_bit++;
    }
    int bit_idx_start=idx_byte*8+idx_bit;//空闲位在位图的下标
    if(cnt==1){
        return bit_idx_start;
    }
    uint32_t bit_left = (btmp->btmp_bytes_len*8-bit_idx_start);
    uint32_t next_bit = bit_idx_start+1;
    uint32_t count = 1;
    bit_idx_start = -1;
    while(bit_left-- >0){
        if(!(bitmap_scan_test(btmp,next_bit))){
            count++;
        }else{
            count = 0;
        }
        if(count == cnt){
            bit_idx_start = next_bit - cnt + 1;
            break;
        }
        next_bit++;
    }
    return bit_idx_start;
}

void bitmap_set(struct bitmap* btmp,uint32_t bit_idx,int8_t value){
    ASSERT((value==0)||(value==1));
    uint32_t byte_idx = bit_idx/8;
    uint32_t bit_odd = bit_idx%8;
    if(value){
        btmp->bits[byte_idx]|=(BITMAP_MASK<<bit_odd);
    }else{
        //0
        btmp->bits[byte_idx]&=~(BITMAP_MASK<<bit_odd);
    }
}

四、内存管理系统

还是先问自己几个问题:

  1. 内存用什么进行管理
  2. 内存如何划分
  3. 管理内存的结构如何实现?放在什么位置?
  4. 如何管理分配内存

1.管理内存的结构 — 内存池

为了方便实现将全部的内存被分成两半:一半是内核内存池,另一半是用户内存池。
除了内核内存池和用户内存池这两个用于管理物理地址空间的结构,同时还有一个用于管理虚拟内存的虚拟地址池。
两者的区别在于虚拟地址没有内存池字节容量,因为虚拟内存池大小是虚拟的,用在保障同一个进程中虚拟地址不冲突,所以并不用在意大小。但是内核内存池和用户内存池表示的是物理内存,需要知道确切的大小。

/*虚拟地址池,用于虚拟地址管理*/
struct virtual_addr{
    struct bitmap vaddr_bitmap; //虚拟地址用到的位图结构
    uint32_t vaddr_start;       //虚拟地址起始地址
};
/* 内存池结构,生成两个实例用于管理内核内存池和用户内存池 */
struct pool {
   struct bitmap pool_bitmap;	 // 本内存池用到的位图结构,用于管理物理内存
   uint32_t phy_addr_start;	 // 本内存池所管理物理内存的起始地址
   uint32_t pool_size;		 // 本内存池字节容量
};

2.内存如何划分

先回顾一下我们已经用了哪些内存。
低1MB用于显存和内核代码。
0x100000~0x1fffff,这1MB的内容用于页目录项和页表。

  • 页目录在0x100000-0x101000的4KB中
  • 页目录的0和768项指向的第一个页表在0x101000-0x102000的4kb中
  • 页目录的769到1022需要254个页表
  • 最后一个页目录项1023,指向页目录表

综上:内核占了1MB的空间,页目录和页表需要1MB的空间,bochsrc.disk中设置了32MB的内存,所以内核内存池和用户内存池瓜分了高2MB以上的空间。

3.管理内存的结构如何实现?放在什么位置?

这里将内存的位图放在0xc009a000处,离栈顶的0xc009f000为5KB,可以使用4KB空间,最大可记录512MB的内存使用情况。
下面是简化流程

  1. 计算可用内存=总内存-2MB。分配内核内存和用户内存大小和可用页数
  2. 设置内核内存池和用户内存池的长度
  3. 设置内核内存池起始=used_mem(在这里为0x200000,2MB开始)
  4. 设置用户内存池起始=used_mem+kernel_free_pages * PG_SIZE(2MB+内核已经占用的空间)
  5. 设置内核内存池和用户内存池的bitmap地址
  6. 设置内核虚拟内存池。(暂时只有内核这么一个进程,所以只需要对内核进行设置)

现在就能回答一开始的问题:使用内存池这个结构管理内存,对应的bitmap起始地址在0xc009a000。

/* 初始化内存池 */
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为低端1M内存
   uint32_t free_mem = all_mem - used_mem;
   uint16_t all_free_pages = free_mem / PG_SIZE;		  // 1页为4k,不管总内存是不是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;

/*********    内核内存池和用户内存池位图   ***********
 *   位图是全局的数据,长度不固定。
 *   全局或静态的数组需要在编译时知道其长度,
 *   而我们需要根据总内存大小算出需要多少字节。
 *   所以改为指定一块内存来生成位图.
 *   ************************************************/
// 内核使用的最高地址是0xc009f000,这是主线程的栈地址.(内核的大小预计为70K左右)
// 32M内存占用的位图是2k.内核内存池的位图先定在MEM_BITMAP_BASE(0xc009a000)处.
   kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;
							       
/* 用户内存池的位图紧跟在内核内存池位图之后 */
   user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);

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

4.如何管理分配内存

在分配内存时需要对页目录和页表进行修改,所以需要获取页表和页目录对应的虚拟地址。
为什么这两个方法可以获取到页目录和页表的地址虚拟地址?全都归功于最后的页目录1023指向了页目录,具体为什么可以思考一下。

/* 得到虚拟地址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) {
   /* 0xfffff是用来访问到页表本身所在的地址 */
   uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr) * 4);
   return pde;
}

从虚拟内存池中获取虚拟页(只实现了内核虚拟池)

  1. 使用bitmap_scan获取cnt个虚拟页
  2. 如果获取到了则循环将bitmap中对应的位设置为1
/* 在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;
}

page_table_add函数将虚拟地址和物理地址建立映射。需要设置对应的页目录表和页表

/* 页表中添加虚拟地址_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);    // US=1,RW=1,P=1
      } else {			    //应该不会执行到这,因为上面的ASSERT会先执行。
	 PANIC("pte repeat");
	 *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);      // US=1,RW=1,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);      // US=1,RW=1,P=1
   }
}

palloc在对应的物理内存池中分配一个物理页

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);	// 将此位bit_idx置1
   uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start);
   return (void*)page_phyaddr;
}

真正给内核使用的申请函数

  1. 通过vaddr_get在虚拟内存池中申请虚拟地址
  2. 通过palloc在物理内存池中申请物理页
  3. 过page_table_add将以上得到的虚拟地址和物理地址在页表中完成映射
/* 分配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通
   ***************************************************************/
   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. 通过vaddr_get申请需要大小的内存
  2. 如果申请成功,则循环申请需要的物理内存每次4KB
  3. 每次申请的物理内存和虚拟内存进行映射(修改PDE和PTE)

总结

对于内存管理用到的数据结构有:bitmap,内核物理内存池,用户物理内存池,内核虚拟内存池。

对于内核申请内存分三步

  1. 申请虚拟内存
  2. 循环申请物理内存
  3. 设置PDE和PTE

这里并没有涉及块的链表、块的合并。以后学到了再记录吧。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值