Ming's Blog

Back

操作系统真象还原笔记——内存管理Blur image

位图

miniOS运行在32位虚拟机上,对应4G内存,因此采用 Sv32 模式管理内存,即10-10-12二级分页管理。而对于内存分配,采用位图方法管理,总计4页位图可管理512MB物理内存,对于内存碎片则暂时略过,其中,划分出1页内存用于提供给 arena 分配内存。

首先是用于内存管理的各类结构体。

内存池结构

"lib/kernel/bitmap.h"

struct bitmap {
   uint32_t btmp_bytes_len;
/* 在遍历位图时,整体上以字节为单位,细节上是以位为单位,所以此处位图的指针必须是单字节 */
   uint8_t* bits;
};
c
"kernel/memory.c"

/* 内存池结构,生成两个实例用于管理内核内存池和用户内存池 */
struct pool {
   struct bitmap pool_bitmap;	 // 本内存池用到的位图结构,用于管理物理内存
   uint32_t phy_addr_start;	 // 本内存池所管理物理内存的起始地址
   uint32_t pool_size;		 // 本内存池字节容量
   struct lock lock;		 // 申请内存时互斥
};
c

初始化

我们首先从初始化入手,总览miniOS对内存的布局。

/* 内存管理部分初始化入口 */
void mem_init() {
   put_str("mem_init start\n");
   uint32_t mem_bytes_total = (*(uint32_t*)(0xb00));
   mem_pool_init(mem_bytes_total);	  // 初始化内存池
/* 初始化mem_block_desc数组descs,为malloc做准备 */
   block_desc_init(k_block_descs);
   put_str("mem_init done\n");
}
c

可以看到内存管理的初始化大致分为三个部分:

  1. 初始化内存池。
  2. 初始化 mem_block_desc 数组 descs,用以提供给 arena 管理内存。

我们依次对其分析。

mem_pool_init

  1. 基本参数的设置:
    • 首先对页表所占内存大小进行计算,根据页表本身所占空间计算出 page_table_size 加上低端内存0x100000(该位置包含了位图本身)得到 used_mem 进而用总空间减去得到 all_free_pages ,其中 kernel_free_pageskernel_free_pages 各占一半。
    • kbm_lengthubm_length 分别代表内核空间和用户空间的bitmap长度,其中位图管理以字节为单位,字节中每一位代表一个页表。
    • kp_startup_start分别代表内核空间和用户空间的起始地址,up_start 等于 kp_start 加上内核空间大小。
    • 内核与用户空间对应的 pool_size 为对应可用页数* PG_SIZE
  2. 之后设置内核与用户内存池的起始位置为MEM_BITMAP_BASEMEM_BITMAP_BASE + kbm_length
  3. 初始化内核虚拟地址位图大小 为kbm_length 与首地址为MEM_BITMAP_BASE + kbm_length + ubm_length
  4. 设置内核虚拟起始地址为K_HEAP_START 即0xc0100000。

地址分配

当分配内存时,内存管理单元首先分配虚拟地址,再分配物理地址,最后完成页表的地址映射。

位图管理

