代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。
比如在C 语言下是用malloc函数向操作系统申请内存的,此函数可以可以申请任意字节尺寸的内存。
回忆一下,32 位虚拟地址的转换过程:
- 高10位是页目录项pde的索引,用于在页目录表中定位pde,细节是处理器获取高10位后自动将其乘以4,再加上页目录表的物理地址,这样便得到了pde索引对应的pde所在的物理地址,然后自动在该物理地址中,即该pde中,获取保存的页表物理地址。
- 中间 10位是页表项 pte的索引,用于在页表中定位 pte。细节是处理器获取中间10 位后自动将其乘以4,再加上第一步中得到的页表的物理地址,这样便得到了pte索引对应的pte所在的物理地址,然后自动在该物理地址(该pte)中获取保存的普通物理页的物理地址。
- 低 12 位是物理页内的偏移量,页大小是 4KB,12 位可寻址的范围正好是 4KB,因此处理器便直接把低12位作为第二步中获取的物理页的偏移量,无需乘以4。用物理页的物理地址加上这低12位的和便是这32位虚拟地址最终落向的物理地址。
页表的作用是将虚拟地址转换成物理地址,其转换过程中涉及访问的页目录表、页目录项及页表项,都是通过真实物理地址访问的,否则若用虚拟地址访问它们的话,会陷入转换的死循环。
这里要做的是实现任意内存分配的基础部分,先支持一次分配n个页的内存,即 n*4096 字节。
/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; //虚拟地址起始地址
};
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属性位值,系统级:只允许特权级0、1、2的程序访问
#define PG_US_U 4 //U/S属性位值,用户级:允许所有特权级的进程访问
extern struct pool kernel_pool, user_pool;
static void* vaddr_get(enum pool_flags pf,uint32_t pg_cnt);
/*得到虚拟地址vaddr对应的pte指针*/
uint32_t* pte_ptr(uint32_t vaddr);
/*得到虚拟地址vaddr对应的pde指针*/
uint32_t* pde_ptr(uint32_t vaddr);
/*在m_pool指向的物理内存池中分配 一个物理页,成功则返回页框物理地址,失败则返回NULL*/
static void* palloc(struct pool* m_pool);
/*页表中添加虚拟地址_vaddr与物理地址_page_phyadddr的映射*/
static void page_table_add(void* _vaddr,void* _page_phyaddr);
/*分配pg_cnt个页空间,成功则返回起始虚拟地址,失败则返回NULL*/
void* malloc_page(enum pool_flags pf,uint32_t pg_cnt);
/*在内核物理池中申请一页内存,成功则返回其虚拟地址,失败返回NULL*/
void* get_kernel_pages(uint32_t pg_cnt);
//初始化内存池
static void mem_pool_init(uint32_t all_mem);
/*内存管理部分初始化入口*/
void mem_init(void);
#endif先看memory.h的变化
这里新增枚举结构 enum pool_flags 用来区分内存池。
enum pool_flags{
PF_KERNEL = 1, //内核内存池
PF_USER = 2 //用户内存池
};PF_KERNEL 值为 1, 它代表内核物理内存池。
PF_USER值为2,它代表用户物理内存池。
为了修改页表,必涉及到页表项及页目录项的操作,因此又在memory.h中定义了一些PG_开头的宏,这是页表项或页目录项的属性。
#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属性位值,系统级:只允许特权级0、1、2的程序访问 #define PG_US_U 4 //U/S属性位值,用户级:允许所有特权级的进程访问
以下所说的页内存表示页表或普通物理页。
PG前缀表示页表项或页目录项,US表示第2位的US位,RW表示第1位的RW位,P表示第0位的P位。
PG_P_1表示P位的值为1,表示此页内存已存在。
PG_P_0 表示 P 位的值为0,表示此页内存不存在。
PG_RW_w表示RW位的值为w,即RW=1,表示此页内存允许读、写、执行。
PG_RW_R表示RW位的值为R,即RW=0,表示此页内存允许读、执行。
PG_US_S表示US位的值为S,即US=0,表示只允许特权级别为0、1、2的程序访问此页内存3 特权级程序不被允许。
PG_US_U表示 US位的值为U,即US=1,表示允许所有特权级别程序访问此页内存。
以上各属性的值是以它们的位次来定义的,并不是0或1,这样方便后面的页表项或页目录项的属性合成。
剩下的就是我们要在memory.c要实现的函数。
/kernel/memory.c修改如下:
#include "memory.h"
#include "bitmap.h"
#include "stdint.h"
#include "global.h"
#include "debug.h"
#include "print.h"
#include "string.h"
#define PG_SIZE 4096
//4KB
#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)
/*位图地址
因为0xc009_f000是内核主线程栈顶,0xc009_e000是内核主线程的pcb。
一个页框大小的位图可表示128MB内存,位图位置安排在地址0xc009_a000,
这样本系统最大支持4个页框的位图,即512MB
*/
#define MEM_BITMAP_BASE 0xc009a000
/****************/
/*0xc000_0000是内核从虚拟地址3G起
0x10_0000是指跨过低端1MB内存,使虚拟地址在逻辑上连续*/
#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; //用来给内核分配虚拟地址
/*在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){
//在位图中申请pg_cnt位
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;
}
// /*得到虚拟地址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;
}
/*在m_pool指向的物理内存池中分配 一个物理页,成功则返回页框物理地址,失败则返回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);
uint32_t page_phyaddr = m_pool->phy_addr_start + (bit_idx*PG_SIZE);
return (void*)page_phyaddr;
}
// /*页表中添加虚拟地址_vaddr与物理地址_page_phyadddr的映射*/
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* pte = pte_ptr(vaddr);
uint32_t* pde = pde_ptr(vaddr);
/*要确保pde创建完成后才能执行pte*/
if(*pde & 0x00000001){
//如果pde存在,则将pte指向物理地址
ASSERT(!(*pte & 0x00000001)); //pte理应不存在
if(!(*pte & 0x00000001)){
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
}else{
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);
//pte清零
memset((void*)((int)pte & 0xfffff000),0,PG_SIZE); //pte & 0xfffff000表示新页表的起始地址,将整页清零
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将两个地址进行映射*/
//1.在虚拟内存池中申请
void* vaddr_start = vaddr_get(pf,pg_cnt);
if(vaddr_start == NULL)
return NULL;
uint32_t vaddr = (uint32_t)vaddr_start;
uint32_t cnt = (uint32_t)pg_cnt;
struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
//2.虚拟内存是连续的,物理内存不是,需要逐个映射
while (cnt--){
void* page_phyaddr = palloc(mem_pool); //申请一个物理地址
if(page_phyaddr == NULL)
return NULL;
//3.在页表中做映射
page_table_add((void*)vaddr,page_phyaddr);
vaddr += PG_SIZE; //下一个虚拟页
}
return vaddr_start;
}
// /*在内核物理池中申请一页内存,成功则返回其虚拟地址,失败返回NULL*/
void* get_kernel_pages(uint32_t pg_cnt){
void* vaddr = malloc_page(PF_KERNEL,pg_cnt);
if(vaddr == NULL)
return NULL;
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; //0x10_0000是指跨过低端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;
uint32_t ubm_length = user_free_pages / 8;
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);
/*输出内存池信息*/
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((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");
}解释一下上面新增的代码:
//4KB #define PDE_IDX(addr) ((addr & 0xffc00000) >> 22) #define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)
PDE_IDX用于返回虚拟地址的高10位,即pde索引部分,此部分用于在页目录表中定位 pde。
PTE_IDX用于返回虚拟地址的中间10位,即pte索引部分,此部分用于在页表中定位pte.
接下来就是vaddr_get函数
/*在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){
//在位图中申请pg_cnt位
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;
}vaddr_get函数接受两个参数,pf是内存池的flag,就是在头文件中定义的enum pool_flags。pg_ent是页数,函数的功能是在pf表示的虚拟内存池中申请pg_cnt个虚拟页,若申请成功,则返回虚拟页的起始地址,失败时,则返回NULL。
vaddr_start用于存储分配的起始虚拟地址,bit_idx_start用于存储位图扫描函数bitmap_scan的返回值,默认为-1。
接下来会判断pf的值,如果其等于PF_KERNEL,便认为是在内核虚拟地址池中申请地址,于是调用bitmap_scan函数扫描内核虚拟地址池中的位图。若bitmap_scan返回-1,则vaddr_get函数返回NULL。
由于目前只是试探着扫描了位图,并未将位图中的相应位置1,所以在后面用while循环,根据申请的页数量,即pg_cnt的值,逐次调用bitmap_set函数将相应位置1。
将位图置1之后,现在需要将bit_idx_start转换为虚拟地址,如代码”vaddr_start_ kernel-vaddr.vaddr_start + bit_idx_start * PG_SIZE”,因为位图中的一位代表实际1页大小的内存,所以转换原理还是很简单的,就是用虚拟内存池的起始地址kernel_vaddr.vaddr_start加上起始位索引bit_idx_start相对于内存池的虚拟页偏移地址bit_idx_start * PG_SIZE。下面的else对应的部分是用户内存池暂时未实现。
最后用”return (void*)vaddr_start”将vaddr_star转换成指针后返回。
接下来是pte_ptr函数。它接受一个参数vaddr,功能是得到地址vaddr所在pte的指针,指针的值也就是虚拟地址,故此函数实际返回的是能够访问vaddr所在pte的虚拟地址。
// /*得到虚拟地址vaddr对应的pte指针*/
uint32_t* pte_ptr(uint32_t vaddr){
uint32_t* pte = (uint32_t*)(0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr)*4);
return pte;
}此函数涉及到页表操作,之前咱们在loader.S中创建页表的时候,在最后一个页目录项里写的是页目录表自己的物理地址,目的就是为了通过此页目录项编辑页表。
由于此函数的目的是获取地址vaddr所在页表项pte的地址,这说明要访问的内存是页表,访问页表也得需要用内存地址来访问,在分页机制下任何地址都是虚拟地址,因此我们要根据vaddr构造出一新的虚拟地址,暂且称之为new_vaddr,用它来访问vaddr本身所在的pte的物理地址。虚拟地址最终会落到某个物理地址上,我们就是要用虚拟地址访问某个确切的物理地址。
我们要创造的这个新的虚拟地址new_vaddr同样要经过三个步骤的拆分处理,最终落到vaddr自身所在的pte的物理地址上。
就是要想获取pte的地址,必须先访问到页目录表,再通过其中的页目录项pde,找到相应的页表,在页表中才是页表项pte,所以我们需要分别在地址的高10位、中间10位和低12位中填入合适的数,拼凑出满足此要求的新的32位地址 new_vaddr.
拼凑新地址new_vaddr的过程可分为三步:
第一步,先访问到页目录表。
首先,处理器需要高10位来定位pde。
32位地址中,高10位用于定位页目录项,由于最后一个页目录项保存的正是页目录表物理地址,所以我们需要让地址的高10位指向最后一个页目录项,即第1023个pde,这样才能获取页目录表本身的物理地址。
1023换算成十六进制是0x3ff,将其移到高10后,变成0xffc00000。于是,0xffc00000让处理器自动在最后一个pde中取出页目录表物理地址,此处页目录表物理地址为0x100000,我们boot.inc中的配置项PAGE_DIR_TABLE_POS,其值便为0x100000。这是最后一个pde中的物理地址是页目录表地址。但处理器把保存在pde中的地址都视为页表地址,即处理器会把刚刚获得的页目录表当成页表来处理,这此时我们拼凑出了新虚拟地址new_vaddr的高10位。
第二步,找到页表。
其次,处理器需要 pte 索引。
我们需要再凑出中间10位。中间10位是页表项的索引,用来在页表中定位页表项pte。我们在上一步中已经得到了页目录表物理地址(其实处理器把页目录表当成页表了),页表地址保存在页目录项中,因此我们要先想办法访问到vaddr所在的页目录项。此时处理器已经把上一步获得的页目录表当成了页表,其需要的是pte的索引,因此我们把vaddr的pde索引当作处理器视角中的pte索引就行了,现在要做的是将参数vaddr的高10位(pde索引)取出来,做新地址new_vaddr的中间10位(pte索引)。
于是我们先用按位与操作”(vaddr & 0xffc00000)”获取高10位,再将其右移10位,使其变成中间10位,于是就成了处理器眼中的pte索引。
这样在处理器处理新地址new_vaddr的pte索引时,以为接下来获得的是pte中的普通物理页地址,但这只是处理器视角中的情景。由于上一步我们获得的是页目录表地址,并且本步中传给它的pte索引是vaddr中的pde索引,故此时处理器获得的是vaddr中高10位的pde索引所对应的pde里保存的页表的物理地址,并不是pte中保存的普通物理页的物理地址。所以,此时我们获得了vaddr所在的页表物理地址。
此时我们已拼凑出了新虚拟地址new_vaddr的中间10位。
第三步,在页表中找到 pte。
最后,处理器需要地址的低12位。
上一步中处理器认为已经找到了最终的物理页地址,所以它此时需要的是32位地址中的低12位,用该12位作为上一步中获取到的物理页的偏移量,当然,这依然只是处理器的视角。在我们眼里,上一步获得的是页表的物理地址,因此我们只要把vaddr的中间10位转换成处理器眼里的12位长度的页内偏移量就行了。由于地址的低12位寻址范围正好是一页的4KB大小,故处理器直接拿低12位去寻址,不会再为其自动乘以4,因此,咱们得手动将vaddr的pte部分乘4后再交给处理器。
这里的做法是先用PTE_IDX(vaddr)获取vaddr的pte索引,即中间10位,再将其乘4,即PTE_IDX(vaddr) *4拼凑出了新虚拟地址new_vaddr的低12位。
故0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr) *4的结果就是最终的新虚拟地址new_vaddr的完整32位数值,new_vaddr保存在指针变量pte中。
由于此结果仅仅是个整型数值,需要将其通过强制类型转换,即(uint32_t),转换成32位整型地址。此时指针变量pte指向vaddr所在的pte。最后通过return pte将此指针返回。
函数pde_ptr只接受一个参数,它的功能是得到虚拟地址vaddr所在pde的指针,也就是返回能够访问该pde的虚拟地址。用此指针可以访问到虚拟地址vaddr对应的pde。
/*得到虚拟地址vaddr对应的pde指针*/
uint32_t* pde_ptr(uint32_t vaddr){
uint32_t* pde = (uint32_t*)((0xfffff000) + PDE_IDX(vaddr)*4);
return pde;
}访问pde也必须要通过地址,因此我们这里也必须要根据vaddr来构造一个新的32位地址new_vaddr。
根据前面所说的处理器处理 32 位地址的三个步骤,处理器先处理的是 32 位地址中高 10 位的pde 索引,其次是中间 10 位的 pte 索引,最后是低 12 位。
由于要访问的是vaddr所在的页目录项pde,所以必须想办法在第2步中让处理器处理pte索引时获得的是页目录表物理地址,然后利用低12位作为物理页的偏移量,此偏移量加上页目录表的物理地址,所得的地址之和便是 vaddr所在的 pde的物理地址。
最后一个页目录项中存储的是页目录表物理地址,故当32位地址中高20位为0xfffff时,这就表示访问到的是最后一个页目录项,即获得了页目录表物理地址。0xfffffxxx的高10位是0x3ff,中间10位也是0x3ff,也就是处理pde索引时得到的是页目录表的物理地址,此时处理器以为此页目录表就是页表,继续用pte索引在该页表(页目录表)找到最后一个页表项pte (其实是页目录项pde),所以再次获得了页目录表物理地址(当然处理器以为获得的是普通物理页的物理地址)。
新虚拟地址new_vaddr等于0xfffff000再加上vaddr的页目录项索引乘以4的积,即”(0xfffff000) +PDE_IDX(vaddr) * 4″。此时的new_vaddr便落到vaddr所在的页目录项pde的物理地址上。由于此结果仅仅是个整型数值,需要将其通过强制类型转换成32位整型指针。最终的新虚拟地址new_vaddr保存在指针变量pde中,因此”pde=(uint32_t*)((0xfffff000) + PDE_IDX(vaddr)*4)”,此时指针变量pde指向了vaddr所在的pde,最后通过return pde将指针返回。
palloc函数功能是在m_pool指向的物理内存池中分配1个物理页,成功时则返回页框的物理地址,失败时则返回NULL。
/*在m_pool指向的物理内存池中分配 一个物理页,成功则返回页框物理地址,失败则返回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);
uint32_t page_phyaddr = m_pool->phy_addr_start + (bit_idx*PG_SIZE);
return (void*)page_phyaddr;
}定义的变量bit_idx用于存储bitmap_scan函数的返回值, bitmap_scan函数在物理内存池的位图中查找可用位,如果失败,则返回-1,因此函数palloc也将返回NULL。若bitmap_scan的返回值不为-1,也就是找到了可用位,接下来再通过函数bitmap_set将bit_idx位设置为1,也就是代码”bitmap_set(&m_pool->pool_bitmap, bit_idx, 1)”。
变量 page_phyaddr 用于保存分配的物理页地址,它的值是物理内存池的起始地址 m_pool-> phy_addr_start+物理页在内存池中的偏移地址(bit_idx * PG_SIZE)。最后通过“return (void*) page_phyaddr”将物理页地址转换成空指针后返回。
函数page_table_add,它接受两个参数,虚拟地址_vaddr和物理地址_page_phyaddr,功能是添加虚拟地址_vaddr与物理地址_page_phyaddr的映射。
// /*页表中添加虚拟地址_vaddr与物理地址_page_phyadddr的映射*/
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* pte = pte_ptr(vaddr);
uint32_t* pde = pde_ptr(vaddr);
/*要确保pde创建完成后才能执行pte*/
if(*pde & 0x00000001){
//如果pde存在,则将pte指向物理地址
ASSERT(!(*pte & 0x00000001)); //pte理应不存在
if(!(*pte & 0x00000001)){
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
}else{
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);
//pte清零
memset((void*)((int)pte & 0xfffff000),0,PG_SIZE); //pte & 0xfffff000表示新页表的起始地址,将整页清零
ASSERT(!(*pte & 0x00000001));
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
}
}虚拟地址和物理地址的映射关系是在页表中完成的,本质上是在页表中添加此虚拟地址对应的页表项pte,并把物理页的物理地址写入此页表项pte中。
也就是说,页表操作会用到上面介绍过的pde_ptr和pte_ptr函数。
函数开头调用了函数pde_ptr(vaddr),以此获取虚拟地址vaddr所在的pde的虚拟地址,此地址用指针变量 pde 保存。接着调用 pte_ptr(vaddr)获得 vaddr 所在的 pte 虚拟地址,此地址保存在指针变量 pte中。pte 隶属于某个页表,而页表地址保存在 pde中。一个 pte代表一个物理页,物理页是4KB大小,一个页表中可支持1024个pte,故一个页表最大支持4MB内存。
由于我们目前已经有了一个页表,故在4MB (0x0~0x3ff000)的范围内新增pte时,只要申请个物理页并将此物理页的物理地址写入新的pte即可,无需再做额外操作。可是,当我们访问的虚拟地址超过了此范围时,比如0x400000,这不仅是添加pte的问题,同时还要申请个物理页来新建页表,同时将用作页表的物理页地址写入页目录表中的第1个页目录项pde中。也就是说,只要新增的虚拟地址是4MB的整数倍时,,就一定要申请两个物理页,一个物理页作为新的页表,同时在页目录表中创建个新的pde,并把此物理页的物理地址写入此pde。另一个物理页作为普通的物理页,同时在新建的页表中添加个新的pte,并把此物理页的物理地址写入此pte。因此,在添加一个页表项pte时,我们务必要判断该pte所在的页表是否存在。
if(*pde & 0x00000001)就是在判断页表项中的P位,如果此位为1,则表示页目录项已存在,不需要再建立。
如果页表已存在,接着if(!(*pte & 0x00000001))判断 pte 是否存在,不过一般申请新的地址时其所对应的 pte不会存在。如果pte不存在,就将物理页的物理地址及相关属性写到此pte中,即代码”*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);”,这样vaddr对应的pte就被映射到物理地址page_phyaddr上,并添加了属性US=1,RW=1,P=1。
如果在上面的判断中发现pde不存在时,需要申请个新的物理页来创建新的页表,因此通过调用”palloc(&kernel_pool)”申请新的物理页并将地址保存在变量pde_phyaddr中。随后将新物理页的物理地址 pde_phyaddr和相关属性写入此 pde中,即代码”*pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);”,属性同样是 US=1,RW=1,P=1。
然后调用memset函数对刚刚申请的物理页初始化为0。
为了初始化物理页,光知道物理页的物理地址是不行的,毕竟在分页机制下一切都是通过虚拟地址来访问的,因此必须得知道该物理页对应的虚拟地址。既然此物理页用作页表,也就是它会被写入某个pde中。
pte 是 vaddr 所在的页表项 pte 的虚拟地址,它就是 pte_ptr 返回的结果 new_vaddr, new_vaddr 的低 12 位是用来在其高20位所定位到的页表中做pte的偏移量,也就是说,如果new_vaddr的低12位为0,访问到的则是页表的起始虚拟地址,而我们要初始化的物理页就是用作此页表。也就是是将new_vaddr的低12位清0,就是说pte的偏移量不要了,而高20位保留,也就是如代码”(int)pte & 0xfffff000″所示,这样就得到了这个vaddr所在页表的虚拟地址,也就是刚刚申请的新物理页的虚拟地址。这样就意味着可以对其用memset清0了,于是用此行代码对该物理页做清0操作,即“memset((void*)((int)pte & 0xfffff000),0,PG_SIZE);”。
接着在本函数的最后,为 vaddr 对应的 pte 赋值,也就是把物理页地址和属性写进去,即“ *pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);”。
malloc_page函数接受2个参数,一个是pf,用来指明内存池,另外一个是pg_cnt,用来指明页数,此函数的功能是在pf所指向的内存池中分配pg_cnt个页,成功则返回起始虚拟地址,失败时返回NULL。
// /*分配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将两个地址进行映射*/
//1.在虚拟内存池中申请
void* vaddr_start = vaddr_get(pf,pg_cnt);
if(vaddr_start == NULL)
return NULL;
uint32_t vaddr = (uint32_t)vaddr_start;
uint32_t cnt = (uint32_t)pg_cnt;
struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
//2.虚拟内存是连续的,物理内存不是,需要逐个映射
while (cnt--){
void* page_phyaddr = palloc(mem_pool); //申请一个物理地址
if(page_phyaddr == NULL)
return NULL;
//3.在页表中做映射
page_table_add((void*)vaddr,page_phyaddr);
vaddr += PG_SIZE; //下一个虚拟页
}
return vaddr_start;
}在函数的开头,有一句”ASSERT(pg_cnt > 0 && pg_cnt < 3840);”,它用来监督申请的内存页数pg_ent是否超过了物理内存池的容量。
内核和用户空间各约16MB空间,保守起见用15MB来限制,申请的内存页数要小于内存池大小,即pg_cnt<15*1024*1024/4096=3840 页。
其实此函数是申请虚拟地址,然后为此虚拟地址分配物理地址,并在页表中建立好虚拟地址到物理地址的映射。
步骤如下:
- 通过vaddr_get在虚拟内存池中申请虚拟地址。
- 通过palloc在物理内存池中申请物理页。
- 通过page_table_add将以上两步得到的虚拟地址和物理地址在页表中完成映射。
先申请虚拟地址,如果失败就返回NULL,然后判断要用的内存池属于内核,还是用户,下面要在相应的内存池中分配物理页。接着在循环为虚拟页分配物理页并在页表中建立映射关系。调用palloc在相应内存池中申请物理页,物理页地址保存在指针变量page_phyaddr中,如果失败(返回值为NULL)则通过”return NULL”返回。
这里还有些工作没完成,当申请物理页失败时,应该将曾经已申请成功的虚拟地址和物理地址全部回滚,虽然地址还未使用,但虚拟内存池的位图已经被修改了,如果物理内存池的位图也被修改过,还要再把物理地址回滚。由于这部分属于地址回收的功能,待将来实现内存回收时再补充吧。
若物理页申请成功,再调用”page_table_add((void*)vaddr, page_phyaddr)”将虚拟地址vaddr映射为物理地址page_phyaddr,随后通过”vaddr +=PG_SIZE”将vaddr更新为下一个虚拟页,继续下一个循环的申请物理页和页表映射。当处理完 pg_cnt个页后,通过”return vaddr_start”将分配的起始虚拟地址返回。
不过注意,虚拟地址是连续的,但物理地址可能连续,也可能不连续,因此第1步中可以一次性申请pg_ent个虚拟页。成功申请之后,根据申请的页数,通过循环依次为每一个虚拟页申请物理页,再将它们在页表中依次映射关联。
最后一个函数是get_kernel_pages,它只接受一个参数申请的页数pg_cnt,函数功能是从内核物理内存池中申请 pg_cnt 页内存,成功则返回其虚拟地址,失败则返回 NULL。
// /*在内核物理池中申请一页内存,成功则返回其虚拟地址,失败返回NULL*/
void* get_kernel_pages(uint32_t pg_cnt){
void* vaddr = malloc_page(PF_KERNEL,pg_cnt);
if(vaddr == NULL)
return NULL;
memset(vaddr,0,pg_cnt*PG_SIZE); //把虚拟地址对应的物理地址清零
return vaddr;
}在其内部是调用 malloc_page来实现的,返回的虚拟地址保存在变量vaddr中,之后再通过memset将此页清0。由于malloc_page返回的是虚拟地址,因此可以直接将vaddr作为memset的参数,由于虚拟地址是连续的,所以置0的字节数直接用pg_cnt乘以PG_SIZE。
申请成功通过return vaddr返回分配的虚拟地址。
然后我们修改/kernel/main.c测试一下
#include "print.h"
#include "init.h"
#include "debug.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;
}
结果如下:

info tab 查看了页表中虚拟地址与物理地址的映射关系,在其后面的最大的长方框中是映射关系,左边是虚拟地址的范围,右边是所映射的物理地址。
在bochs的输出中,无论地址是虚拟地址,还是物理地址,只要其是连续的,bochs就会合并到一起输出。因此,虚拟地址范围0xc0100000~0xc0102fff所映射的物理地址范围是0x200000~0x202fff。
用page指令分别对各个虚拟页查看其映射到的物理页。
x 0xc009a000查看了内核内存池的位图所在地址,其内容是0x00000007,这说明低3位都是1,这是因为咱们申请了3个页,位图的变化与预期符合。

参考
郑钢著操作系统真象还原
田宇著一个64位操作系统的设计与实现
丁渊著ORANGE’S:一个操作系统的实现


