MIT 6.828 JOS学习笔记15. Lab 2.1
Lab 2: Memory Management
lab2中多出来的几个文件:
inc/memlayout.h
kern/pmap.c
kern/pmap.h
kern/kclock.h
kern/kclock.c
memlayout.h描述了虚拟地址空间的结构,我们需要通过修改pmap.c文件来实现这个结构。memlayout.h和pmap.h文件定义了一个PageInfo结构,利用这个结构可以记录有哪些物理页是空闲的。kclock.c和kclock.h文件中操作的是用电池充电的时钟,以及CMOS RAM设备。在这个设备中记录着PC机拥有的物理内存的数量。在pmap.c中的代码必须读取这个设备中的信息才能弄清楚到底有多少内存。
Part 1:Physical Page Management
操作系统必须要追踪记录哪些内存区域是空闲的,哪些是被占用的。JOS内核是以页(page)为最小粒度来管理内存的,它使用MMU来映射,保护每一块被分配出去的内存。
在这里你要具体编写一下物理内存页的分配子函数。它利用一个结构体PageInfo的链表来记录哪些页是空闲的,链表中每一个结点对应一个物理页。
Exercise 1. 在文件 kern/pmap.c 中,你必须要完成以下几个子函数的代码
boot_alloc(); mem_init(); page_init(); page_alloc(); page_free();
check_page_free_list()和check_page_alloc()两个函数将会检测你写的页分配器代码是否正确。
答:
我们观察一下pmap.c中的代码,其中最重要的函数就是mem_init()了,在内核刚开始运行时就会调用这个子函数,对整个操作系统的内存管理系统进行一些初始化的设置,比如设定页表等等操作。
下面进入这个函数,首先这个函数调用 i386_detect_memory 子函数,这个子函数的功能就是检测现在系统中有多少可用的内存空间。
之前我们介绍过,jos把整个物理内存空间划分成三个部分:
一个是从0x00000~0xA0000,这部分也叫basemem,是可用的。
紧接着是0xA0000~0x100000,这部分叫做IO hole,是不可用的,主要被用来分配给外部设备了。
再紧接着就是0x100000~0x,这部分叫做extmem,是可用的,这是最重要的内存区域。
这个子函数中包括三个变量,其中npages记录整个内存的页数,npages_basemem记录basemem的页数,npages_extmem记录extmem的页数。
执行完这个函数,下一条指令为:
kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
memset(kern_pgdir, 0, PGSIZE);
其中kern_pgdir是一个指针,pde_t *kern_pgdir,它是指向操作系统的页目录表的指针,操作系统之后工作在虚拟内存模式下时,就需要这个页目录表进行地址转换。我们为这个页目录表分配的内存大小空间为PGSIZE,即一个页的大小。并且首先把这部分内存清0。
这里调用了boot_alloc函数,这个函数使我们要首先实现的函数:
这个函数就像在注释中说的那样,它只是被用来暂时当做页分配器,之后我们使用的真实页分配器是page_alloc()函数。而这个函数的核心思想就是维护一个静态变量nextfree,里面存放着下一个可以使用的空闲内存空间的虚拟地址,所以每次当我们想要分配n个字节的内存时,我们都需要修改这个变量的值。
所以添加的代码为:
result = nextfree; nextfree = ROUNDUP(nextfree+n, PGSIZE); if((uint32_t)nextfree - KERNBASE > (npages*PGSIZE)) panic("Out of memory!\n"); return result;
所以这条kern_pgdir = (pde_t *) boot_alloc(PGSIZE);指令就会分配一个页的内存,并且这个页就是紧跟着操作系统内核之后。
再看下一条命令:
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
这一条指令就是再为页目录表添加第一个页目录表项。通过查看memlayout.h文件,我们可以看到,UVPT的定义是一段虚拟地址的起始地址,0xef400000,从这个虚拟地址开始,存放的就是这个操作系统的页表kern_pgdir,所以我们必须把它和页表kern_pgdir的物理地址映射起来,PADDR(kern_pgdir)就是在计算kern_pgdir所对应的真实物理地址。
下一条命令需要我们去补充,这条命令要完成的功能是分配一块内存,用来存放一个struct PageInfo的数组,数组中的每一个PageInfo代表内存当中的一页。操作系统内核就是通过这个数组来追踪所有内存页的使用情况的。我写的代码如下:
pages = (struct PageInfo *) boot_alloc(npages * sizeof(struct PageInfo)); memset(pages, 0, npages * sizeof(struct PageInfo));
下一条指令我们将运行一个子函数,page_init(),这个子函数的功能包括:
1. 初始化pages数组 2.初始化pages_free_list链表,这个数组中存放着所有空闲页的信息
我们可以到这个函数的定义处具体查看,整个函数是由一个for循环构成,它会遍历所有内存页所对应的在npages数组中的PageInfo结构体,并且根据这个页当前的状态来修改这个结构体的状态,如果页已被占用,那么要把PageInfo结构体中的pp_ref属性置一;如果是空闲页,则要把这个页送入pages_free_list链表中。根据注释中的提示,第0页已被占用,io hole部分已被占用,还有在extmem区域还有一部分已经被占用,所以我们的代码如下:
size_t i; page_free_list = NULL; //num_alloc:在extmem区域已经被占用的页的个数 int num_alloc = ((uint32_t)boot_alloc(0) - KERNBASE) / PGSIZE; //num_iohole:在io hole区域占用的页数 int num_iohole = 96; for(i=0; i<npages; i++) { if(i==0) { pages[i].pp_ref = 1; } else if(i >= npages_basemem && i < npages_basemem + num_iohole + num_alloc) { pages[i].pp_ref = 1; } else { pages[i].pp_ref = 0; pages[i].pp_link = page_free_list; page_free_list = &pages[i]; } }
初始化关于所有物理内存页的相关数据结构后,进入check_page_free_list(1)子函数,这个函数的功能就是检查page_free_list链表的所谓空闲页,是否真的都是合法的,空闲的。当输入参数为1时,这个函数要在检查前先进行一步额外的操作,对空闲页链表free_page_list进行修改,经过page_init,free_page_list中已经存放了所有的空闲页表,但是他们的顺序是按照页表的编号从大到小排列的。当前操作系统所采用的页目录表entry_pgdir(不是kern_pgdir)中,并没有对大编号的页表进行映射,所以这部分页表我们还不能操作。但是小编号的页表,即从0号页表开始到1023号页表,已经映射过了,所以可以对这部分页表进行操作。那么check_page_free_list(1)要完成的就是把这部分页表对应的PageInfo结构体移动到free_page_list的前端,供操作系统现在使用。
剩下的操作就是对你的free_page_list进行检查了。
check_page_free_list(1)执行完成,我们将进入下一个检查函数check_page_alloc(),这个函数的功能是检查page_alloc(),page_free()两个子函数是否能够正确运行。所以我们首先要实现这两个子函数。
先实现page_alloc()函数,通过注释我们可以知道这个函数的功能就是分配一个物理页。而函数的返回值就是这个物理页所对应的PageInfo结构体。
所以这个函数的大致步骤应该是:
1. 从free_page_list中取出一个空闲页的PageInfo结构体
2. 修改free_page_list相关信息,比如修改链表表头
3. 修改取出的空闲页的PageInfo结构体信息,初始化该页的内存
代码如下:
struct PageInfo * page_alloc(int alloc_flags) { struct PageInfo *result; if (page_free_list == NULL) return NULL; result= page_free_list; page_free_list = result->pp_link; result->pp_link = NULL; if (alloc_flags & ALLOC_ZERO) memset(page2kva(result), 0, PGSIZE); return result; }
然后实现page_free()方法,根据注释可知,这个方法的功能就是把一个页的PageInfo结构体再返回给page_free_list空闲页链表,代表回收了这个页。
主要完成以下几个操作:
1. 修改被回收的页的PageInfo结构体的相应信息。
2. 把该结构体插入回page_free_list空闲页链表。
代码如下:
void page_free(struct PageInfo *pp) { // Fill this function in // Hint: You may want to panic if pp->pp_ref is nonzero or // pp->pp_link is not NULL. assert(pp->pp_ref == 0); assert(pp->pp_link == NULL); pp->pp_link = page_free_list; page_free_list = pp; }
至此,我们已经完成了这个Exercise为我们布置的任务,但是mem_init()函数的完善没有完成,这个将在下面的练习中继续完善。
Part 2: Virtual Memory
Virtual, Linear, and Physical Addresses
在x86体系中,一个虚拟地址(Virtual Address)是由两部分组成,一个是段选择子(segment selector),另一个是段内偏移(segment offset)。一个线性地址(Linear Address)指的是通过段地址转换机构把虚拟地址进行转换之后得到的地址。一个物理地址(Physical Addresses)是分页地址转换机构把线性地址进行转换之后得到的真实的内存地址,这个地址将会最终送到你的内存芯片的地址总线上。
我们所编写的C语言程序中的指针的值是虚拟地址中段内偏移部分的值。在boot/boot.S文件中,我们引入了一个全局描述符表,这个表通过把所有的段的基址设置为0,界限设置为0xffffffff的方式,关闭了分段管理的功能。因此虚拟地址中的段选择子字段的内容已经没有任何意义,线性地址的值总是等于虚拟地址中段内偏移的值。
回顾一下lab1中的part 3,我们引入了一个简单的页表,使得内核可以运行与0xf0100000的虚拟地址空间,尽管它所在的真实位置是物理地址0x00100000处,刚刚好在ROM BIOS之上。这个页表仅仅映射了4MB的内存空间。在我们这个JOS操作系统中,我们希望把这种映射扩展到物理内存的头256MB空间上,并且把这部分物理空间映射到从0xf0000000开始的虚拟空间中,以及一些其他的虚拟地址空间中。
Exercise 3
通过GDB,我们只能通过虚拟地址来查看内存所存放的内容,但是如果我们能够访问物理内存的话,肯定会更有帮助的。我们可以看一下QEMU中的一些常用指令,特别是xp指令,可以允许我们去访问物理内存地址。
“QEMU中有一个内置的监控器(moniter),首先通过在运行着QEMU软件的terminal里面输入 ctrl-a c,可以让我们切换到这个监控器。” 这个是官方给出的做法,但是在我的机器上并不好使,所以通过查询,发现在lab目录下面输入如下指令,一样可以打开moniter:
qemu-system-i386 -hda obj/kern/kernel.img -monitor stdio -gdb tcp::26000 -D qemu.log
打开monitor后,我们可以输入如下比较常见的指令:
xp/Nx paddr -- 查看paddr物理地址处开始的,N个字的16进制的表示结果。
info registers -- 展示所有内部寄存器的状态。
info mem -- 展示所有已经被页表映射的虚拟地址空间,以及它们的访问优先级。
info pg -- 展示当前页表的结构。
一旦进入保护模式,我们就不能直接使用线性地址或者物理地址了。所有代码中的地址引用都是虚拟地址的形式,然后被MMU系统所转换,所以C语言中的指针其实都是虚拟地址。
JOS内核通常需要把地址按照以一种模糊的值或者整数值的形式来操纵,而不是直接解析引用,比如物理内存分配器。有时使用虚拟地址,有时使用物理地址。为了能够帮助我们记录代码,JOS源文件中的地址被区分为两种情况:
uintptr_t -- 表示虚拟地址
physaddr_t -- 表示物理地址
这两种类型其实都是32位的整型数(uint32_t),所以如果你把一个类型的变量的值赋给另一个类型变量,编译器不会报错。但是由于他们都是整型数,所以如果你打算解引用(deference)他们,编译器会报错。
JOS内核可以先对uintptr_t类型的值进行强制类型转换,然后再解析引用。但是对于physaddr_t的值,我们不能这么做,因为内核是需要MMU(内存管理单元)来首先对你输入的地址进行转化的,如果你对physaddr_t进行强制类型装换再解引用,最终你得到的你要访问的地址,可能不是你要找的真实物理地址。
总结以下:
问题:
假设下述JOS内核代码是正确的,那么变量x应该是uintptr_t类型呢,还是physaddr_t呢?
mystery_t x; char* value = return_a_pointer(); *value = 10; x = (mystery_t) value;
答:
由于这里使用了 * 操作符解析地址,所以变量x应该是uintptr_t类型。
JOS内核有时需要读取或者修改内存,但是这时有可能他只知道这个要被修改的内存的物理地址。举个例子,当我们想要加入一个新的页表项时,我们需要分配一块物理内存来存放页目录项,然后初始化这块内存。然而,内核,它是不能绕过 虚拟地址转换 这一步的,因而它也不能直接加载或者存储物理地址。那么我们如何把物理地址转换为虚拟地址,我们可以采用KADDR(pa)指令来获取。其中pa指的是物理地址。
同样的,如果想通过虚拟地址的值求得物理地址的值,我们可以采用PADDR(va)指令。
Reference counting
在之后的实验中,你将会经常遇到一种情况,多个不同的虚拟地址被同时映射到相同的物理页上面。这时我们需要记录一下每一个物理页上存在着多少不同的虚拟地址来引用它,这个值存放在这个物理页的PageInfo结构体的pp_ref成员变量中。当这个值变为0时,这个物理页才可以被释放。通常来说,任意一个物理页p的pp_ref值等于它在所有的页表项中,被位于虚拟地址UTOP之下的虚拟页所映射的次数(UTOP之上的地址范围在启动的时候已经被映射完成了,之后不会被改动)。
当我们使用page_alloc函数的时候需要注意。它所返回的页的引用计数值总是0,所以pp_ref应该被马上加一。
Page Table Management
现在你应该可以着手开始编写管理页表的程序了:包括插入和删除线性地址到物理地址的映射关系,以及创建页表等操作。
Exercise 4. 完成kern/pmap.c中的下面几个子函数的编码
pgdir_walk() boot_map_region() page_lookup() page_remove() page_insert()
check_page()子函数将会被用来检查你所编写的这些程序是否正确。
答:
首先完成pgdir_walk函数,函数原型 pgdir_walk(pde_t *pgdir, const void *va, int create),该函数的功能在注释中解释道:
给定一个页目录表指针 pgdir ,该函数应该返回线性地址va所对应的页表项指针。
所以在这里我们应该完成以下几个步骤:
1. 通过页目录表求得这个虚拟地址所在的页表页对于与页目录中的页目录项地址 dic_entry_ptr。(7-8)
2. 判断这个页目录项对应的页表页是否已经在内存中。 (10)
3. 如果在,计算这个页表页的基地址page_base,然后返回va所对应页表项的地址 &page_base[page_off] (23-25)
4. 如果不在则,且create为true则分配新的页,并且把这个页的信息添加到页目录项dic_entry_ptr中。(11-18)
5. 如果create为false,则返回NULL。(19-20)
代码
1 pte_t * pgdir_walk(pde_t *pgdir, const void * va, int create) 2 { 3 unsigned int page_off; 4 pte_t * page_base = NULL; 5 struct PageInfo* new_page = NULL; 6 7 unsigned int dic_off = PDX(va); 8 pde_t * dic_entry_ptr = pgdir + dic_off; 9 10 if(!(*dic_entry_ptr & PTE_P)) 11 { 12 if(create) 13 { 14 new_page = page_alloc(1); 15 if(new_page == NULL) return NULL; 16 new_page->pp_ref++; 17 *dic_entry_ptr = (page2pa(new_page) | PTE_P | PTE_W | PTE_U); 18 } 19 else 20 return NULL; 21 } 22 23 page_off = PTX(va); 24 page_base = KADDR(PTE_ADDR(*dic_entry_ptr)); 25 return &page_base[page_off]; 26 }
接下来完成boot_map_region函数,函数原型 static void boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm),这个函数的功能在注释中被这样解释:
把虚拟地址空间范围[va, va+size)映射到物理空间[pa, pa+size)的映射关系加入到页表pgdir中。这个函数主要的目的是为了设置虚拟地址UTOP之上的地址范围,这一部分的地址映射是静态的,在操作系统的运行过程中不会改变,所以这个页的PageInfo结构体中的pp_ref域的值不会发生改变。
这个函数要完成的步骤如下:
1. 需要完成一个循环,在每一轮中,把一个虚拟页和物理页的映射关系存放到响应的页表项中。直到把size个字节的内存都分配完。
1 static void 2 boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm) 3 { 4 int nadd; 5 pte_t *entry = NULL; 6 for(nadd = 0; nadd < size; nadd += PGSIZE) 7 { 8 entry = pgdir_walk(pgdir,(void *)va, 1); //Get the table entry of this page. 9 *entry = (pa | perm | PTE_P); 10 11 12 pa += PGSIZE; 13 va += PGSIZE; 14 15 } 16 }
接下来再继续查看page_insert(),函数原型如下 page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm),功能上是完成:把一个物理内存中页pp与虚拟地址va建立映射关系。
这个函数的主要步骤如下:
1. 首先通过pgdir_walk函数求出虚拟地址va所对应的页表项。(4)
2. 修改pp_ref的值。(8)
3. 查看这个页表项,确定va是否已经被映射,如果被映射,则删除这个映射。(9-13)
4. 把va和pp之间的映射关系加入到页表项中。(14-15)
代码:
1 int 2 page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm) 3 { 4 pte_t *entry = NULL; 5 entry = pgdir_walk(pgdir, va, 1); //Get the mapping page of this address va. 6 if(entry == NULL) return -E_NO_MEM; 7 8 pp->pp_ref++; 9 if((*entry) & PTE_P) //If this virtual address is already mapped. 10 { 11 tlb_invalidate(pgdir, va); 12 page_remove(pgdir, va); 13 } 14 *entry = (page2pa(pp) | perm | PTE_P); 15 pgdir[PDX(va)] |= perm; //Remember this step! 16 17 return 0; 18 }
这里要注意,pp->pp_ref++这条语句,一定要放在page_remove之前,这是为了处理一种特殊情况:pp已经映射到va上了。至于为什么要这么做,大家可以思考一下。
接下来继续完成page_lookup()函数,函数原型:struct PageInfo * page_lookup(pde_t *pgdir, void *va, pte_t **pte_store), 函数的功能为:
返回虚拟地址va所映射的物理页的PageInfo结构体的指针,如果pte_store参数不为0,则把这个物理页的页表项地址存放在pte_store中。
这个函数的功能就很容易实现了,我们只需要调用pgdir_walk函数获取这个va对应的页表项,然后判断这个页是否已经在内存中,如果在则返回这个页的PageInfo结构体指针。并且把这个页表项的内容存放到pte_store中。
代码:
1 struct PageInfo * 2 page_lookup(pde_t *pgdir, void *va, pte_t **pte_store) 3 { 4 pte_t *entry = NULL; 5 struct PageInfo *ret = NULL; 6 7 entry = pgdir_walk(pgdir, va, 0); 8 if(entry == NULL) 9 return NULL; 10 if(!(*entry & PTE_P)) 11 return NULL; 12 13 ret = pa2page(PTE_ADDR(*entry)); 14 if(pte_store != NULL) 15 { 16 *pte_store = entry; 17 } 18 return ret; 19 }
最后一个就是page_remove函数,它的原型是:void page_remove(pde_t *pgdir, void *va),功能就是把虚拟地址va和物理页的映射关系删除。
注释里面还提示了要注意的几个细节:
1. pp_ref值要减一
2. 如果pp_ref减为0,要把这个页回收
3. 这个页对应的页表项应该被置0
代码:
1 void 2 page_remove(pde_t *pgdir, void *va) 3 { 4 pte_t *pte = NULL; 5 struct PageInfo *page = page_lookup(pgdir, va, &pte); 6 if(page == NULL) return ; 7 8 page_decref(page); 9 tlb_invalidate(pgdir, va); 10 *pte = 0; 11 }
以上就是Lab2 Part1和Part2的分析。
欢迎大家的建议与提问~
zzqwf12345@163.com