前情提要
前面我们实现了一个简单的C库,现在我们将实现一个内存池。
之前我们的内存都是自己规划的,我们需要 0xc0001500
这个地址,就将程序放在哪儿。但是程序多了怎么办?还需要我们自己去一个一个安排位置吗,有一块不要了怎么办,这一块空出来的怎么回收,这些需求就导致了内存池的诞生。
一、内存池概述
在分页机制下程序中的地址都是虚拟地址,虚拟地址的范围取决于地址总线的宽度,咱们是在32位环境下,所以虚拟地址空间为4GB。除了地址空间比较大以外,分页机制的另一个好处是每个任务都有自己的 4GB 虚拟地址空间,也就是各程序中的虚拟地址不会与其他程序冲突,都可以为相同的虚拟地址,不仅用户进程是这样,内核也是。程序中的地址是由链接器在链接过程中分配的,分配之后就不会再变了,运行时按部就班地送上处理器的CS和EIP即可。
但程序(进程、内核线程)在运行过程中也有申请内存的需求,这种动态申请内存一般是指在堆中申请内存,操作系统接受申请后,为进程或内核自己在堆中选择一空闲的虚拟地址,并且找个空闲的物理地址作为此虚拟地址的映射,之后把这个虚拟地址返回给程序。那么问题又来了,哪些虚拟地址是空闲的?如何跟踪它们的?
对于所有任务(包括用户进程、内核)来说,它们都有各自4GB的虚拟地址空间,因此需要为所有任务都维护它们自己的虚拟地址池,即一个任务一个。
内核为完成某项工作,也需要申请内存,当然它绝对有能力不通过内存管理系统申请内存,先斩后奏,或者奏都不奏一声,直接拿来就用。当然,这种“王者之风”显然不是那么和谐,我们让内核也通过内存管理系统申请内存,为此,它也要有个虚拟地址池,当它申请内存时,从内核自己的虚拟地址池中分配虚拟地址,再从内核物理内存池(内核专用)中分配物理内存,然后在内核自己的页表将这两种地址建立好映射关系。
对用户进程来说,它向内存管理系统,即操作系统,申请内存时,操作系统先从用户进程自己的虚拟地址池中分配空闲虚拟地址,然后再从用户物理内存池(所有用户进程共享)中分配空闲的物理内存,然后在该用户进程自己的页表将这两种地址建立好映射关系。
二、接口与实现
2.1、先看接口
// 一个页框的大小,4KB
#define PG_SIZE 4096
// 位图地址
#define MEM_BITMAP_BASE 0xc009a000
// 0x100000意指跨过低端1M内存,也就是低端的1MB随我们折腾了
#define K_HEAP_START 0xc0100000
#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 属性位值, 用户级
/* 内存池标记,用于判断用哪个内存池 */
void malloc_init(void);
enum pool_flags {
PF_KERNEL = 1, // 内核内存池
PF_USER = 2 // 用户内存池
};
/* 内存池结构 */
struct pool {
struct bitmap pool_bitmap; // 本内存池用到的位图结构,用于管理物理内存
uint32_t phy_addr_start; // 本内存池所管理物理内存的起始地址
uint32_t pool_size; // 本内存池字节容量
};
/* 用于虚拟地址管理,虚拟地址池 */
struct virtual_addr {
/* 虚拟地址用到的位图结构,用于记录哪些虚拟地址被占用了。以页为单位。*/
struct bitmap vaddr_bitmap;
/* 管理的虚拟地址 */
uint32_t vaddr_start;
};
extern struct pool kernel_pool, user_pool;
/* 得到虚拟地址vaddr对应的pte指针*/
uint32_t* pte_ptr(uint32_t vaddr);
/* 得到虚拟地址vaddr对应的pde指针*/
uint32_t* pde_ptr(uint32_t vaddr);
/* 内存管理部分初始化 */
void mem_init(void);
/* 从内核物理内存池中申请pg_cnt页内存,成功则返回其虚拟地址,失败则返回NULL */
void* get_kernel_pages(uint32_t pg_cnt);
/* 分配pg_cnt个页空间,成功则返回起始虚拟地址,失败时返回NULL */
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt);
有一个值 0xc009a000
,这个地址是位图的地址。为什么放到这里呢?这就需要我们梳理一下现在内存的分布了
首先是低端 1MB
0xc0000400 ~ 0xc0000500
: BIOS的数据区,包含系统启动时的一些信息
0xc0001500~0xc0099FFF
内核空间的起始,这里大概有610KB的空间,我们的内核也就这么大
0xc009a000~0xc009dfff
物理内存的位图,这里是16KB = 131072bit 可以管理512MB物理内存
0xc009e000~0xc009efff
main线程的pcb,这个还没讲
0xc009f000
main线程的栈,也是main线程pcb的最高地址
0xc009FC00~0xc00A0000
: BIOS数据扩展区
0xc000A0000~0xc00100000
:用于 VGA 显存、BIOS 映射
看一看出来,这一块在低端的1MB内存中,属于比较靠上的部分了
2.2、首先先来个难的
看两个函数
// 拿到addr的前10bit,也就是页目录的索引
#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)
// 拿到addr的中10bit,也就是页表的索引
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)
/* 得到虚拟地址vaddr对应的pte指针*/
uint32_t* pte_ptr(uint32_t vaddr) {
uint32_t* pte = (uint32_t*)(0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr) * 4);
return pte;
}
/* 得到虚拟地址vaddr对应的pde的指针 */
uint32_t* pde_ptr(uint32_t vaddr) {
uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr) * 4);
return pde;
}
上面两个宏比较好说,下面这两个函数就难了。
先看第二个,找到vaddr对应的pde指针,也就是找到指向这个虚拟地址的pde的地址,这个是为了修改vaddr对应的页目录而创建的函数。
还记得分页机制嘛,我们是怎么做的,
页目录最后一项是指向自己的物理地址。所以为了能修改自己,第一步是将访问地址前十bit全部置为1
现在页目录被当做了页表,为了能够修改自己,把自己当做内存对待,需要将访问地址中间10bit全部置为1
现在页目录被当做了找到的一个普通页框,修改自己只需要索引就好,索引就是vaddr的前10bit
所以访问vaddr对应的pde就是访问 (0xfffff000) + PDE_IDX(vaddr) * 4
这个地址
再看第一个,得到虚拟地址vaddr对应的pte指针
第一步、访问页目录表,将页目录表当做页表处理,这一步我们已经熟悉了,就是将前10位置1
第二步、找到页表,由于现在的页目录被当做了页表,想要找到真正的页表需要将中十位置为vaddr的前十位
第三步、在页表中找到pte,拿到vaddr的中间十位,乘以4作为pte指针的最后十二位
2.3、获得物理内存容量
如何获得物理内存容量我们在前面的章节已经讲过了,这里不在赘述,不过如何拿到这个地址呢?我们看一下loader的代码
其中total_mem_bytes
被保存在了一条指令和一些数据后面。我们计算一下这些有多少
首先是gdt,一共64个描述符,每个描述符是8位
然后是lgdtr,一共是6位
但是还有一个跳转指令,我们不知道是多少字节,但是bochs可以看到gdt的起始地址如下图
这里给出的地址是 0xc0000603
,所以保存内存的地址就是0xc0000603 + 64*8 + 6 = 0xc0000809
,也可以看一下这里的数据
其单位为Byte,小端字节序,所以C语言读入后是 0x2000000
2.4、整体代码
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;
}
/* 在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;
}
/* 页表中添加虚拟地址_vaddr与物理地址_page_phyaddr的映射 */
static void page_table_add(void* _vaddr, void* _page_phyaddr) {
uint32_t vaddr = (uint32_t)_vaddr;
uint32_t page_phyaddr = (uint32_t)_page_phyaddr;
uint32_t* pde = pde_ptr(vaddr);
uint32_t* pte = pte_ptr(vaddr);
/* 先在页目录内判断目录项的P位,若为1,则表示该表已存在 */
if (*pde & 0x00000001) { // 页目录项和页表项的第0位为P,此处判断目录项是否存在
ASSERT(!(*pte & 0x00000001)); // 确保pte的最后一位为0,也就是这一位并未使用
*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绑定到申请的物理内存上
memset((void*)((int)pte & 0xfffff000), 0, PG_SIZE); // 将申请到的物理内存全部清0,避免旧数据的影响
ASSERT(!(*pte & 0x00000001)); // 确保pte的最后一位为0,也就是这一位并未使用
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1); // 写入pte完成绑定
}
}
/* 分配pg_cnt个页空间,成功则返回起始虚拟地址,失败时返回NULL */
void* malloc_page(enum pool_flags pf, uint32_t pg_cnt) {
if (pg_cnt < 0 && pg_cnt > 3840) {
put_str("malloc_page error: pg_cnt error!\n");
return NULL;
}
void* vaddr_start = vaddr_get(pf, pg_cnt);
if (vaddr_start == NULL) {
put_str("malloc_page error: vaddr_get error!\n");
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);
// 失败时要将曾经已申请的虚拟地址和物理页全部回滚 #TODO
if (page_phyaddr == NULL) {
put_str("malloc_page error: palloc error!\n");
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) {
memset(vaddr, 0, pg_cnt * PG_SIZE);
} else {
put_str("get_kernel_pages error: vaddr error!\n");
}
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; // 使用的内存,也就是低端1MB+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; // 内核空间bitmap的长度
uint32_t ubm_length = user_free_pages / 8; // 用户空间bitmap的长度
uint32_t kp_start = used_mem; // 内核空间的起始地址
uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE; // 用户空间的起始地址
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; // 用户内存池位图的长度
kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE; // 内核内存池位图的起始地址
user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length); // 用户内存池位图的起始地址
bitmap_init(&kernel_pool.pool_bitmap); // 内核内存池位图初始化
bitmap_init(&user_pool.pool_bitmap); // 用户内存池位图初始化
/******************** 输出内核内存池和用户内存池信息(物理地址) **********************/
put_str("--------kernel_pool_bitmap_start:");put_hex((int)kernel_pool.pool_bitmap.bits);
put_str("kernel_pool_phy_addr_start:");put_hex(kernel_pool.phy_addr_start);put_str("\n");
put_str("--------user_pool_bitmap_start:");put_hex((int)user_pool.pool_bitmap.bits);
put_str("user_pool_phy_addr_start:");put_hex(user_pool.phy_addr_start);put_str("\n");
/* 下面初始化内核虚拟地址的位图,按实际物理内存大小生成数组。*/
kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length;
/* 位图的数组指向一块未使用的内存,目前定位在内核内存池和用户内存池之外*/
kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);
/* 虚拟地址跨过低端的1MB以及页表的1MB */
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*)(0xc0000809));
mem_pool_init(mem_bytes_total); // 初始化内存池
put_str("mem_init done\n");
}
整体代码就比较简单了,就是两个物理内存池(内核物理内存池,用户物理内存池)的初始化,以及一些申请内存的函数,这里最难得还是修改页目录与页表实现虚拟内存与物理内存的映射。
三、仿真
结束语
本章我们实现了用户和内核的地址分配,下一章,我们将实现内核级线程
老规矩,代码地址 https://github.com/lyajpunov/os.git