《操作系统真象还原》第八章(下) 内存管理系统
第八章(下) 内存管理系统
本文是对《操作系统真象还原》第八章学习的笔记,欢迎大家一起交流。
在上一节中,我们实现了位图的定义以及相关操作,这节中我们要继续完善内存管理系统,最终实现malloc函数,拆分成两个步骤就是内存池的初始化以及内存分配的实现。
内存池的初始化
本节我们将规划实现三个内存池,分别是
- 内核虚拟内存池
- 内核物理内存池
- 用户物理内存池
事实上,应该还有一个用户虚拟内存池,但这部分内容我们在线程管理部分再进行补充。虚拟内存池规划在虚拟内存中,物理内存池规划在物理内存中,他们之间的关系通过页表进行关联
知识部分
物理内存池的规划
对于物理内存,我们低1MB是已经被使用的,且里面有众多内核需要的数据结构,所以肯定是不能动的,1MB之上又是我们的页目录表和内核页表,一共占256*4KB=1MB,所以低端2MB都不可用,剩下的直接内核用户对半分,如下图:
虚拟内存池的规划
目前我们暂时只对内核虚拟内存池(内核堆区空间)进行规划,在3GB ~ 4GB中间的内核虚拟空间中取一块作为虚拟内存池,其中起始地址为0xc0100000
,也就是绕过低端的1MB内存:
位图的规划存放
既然有物理内存池和虚拟内存池,位图作为管理内存空间的数据结构,当然也要有地方进行存放,这样我们才能从位图中知道哪些内存被使用了,哪些内存块是空闲的,从而进一步管理内存
三种内存池对应着三种位图,由于我们还有一个用户虚拟内存池还暂时没有开辟,因此在这里应该是四种位图
- 负责管理内核物理内存池的位图
- 负责管理用户物理内存池的位图
- 负责管理内核虚拟内存池的位图
- 负责管理用户虚拟内存池的位图
位图作为内存管理的数据结构,它自身也是存在于内存中的,让他自己来管理自己吗?这有点复杂,所以我们把位图放到一个不需要内存管理的内存部分,就是低端1MB,我们在低端1MB找到一个位置放位图。
如下是我们在低1MB内存空间中已经使用的内存
-
0x7c00
~0x7e00
是MBR程序-
0x900
是loader的起始地址-
0x70000
~0x9f000
是我们内核文件的存放位置。0x70000
是内核文件的起始地址,由于0x9fc000
地址以下是我们可用的空间,因此我们选取0x9f000
作为内核文件的最终位置(注意,这也意味着0x9f000
其实就是内核的栈顶指针)
而我们的内核文件在映射完是可以被覆盖的,所以我们把位图放到0x70000-9f000,1页大小位图可管理128MB,已经绰绰有余,不过我们任性点,为了以后的扩展,在此用4页位图管理512MB。
此外,这块内存还需要存储主线程的PCB,还记得我们之前mov esp, 0xc009f000
,所以主线程的PCB存储在0xc009e000-0xc009f000
,所以位图起始地址0xc009e000-4*4KB=0xc009a000
,故位图起始地址如下:0xc009a000
目前低端1MB安排
在网上找了个博主的图,十分详细的画出来低端1MB的安排,如下图所示:
代码部分
我们在这一部分要实现的就是初始化,具体代码如下:
memory.h
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
核心就是虚拟内存池的结构体,两部分是位图结构和起始地址。
memory.c
kernel/memory.c如下:
#include "memory.h"
#include "stdint.h"
#include "print.h"
#define PG_SIZE 4096 // 一页的大小
#define MEM_BITMAP_BASE 0xc009a000 // 这个地址是位图的起始地址,1MB内存布局中,9FBFF是最大一段可用区域的边界,而我们计划这个可用空间最后的位置将来用来
// 放PCB,而PCB占用内存是一个自然页,所以起始地址必须是0xxxx000这种形式,离0x9fbff最近的符合这个形式的地址是0x9f000。我们又为了将来可能的拓展,
// 所以让位图可以支持管理512MB的内存空间,所以预留位图大小为16KB,也就是4页,所以选择0x9a000作为位图的起始地址
// 定义内核堆区起始地址,堆区就是用来进行动态内存分配的地方,咱们的系统内核运行在c00000000开始的1MB虚拟地址空间,所以自然要跨过这个空间,
// 堆区的起始地址并没有跨过256个页表,没关系,反正使用虚拟地址最终都会被我们的页表转换为物理地址,我们建立物理映射的时候,跳过256个页表就行了
#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; // 为kernel与user分别建立物理内存池,让用户进程只能从user内存池获得新的内存空间,
// 以免申请完所有可用空间,内核就不能申请空间了
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; // 已使用内存 = 1MB + 256个页表
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; // 内核物理内存池的位图长度,位图中的一位表示一页,以字节为单位
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; // 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); //管理用户使用的物理内存池的位图起始地址
/******************** 输出内存池信息 **********************/
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");
}
7-14行定义了几个宏变量,分别用来指示页大小,位图起始地址,堆区起始地址,我们在上面已经讲过了
15-20行声明物理内存池结构体,包含位图,起始地址,大小
22-24行声明内核/用户物理内存池,内核虚拟内存池
下面就进入mem_pool_init函数,这也是这小节的核心函数
29-36行先准备初始化所需要的数据,all_mem是总内存,该数值在loader.S
中获取过,存放在0xb00
位置处,然后该数值减去低端1MB,再减去页目录表和内核页表所占用的空间,就是剩余可用的物理内存,然后内核/用户对半分即可
40-41行在准备位图,位图的长度直接用free_pages/8
表示,虽然会丢掉一些,但是也无所谓了
43-65行就是将上述定义的数据赋给内核/用户物理内存池,以进行初始化
78-87行初始化内核虚拟内存池
内存分配
我们要做的就是:
- 在内核虚拟内存池中寻找到空闲的连续page个虚拟页面
- 在内核物理内存池找到page个页面,不一定连续
- 建立他们之间的对应关系
代码部分
memory.h
最新的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);
#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 属性位值, 用户级
/* 内存池标记,用于判断用哪个内存池 */
enum pool_flags {
PF_KERNEL = 1, // 内核内存池
PF_USER = 2 // 用户内存池
};
void* get_kernel_pages(uint32_t pg_cnt);
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt);
void malloc_init(void);
uint32_t* pte_ptr(uint32_t vaddr);
uint32_t* pde_ptr(uint32_t vaddr);
#endif
15-20行定义了页表需要的一些属性
24-27行定义了枚举类型,用于区分内核还是用户的内存池
memory.c
以下是我们新增部分的代码
申请物理/虚拟内存页
/* 在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;
}
/* 在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;
}
这两个函数分别用于申请物理/虚拟内存页,其中虚拟内存页可以连续申请,而物理内存页只可以一个一个申请。
获取pte/pde指针
#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22) // 取前10位, 右移22位, 用于获取页目录索引
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12) // 取中间10位, 右移12位, 用于获取页表索引
/* 得到虚拟地址vaddr对应的pde的指针 */
uint32_t *pde_ptr(uint32_t vaddr)
{
/* 0xfffff是用来访问到页表本身所在的地址 */
uint32_t *pde = (uint32_t *)(0xfffff000 + PDE_IDX(vaddr) * 4);
return pde;
}
/* 得到虚拟地址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;
}
先声明两个宏,分别用于获取高10位和中间10位。
我们要获取pte和pde指针,拿到的肯定也是虚拟地址,还记得我们之前将页目录表的最后一个页目录项放入页目录表的地址吗,就是为了此刻准备的。
为了获取pde指针,我们也要进行两次查表再加上页内偏移获取最终的值,0xfffff000 + PDE_IDX(vaddr) * 4
,故我们这样构造虚拟地址,前20位都是1,所以在查表时就会先查页目录表,然后根据页目录表项指向的二级页表还是页目录表,继续查,最终的物理页框还是页目录表所在页框,一个页目录项长度4,故最终会获取pde的值。
pte指针也一样,0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr) * 4
,借助上面的分析过程,先查页目录表,然后对应二级页表还是页目录表,然后物理页框就到了真正的二级页表,最后加上偏移就可以获取pte的值了
虚拟地址和物理地址进行映射
/* 页表中添加虚拟地址_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
{ // 理论上不会执行到这,因为我们前面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
}
}
首先我们要判断页目录项是否存在,如果存在再看对应页表项是否存在,正常情况下肯定是不存在的,所以我们这里用断言判断一下,*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1)
,然后在对应位置填上值
若页目录项不存在,则去申请一个新的物理页框,然后在对应的pde填上pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1
,然后将物理页框全初始化为0,然后再在对应二级页表的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将以上得到的虚拟地址和物理地址在页表中完成映射
***************************************************************/
uint32_t *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;
}
/* 从内核物理内存池中申请pg_cnt页内存,成功则返回其虚拟地址,失败则返回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;
}
分配的函数很简单了,注释写的很详细,注意的就是要将最终分配的物理页框进行清0操作
结果
main.c如下:
#include "print.h"
#include "init.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_page start vaddr is ");
put_int((uint32_t)addr);
put_str("\n");
while(1);
return 0;
}
我们在main.c里面get_kernel_pages(3)
,申请3个页框
分配的地址是c0100000,这是合理的,正是我们前面说的内核堆区起始地址
这个映射正好绕过了低端1MB,符合预期
再看一下位图,007第三位全1,代表分配了3个,符合预期
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具