【MIT 6.828 Lab2 】幻境-内存管理

MIT 6.828 Lab2 内存管理

JOS 的内存管理中主要包括两部分:内存管理器与虚拟内存,内存单元以 page 为单位,整体图示如下

image-20210805151919096

1. 物理内存管理

操作系统需要记录 page 的使用情况,以便分配与回收,JOS 的实现中以 PageInfo 来记录 page 信息,通过链表来维护空闲 page,其中通过 PageInfo 数组 pages 与 物理页为一对一映射,可通过函数映射计算彼此的地址;

Exercise 1

  • boot_alloc

在内存分配器未初始化之前,JOS 使用 boot_alloc 来进行内存的分配,通过一个 static 指针变量 nextfree 来记录内存使用情况;

其中 nextfree 指针指向未被分配的地址空间,通过链接器提供的 end 符号地址进行初始化,内存分配也很简单,就是将 nextfree 指针偏移待分配的大小(参数 n 向上取整),需要注意的是,函数的返回结果是分配内存块的起始地址,分配内存单元可以理解为区间,C 中通过起始地址来标识;

image-20210803101546273

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;
}
复制代码

补充资料:

2. 虚拟内存

虚拟内存部分需要区分,线性地址,物理地址,虚拟地址,它们之间的关系如下图所示:

image-20211009165010010

  • 首先是分段寻址,segment translation and segment-based protection cannot be disabled on the x86,关于分段寻址可以参考 lab1 中 GDT 相关内容,它通过 (selector:offset)二元组来寻址,在未开启虚拟内存前,它可以直接得到物理地址

image-20210730181710510

figureB-2

  • 开启虚拟内存后,地址映射部分又加了一层,需要通过页表相关的翻译机制才能得到物理地址,当然也增加了操作系统的灵活性

    figureB-1

另外就是 Lab1 中 bootloader 加载内核之后的流程,其中因为链接器将 Kernel 链接至高位地址空间,所以 Entry.S 中开启了虚拟内存,将链接地址映射至内核镜像加载的物理地址;

image-20210803123616595

image-20210803133713648

JOS 配套的 qemu 提供了一些方便debug的命令,使用 ctrl-a c 即可进入

image-20211009172346169

Exercise 4

页表相关的函数,主要是映射关系的增删以及相关的页分配,核心函数为 pgdir_walk

figure2-1

  • 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 的测试代码驱动,按照内存布局进行映射即可完成本部分

image-20210805131530243

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);
复制代码

补充资料,其他博主实现:

blog.csdn.net/qq_40871466…

[asd][www.cnblogs.com/JayL-zxl]

git merge

从 lab2 开始,每次进行编码前都需要进行 git merge,可以将 merge 理解为向量的加法,将每一个分支为一个向量,git merge 之后得到新的分支,如果遇到冲突,删掉冲突代码后,git comit 即可。

image-20210807101054504

总结

不管是分段管理还是分页管理,本质上都是针对计算机同一块内存地址空间的管理,要想在一块内存中完成多个功能就要将其分成多个部分,类似鸽巢引理,一个盒子中无法保存两个苹果,所以需要进行切分。

切分之后的内存单元数组需要保存一些元信息(控制信息),存放这些元信息的地方在分段寻址中是 gdt entry,在分页寻址中是 页表|页目录,它们与物理地址空间是一对一的映射关系(非双射),地址翻译时查找映射表即可;

猜你喜欢

转载自juejin.im/post/7017014283590434853