虚拟地址分配

  1. 首先对传入的 pf 做判断,若是 PF_KERNEL 则直接分配 pg_cnt 页内存——先检查是否有足够空间,若足够则循环分配,每次分配1页,直至到目标数量。之后返回分配内存的首地址 vaddr_start
  2. 若是内存虚拟内存池,则先用 running_thread() 获取当前进程 task_struct 指针 cur,再在 cur` 对应的虚拟内存池中分配内存。

物理地址分配

/* 在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);	// 将此位bit_idx置1
   uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start);
   return (void*)page_phyaddr;
}
c
  1. bitmap_scan 找一个空闲页面。
  2. bitmap_set 将对应位图位置1。

地址映射

  1. pde_ptr() 通过计算 0xfffff000(4GB内存最后一页,为页目录表最后一项,存储了页表基地址)和索引 * 4(页目录项大小为4字节)之和,获得指向 vaddr 对应页目录项的虚拟地址。
  2. pte_ptr() 先通过地址 0xffc00000 访问页目录表基地址,(vaddr & 0xffc00000) >> 10) 作为页目录表索引得到页表对应地址,最后用 PTE_IDX 获取索引进而获得指向 vaddr 对应 pte 的指针。
  1. 首先用 pde_ptrpte_ptr 获得对应 pde , pte 指针。
  2. 之后对 pdeP位进行判断:
    • 若存在则通过 pte 指针在页表中写入物理位置并设置页面相关标志位 (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
    • 若不存在则先用 palloc 分配内存并返回 pde,再 memset 清零物理页内存,防止标志位混乱,最后写入物理位置并设置标志位。

综合

  1. malloc_page()
    • vaddr_get() 在虚拟内存池中申请虚拟地址;
    • palloc() 在物理内存池中申请物理页;
    • page_table_add() 将以上两步得到的虚拟地址和物理地址在页表中完成映射。
  2. get_kernel_pages()get_user_pages() 大致相同,上锁并用 malloc_page() 分配内存并清零,返回虚拟地址。

arena

我们将从堆中创建 arena,并给其结构体指针赋予1个页框以上的内存,页框中除了此结构体外的部分都将作为 arena 的内存池区域,该区域会被平均拆分成多个规格大小相等的内存块,即 mem_block,这些 mem_block 会被添加到内存块描述符的 free_list

内存块描述符

/* 内存块 */
struct mem_block {
   struct list_elem free_elem;
};

/* 内存块描述符 */
struct mem_block_desc {
   uint32_t block_size;		 // 内存块大小
   uint32_t blocks_per_arena;	 // 本arena中可容纳此mem_block的数量.
   struct list free_list;	 // 目前可用的mem_block链表
};

#define DESC_CNT 7
c

总共7种内存块描述符,对应 16、32、64、128、256、512、1024 字节。

/* 内存仓库arena元信息 */
struct arena {
   struct mem_block_desc* desc;	 // 此arena关联的mem_block_desc
/* large为true时,cnt表示的是页框数。
 * 否则cnt表示空闲mem_block数量 */
   uint32_t cnt;
   bool large;
};
c
  1. 结构中第1个成员是 desc,它指向本 arena 中的内存块被关联到哪个内存块描述符,同一规格的 arena 只能关联到同一规格的内存块描述符,比如本 arena 中的内存块规格为 64 字节,desc 只能指向规格为 64 字节的内存块描述符。
  2. 第2个成员是 cnt,当 largeture 时,cnt 表示的是本 arena 占用的页框数,否则 largefalse 时,cnt 表示本 arena 中还有多少空闲内存块可用,将来释放内存时要用到此项。

即一个 arena 由多个 mem_block 组成,通过 mem_block_desc 进行管理。

初始化

struct mem_block_desc k_block_descs[DESC_CNT];	// 内核内存块描述符数组

/* 初始化mem_block_desc数组descs,为malloc做准备 */
   block_desc_init(k_block_descs);
c

sys_malloc

arena 本身信息存在它所管理的内存的起始位置。free_list 中存储了每个 mem_blocklist_elem ,分配时通过 elem2entry 获得 mem_block 的首地址。

  1. 先判断是哪个内存池,再到对应内存池操作。
  2. 若要求的 size 大于1024,则直接分配页框并返回。
  3. 对于小的内存要求,用最佳适应去选择内存块大小。
  4. 分配小块内存时,若对应段描述符的块不够,则分配新页并拆分,把每个块的首地址添加到内存块描述符的 free_list 中,维护 arenacnt 值。
  5. 从对应段描述符的 free_list 分配内存块并返回地址。

这里有个疑问,直接返回 mem_block 的首地址不会导致它的 list_elem 被覆盖吗? 事实上是不是应该返回 (void*)(b + sizeof(b->free_elem))

实际上返回 b 即可。struct mem_block 结构里的 list_elem 成员是用来在链表中连接各个内存块时使用的。只有在内存块处于空闲状态(即位于 free_list 中)时,这个 list_elem 成员才有意义。当 mem_block 被分配出去时,这块内存的使用权切换到了用户,list_elem 的区域可以被安全地覆盖,因为它不再在链表中,也不需要作为链表的一部分。

arena2block与block2arena

/* 返回arena中第idx个内存块的地址 */
static struct mem_block* arena2block(struct arena* a, uint32_t idx) {
  return (struct mem_block*)((uint32_t)a + sizeof(struct arena) + idx * a->desc->block_size);
}

/* 返回内存块b所在的arena地址 */
static struct arena* block2arena(struct mem_block* b) {
   return (struct arena*)((uint32_t)b & 0xfffff000);
}
c

malloc

调用 sys_malloc 系统调用实现。

sys_free

其他

vaddr2phyaddr

/* 得到虚拟地址映射到的物理地址 */
uint32_t addr_v2p(uint32_t vaddr) {
   uint32_t* pte = pte_ptr(vaddr);
/* (*pte)的值是页表所在的物理页框地址,
 * 去掉其低12位的页表项属性+虚拟地址vaddr的低12位 */
   return ((*pte & 0xfffff000) + (vaddr & 0x00000fff));
}
c
操作系统真象还原笔记——内存管理
https://astro-pure.js.org/blog/eleos-memory
Author Ming
Published at February 22, 2024
Comment seems to stuck. Try to refresh?✨