MIT 6.828 Lab2 内存管理
JOS 的内存管理中主要包括两部分:内存管理器与虚拟内存,内存单元以 page 为单位,整体图示如下
1. 物理内存管理
操作系统需要记录 page 的使用情况,以便分配与回收,JOS 的实现中以 PageInfo 来记录 page 信息,通过链表来维护空闲 page,其中通过 PageInfo 数组 pages 与 物理页为一对一映射,可通过函数映射计算彼此的地址;
Exercise 1
- boot_alloc
在内存分配器未初始化之前,JOS 使用 boot_alloc 来进行内存的分配,通过一个 static 指针变量 nextfree 来记录内存使用情况;
其中 nextfree
指针指向未被分配的地址空间,通过链接器提供的 end 符号地址进行初始化,内存分配也很简单,就是将 nextfree 指针偏移待分配的大小(参数 n 向上取整),需要注意的是,函数的返回结果是分配内存块的起始地址,分配内存单元可以理解为区间,C 中通过起始地址来标识;
static void *
boot_alloc(uint32_t n)
{
static char *nextfree; // virtual address of next byte of free memory
char *result;
if (!nextfree) {
extern char end[];
nextfree = ROUNDUP((char *) end, PGSIZE);
}
cprintf("nextfree is:%08x,request alloc size:%d\n",nextfree,n);
// LAB 2: Your code here.
char *prev_free = NULL;
if(n==0){
return nextfree;
}else{
//char型指针,Round之后直接相加
prev_free = nextfree;
nextfree+=ROUNDUP(n,PGSIZE);
if(((uint32_t)nextfree-KERNBASE)>(npages*PGSIZE)){
panic("The Requested Address Break Page Table Limitation!");
}
}
//返回起始地址!!
return prev_free;
}
复制代码
- mem_init
通过之前完成的 boot_alloc 来分配 PageInfo 数组,记得清零
pages = (struct PageInfo *)boot_alloc(npages*sizeof(struct PageInfo));
memset(pages,0,npages*sizeof(struct PageInfo));
复制代码
- page_init
page_init 负责完成空闲链表的初始化,按照 hint 进行处理即可
void
page_init(void)
{
size_t i;
int num_alloc = PADDR(boot_alloc(0))/PGSIZE;
cprintf("num_alloc:%d\n",num_alloc);
size_t ext_pgno = ROUNDUP(EXTPHYSMEM,PGSIZE)/PGSIZE;
cprintf("range:(%d,%d],(%d,%d]\n",npages_basemem-1,ext_pgno-1,ext_pgno-1,num_alloc-1);
//通过映射处理边界条件,-1
for (i = 0; i < npages; i++) {
// 1) Mark physical page 0 as in use.
if(i==0){
continue;
// 2)if(i>=1 && i<=npages_basemem) pass // 3)IO hole [IOPHYSMEM, EXTPHYSMEM)
}else if(i>npages_basemem-1 && i<=ext_pgno-1){
continue;
// 4)Then extended memory [EXTPHYSMEM, ...).BIOS与Kernel,以及分配的页表
}else if(i>ext_pgno-1 && i<=num_alloc-1){
continue;
}
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
}
复制代码
- page_alloc
物理页申请,删除空闲链表的头结点,用它来作为分配物理页的表示,需要注意的是此处已开启虚拟内存,内存清零时传入参数要映射为虚拟内存中的表示;
struct PageInfo *
page_alloc(int alloc_flags)
{
if(page_free_list==NULL){
return NULL;
}
//Header
struct PageInfo *cur_page = page_free_list;
page_free_list = cur_page->pp_link;
cur_page->pp_link = NULL;
//初始化,虚拟地址转物理地址
if(alloc_flags & ALLOC_ZERO){
//WARNING,地址转换
memset(page2kva(cur_page),0,PGSIZE);
}
return cur_page;
}
复制代码
- page_free
回收物理页,引用清零,将节点插入至空闲链表中
void
page_free(struct PageInfo *pp)
{
if(pp->pp_ref!=0){
panic("Can't Release Nonzero Page");
return;
}
if(pp->pp_link!=NULL){
panic("MalFormed Page,pp_link should be null");
return;
}
if(pp==NULL){
return;
}
//clear pp_ref
pp->pp_ref = 0;
pp->pp_link = page_free_list;
page_free_list = pp;
}
复制代码
补充资料:
- static变量及其作用,C语言static变量详解 c.biancheng.net/view/301.ht…
2. 虚拟内存
虚拟内存部分需要区分,线性地址,物理地址,虚拟地址,它们之间的关系如下图所示:
- 首先是分段寻址,segment translation and segment-based protection cannot be disabled on the x86,关于分段寻址可以参考 lab1 中 GDT 相关内容,它通过 (selector:offset)二元组来寻址,在未开启虚拟内存前,它可以直接得到物理地址
-
开启虚拟内存后,地址映射部分又加了一层,需要通过页表相关的翻译机制才能得到物理地址,当然也增加了操作系统的灵活性
另外就是 Lab1 中 bootloader 加载内核之后的流程,其中因为链接器将 Kernel 链接至高位地址空间,所以 Entry.S 中开启了虚拟内存,将链接地址映射至内核镜像加载的物理地址;
JOS 配套的 qemu 提供了一些方便debug的命令,使用 ctrl-a c 即可进入
Exercise 4
页表相关的函数,主要是映射关系的增删以及相关的页分配,核心函数为 pgdir_walk
- pgdir_walk
pgdir_walk 负责实现虚拟地址与对应页表项的查找,可以参照向量的加法来进行理解,先找到页目录对应的 pde_t,然后找 pte_t ,页目录的内存空间已经申请完毕,但页表的空间可能未分配,所以需要判断 pde_t 对应的状态位进行判断,然后按需进行页分配,注意 pte_t 中存放的是物理地址,但系统中参数传递使用的是虚拟地址,所以需要进行相关的转换;
//PageInfo转物理地址
page2pa(alloc_pg);
//物理地址转虚拟地址
(pte_t *)KADDR(pa);
复制代码
pte_t * pgdir_walk(pde_t *pgdir, const void *va, int create){
// Fill this function in
uintptr_t pd_index = PDX(va);
uintptr_t pt_index = PTX(va);
uintptr_t offset = PGOFF(va);
pde_t pd_entry = pgdir[pd_index];
size_t invalid = 0;
//填充pde,物理地址与标志位,不能用pd_entry==0来判断,根具体的位|分量来判断
if(!(pd_entry&PTE_P)){
if(create){
struct PageInfo *alloc_pg = page_alloc(ALLOC_ZERO);
if(alloc_pg==NULL){
return NULL;
}
alloc_pg->pp_ref++;
//物理地址
pgdir[pd_index] = page2pa(alloc_pg)|PTE_P|PTE_U|PTE_W;
}else{
return NULL;
}
}
//页目录中的Entry
uintptr_t pa = PTE_ADDR(pgdir[pd_index]);
pte_t *pt_addr = (pte_t *)KADDR(pa);
//PG位开启,物理地址映射虚拟地址
return &pt_addr[pt_index];
}
复制代码
- boot_map_region,page_lookup,page_remove,page_insert
static void boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
//Fill this function in
uintptr_t count = 0;
uintptr_t offset = 0;
for(;count<size/PGSIZE;count++){
offset = count*PGSIZE;
pde_t *cur_pde= pgdir_walk(pgdir,(void *)(va+offset),1);
*cur_pde = (pa+offset) | perm | PTE_P;
}
}
复制代码
struct PageInfo * page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
// Fill this function in
pde_t * target= pgdir_walk(pgdir,va,0);
// PTE
if(!target){
return NULL;
}
if(!(*target & PTE_P)){
return NULL;
}
if(pte_store){
*pte_store = target;
}
physaddr_t ph_addr = PTE_ADDR(*target);
struct PageInfo *res = pa2page(ph_addr);
return res;
}
复制代码
void page_remove(pde_t *pgdir, void *va)
{
// Fill this function in
pte_t *pte_store;
struct PageInfo *res = page_lookup(pgdir,va,&pte_store);
if(!res){
return;
}
*pte_store = 0;
page_decref(res);
tlb_invalidate(pgdir,va);
}
复制代码
int page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
// Fill this function in
pte_t *pt_entry = pgdir_walk(pgdir,va,1);
if(!pt_entry){
return -E_NO_MEM;
}
//可能存在同一地址重复映射的情况,防止误释放
pp->pp_ref++;
// 已有va映射,需要remove
if(*pt_entry&PTE_P){
page_remove(pgdir,va);
}
physaddr_t ph_addr = page2pa(pp);
*pt_entry = ph_addr | perm | PTE_P;
return 0;
}
复制代码
3. 内核地址空间
根据 checkpage 的测试代码驱动,按照内存布局进行映射即可完成本部分
Exercise 5
boot_map_region(kern_pgdir,UPAGES,npages*sizeof(struct PageInfo),PADDR(pages),PTE_U|PTE_P);
cprintf("UPAGES,Entry:%d,Base VA:%08x\n",PDX(UPAGES),pages);
boot_map_region(kern_pgdir,KSTACKTOP-KSTKSIZE,KSTKSIZE,PADDR(bootstack),PTE_P|PTE_W);
cprintf("KERNSTK,Entry:%d,Base VA:%08x\n",PDX(KSTACKTOP-KSTKSIZE),bootstack);
boot_map_region(kern_pgdir,KERNBASE,0xffffffff-KERNBASE,PADDR((void *)KERNBASE),PTE_W|PTE_P);
cprintf("KERNBASE,Entry:%d,Base VA:%08x\n",PDX(KERNBASE),KERNBASE);
复制代码
补充资料,其他博主实现:
[asd][www.cnblogs.com/JayL-zxl]
git merge
从 lab2 开始,每次进行编码前都需要进行 git merge,可以将 merge 理解为向量的加法,将每一个分支为一个向量,git merge 之后得到新的分支,如果遇到冲突,删掉冲突代码后,git comit 即可。
总结
不管是分段管理还是分页管理,本质上都是针对计算机同一块内存地址空间的管理,要想在一块内存中完成多个功能就要将其分成多个部分,类似鸽巢引理,一个盒子中无法保存两个苹果,所以需要进行切分。
切分之后的内存单元数组需要保存一些元信息(控制信息),存放这些元信息的地方在分段寻址中是 gdt entry,在分页寻址中是 页表|页目录,它们与物理地址空间是一对一的映射关系(非双射),地址翻译时查找映射表即可;