MIT6.828 LAB2:http://pdos.csail.mit.edu/6.828/2014/labs/lab2/
LAB2里面主要讲的是系统的分页过程,还有就是简单的虚拟地址到物理地址的过程。关于系统分页,在MIT6.828 虚拟地址转化为物理地址——二级分页:http://blog.csdn.net/fang92/article/details/47320747中有讲。
下面主要是lab2的几个exercise的解题过程。
1.第一个boot_alloc()函数:
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[]; //end points to the end of the kernel's bss segment:
nextfree = ROUNDUP((char *) end, PGSIZE);
}
if(n==0)
return nextfree;
result = nextfree;
nextfree += n;
nextfree = ROUNDUP( (char*)nextfree, PGSIZE);
return result;
}
boot_alloc(unit32_t n)主要是申请n个字节的地址空间,返回申请空间的首地址。如果n是0,则返回nextfree(未分配空间的首地址)。其中,分配的地址是页对齐的,即4K对齐。
这个函数,巧妙应用了未初始化的全局变量和静态变量会被自动初始化为0这个特性。函数以首先定义静态局部变量nextfree,它指向空闲内存空间的首地址。由于未初始化,所以变量自动初始化为0,所以首次调用boot_alloc()函数的时候,nextfree的值是0,会执行下面的语句:
if (!nextfree) {
//end points to the end of the kernel's bss segment
nextfree = ROUNDUP((char *) end, PGSIZE);
}
从上面的图片可以看出,在整个程序里面,.bss段的位置是最下面的,注意.comment段是一些程序的注释信息,是不进内存的。所以end未初始化的全局变量,在.bss段中。所以end是指向的是整个内存的最后一个地址。(.bss段到底是怎么回事)。
接下来的ROUNDUP就是一个宏定义,用来4K对齐的,最后会返回一个地址,指向end的下一个空闲页的首地址。
所以在系统第一次调用boot_alloc()这个函数的时候,首先nextfree会被指向第一个空闲页的首地址。接下来,根据输入的n,来分配地址。如果n=0,则返回nextfree,否则分配n字节的地址,返回分配地址的首地址。注意,整个过程中,需要4K对齐。
2.mem_init()函数
这个函数只需要补充一部分就可以了。主要是要为struct PageInfo的结构体的指针pages申请一定的地址空间。首先来看struct PageInfo的定义:
struct PageInfo
{ //Next page on the free list.
struct PageInfo *pp_link;
uint16_t pp_ref;
}
这个结构体,主要是用来保存内存中的所有物理页面的信息的。每一个PageInfo对应一个物理页面。
pageInfo主要有两个变量:
pp_link表示下一个空闲页,如果pp_link=0,则表示这个页面被分配了,否则,这个页面未被分配,是空闲页面。
pp_ref表示页面被引用数,如果为0,表示是空闲页。(这个变量类似于智能指针中指针的引用计数)。
补充的代码比较简单,就是位pages申请足够的空间(npages的页面),来存放这些结构体,并且用memset来初始化:
pages = boot_alloc(npages * sizeof (struct PageInfo));
memset(pages, 0, npages*sizeof(struct PageInfo));
关于pages:
pages是用来管理物理内存页的一个结构体。首先,pages是整个系统物理内存的第0页,pages的值是一个虚拟地址。由于结构体是连续的,所以通过pages[i]-pages可以得到页的编号i,在通过i<<12就可以得到pages[i]所对应的页的物理内存,由于实现系统的物理内存和虚拟内存的转换比较简单,虚拟内存=物理内存+ 0xF0000000.所以通过pages这个结构体,在知道具体的物理页时,就可以很容易得到物理页对应的物理地址和虚拟地址,如图。
3.page_init()
page_init()函数主要就是结构体pages的初始化,就是在系统刚刚开始运行时,对物理内存分配进行初始化操作,按照上面的提示一步一步写就可以了,比较简单。
void
page_init(void)
{
size_t i;
for (i = 0; i < npages; i++) {
if(i == 0)
{ pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}
else if(i>=1 && i=IOPHYSMEM/PGSIZE && i< EXTPHYSMEM/PGSIZE )
{
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}
else if( i >= EXTPHYSMEM / PGSIZE &&
i < ( (int)(boot_alloc(0)) - KERNBASE)/PGSIZE)
{
pages[i].pp_ref = 1;
pages[i].pp_link =NULL;
}
else
{
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
}
}
在这个初始化的函数里面,有一点需要注意。整个pages的结构通过pp_link来区分页面是否是空闲页。对于非空闲页,只需要把pp_ref置位,pp_link置0就可以了,非空闲页不用通过什么东西来让他相互关联。但是对于空闲页,由于需要实时申请页面,在非空闲页释放页面之后,也需要把对应的页面加入到空闲页的结构中,所以空闲页的结构设计很重要。空闲页的结构设计要满足:申请页面时,能够较快的得到空闲页;2.释放页面时,能把页面较快的插入。
在这个实验系统中,采用了一个比较巧妙的结构,通过page_free_list这个变量来完成页面的申请和调用。
首先,页面的结构如下图所示:
通过图可以看到,整个空闲页面的结构有点像一个倒置的链表,因为在空闲页面的结构的建立过程中,是从下往上建立的,相当于在链表里面,其头节点是一个NULL指针。而使用过程中,整个结构类似于一个栈,采用先进后出的原则,page_free_list指向栈顶点。
在刚开始的时候,page_free_list =0,page_free_list表示系统的当前空闲页面。 初始化时,插入第一个空闲页表,把page_free_list赋值给空闲页表的pp_link,然后更新page_free_list,使得其指向第一个空闲页表(图中page0), 依次操作,直到空闲页表插入完毕。当free_page的时候,只需按照上面的过程,插入被释放的空闲页面就可以了。当系统申请空闲页表时,只需给出page_free_list所指向的页表就行,然后更新page_free_list.当page_free_list = NULL时,表示没有空闲页表。
总的来说,整个空闲页面的使用更像是一个栈。在整个结构中,page_free_list 起着很关键的作用。
这种结构,对于较大单位空间的管理比较有效,即简单又高效。如果把单位页面的大小缩小,考虑极端情况,以16B为一个页面,那么这种页面管理方法就不行了。可以看到,每个pageInfo里面有两个变量,一个位指针,一个为int, 即一个page的大小为8字节,以8字节来管理16字节大小的页面,到时候整个内存中,有50%都是pages结构体。浪费的空间非常巨大。而对于实验中的4K大小的页面,其空间利用率就非常高,整个pages占的空间大小大约为8/4000。
总的来说,这种管理方式和结构使用于大单位内存的管理。而对于堆内存的管理,由于堆的申请是以4或8字节为单位的,相当于页面大小位4B/8B,这种管理方法就不可行了。
4.page_alloc() 和 page_free()
page_alloc()是页面申请函数,整个函数思路比较简单,就是通过读取和更新page_free_list来申请页面。只要注意空闲页面被申请完的情况就可以了。
struct PageInfo *
page_alloc(int alloc_flags)
{ if(page_free_list == NULL)
return NULL;
struct PageInfo* page = page_free_list;
page_free_list = page->pp_link;
page->pp_link = 0;
if(alloc_flags & ALLOC_ZERO)
memset(page2kva(page), 0, PGSIZE);
return page;
}
page_free就是释放页面,也比较简单,只需要注意pp_ref和pp_link是否为0即可。
void
page_free(struct PageInfo *pp)
{
if(pp->pp_link != 0 || pp->pp_ref != 0)
panic("page_free is not right");
pp->pp_link = page_free_list;
page_free_list = pp;
return;
}
总共5个函数:
1. pgdir_walk():返回va对应的二级页表的地址(PTE)
2. boot_map_region: [va, va+size)映射到[pa, pa+size)
3.page_lookup:返回虚拟地址va对应的物理地址的页面page
4.page_remove:对va和其对应的页面取消映射
5. page_insert():把va映射到指定的物理页表page
1. pgdir_walk():返回va对应的二级页表的地址(PTE)
这个函数的主要作用是给定一个虚拟地址va和pgdir(page director table 的首地址), 返回va所对应的pte(page table entry)。当va对应的二级页表存在时,只需要直接按照页面翻译的过程给出PTE的地址就可以了。但是,当va对应的二级页表还没有被创建的时候,就需要手动的申请页面,并且创建页面了。过程比较简单,但是在最后返回PTE的地址的时候,需要返回PTE地址对应的虚拟地址,而不能直接把pte的物理地址给出。因为程序里面只能执行虚拟地址,给出的物理地址也会被当成是虚拟地址,一般会引发段错误。
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// Fill this function in
int pdeIndex = (unsigned int)va >>22;
if(pgdir[pdeIndex] == 0 && create == 0)
return NULL;
if(pgdir[pdeIndex] == 0){
struct PageInfo* page = page_alloc(1);
if(page == NULL)
return NULL;
page->pp_ref++;
pte_t pgAddress = page2pa(page);
pgAddress |= PTE_U;
pgAddress |= PTE_P;
pgAddress |= PTE_W;
pgdir[pdeIndex] = pgAddress;
}
pte_t pgAdd = pgdir[pdeIndex];
pgAdd = pgAdd>>12<<12;
int pteIndex =(pte_t)va >>12 & 0x3ff;
pte_t * pte =(pte_t*) pgAdd + pteIndex;
return KADDR( (pte_t) pte ); //一定要返回虚拟地址,物理地址会引发错误。
}
2. boot_map_region: [va, va+size)映射到[pa, pa+size)
这个函数比较简单,功能就是把 [va, va+size)映射到[pa, pa+size)
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
while(size)
{
pte_t* pte = pgdir_walk(pgdir, (void* )va, 1);
if(pte == NULL)
return;
*pte= pa |perm|PTE_P;
size -= PGSIZE;
pa += PGSIZE;
va += PGSIZE;
}
}
3.page_lookup:返回虚拟地址va对应的物理地址的页面page
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{ pte_t* pte = pgdir_walk(pgdir, va, 0);
if(pte == NULL)
return NULL;
pte_t pa = *pte>>12<<12;
if(pte_store != 0)
*pte_store = pte ;
return pa2page(pa);
}
4.page_remove:对va和其对应的页面取消映射
对va对应的页面进行释放,换句话说,就是取消va的映射。在这里注意,进行页面释放之后,需要把va对应的PTE里面存储的值进行清零操作,否则查询va对应的PTE时,会发生错误,系统会误以为va和pa还是存在对应关系。
void
page_remove(pde_t *pgdir, void *va)
{ pte_t* pte;
struct PageInfo* page = page_lookup(pgdir, va, &pte);
if(page == 0)
return;
*pte = 0;
page->pp_ref--;
if(page->pp_ref ==0)
page_free(page);
tlb_invalidate(pgdir, va);
}
这里,
void
tlb_invalidate(pde_t *pgdir, void *va)
{
// Flush the entry only if we're modifying the current address space.
// For now, there is only one address space, so always invalidate.
invlpg(va);
}
关于TLB,可以看:http://blog.chinaunix.net/uid-16361381-id-3044981.htm
由于TLB是页表条目的缓存,由于在这里,我们暂时没有考虑系统中的多进程的问题。所以可以假设TLB中存储的缓存条目都是一个进程的,即地址空间是一个。所以当va进行取消映射的时候,需要检查TLB中有无va的缓存页面条目,如果有,进行相应的操作。
5. page_insert():把va映射到指定的物理页表page
这个函数要考虑三种情况:
1.va没有对应的映射page
2:va有对应的映射page,但是不是指定的page
3.: va有对应的映射page,并且和指定的page相同。
对于情况1,最简单,直接把va和page映射就可以了,具体方法就是把va对应的pte求出,然后修改pte里面的值,使其为对应的page的物理地址。
对于情况2,先要把va对应的page释放(remove,ref-1),然后和情况1一样的处理方法。
对于情况3,要注意一点,当两个page相同的时候,不能直接返回。因为page_insert函数不仅要进行虚拟地址和页面的映射,它还要对页面的特权(PTE_U,PTE_P,PTE_W)进行设置,如果原来的page的特权和现在的特权不一样,那么直接return,就会存在问题。
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
pte_t* pte = pgdir_walk(pgdir, va, 1);
if(pte == NULL)
return -E_NO_MEM;
if( (pte[0] & ~0xfff) == page2pa(pp))
pp->pp_ref--;
else if(*pte != 0)
page_remove(pgdir, va);
*pte = (page2pa(pp) & ~0xfff) | perm | PTE_P;
pp->pp_ref++;
return 0;
}
现在对part2部分的页面管理做一个总结:
part2里面的各种函数其实就是对上面的二级页目录做一个管理,主要就是va和pa的映射。
pgdir_walk()函数就是返回va所对应的二级页表的pte的地址,在查询一级页目录时,如果va对应的pde里面没有存储对应的pte的物理基址,表明va还没有进行地址映射。此时,根据creat,来决定是否需要进行二级页面的创建。若需要,则申请一个空闲的页面,作为va的二级页表,然后返回对应的二级页表的地址(pte的地址)。
boot_map_region()函数就是向va对应的pte中填入对应的物理地址,以完成虚拟地址到物理地址的映射。
page_lookup()函数则是返回va对应的物理地址所在的页面page。
page_remove()函数则是取消va和对应的page的映射,即是把va对应的pte中的内容清零,然后由于对应的page和va失去了映射,所以需要把pp_ref-1.
page_insert()函数则是把va和要求的page建立映射关系,就是向pte里面填入page对应的物理地址。
综上,总的来说,part2就是写各种使用建立二级页面的函数。
上图就是pte和pde中的地址分布情况,由于低12位是偏移位,是由va决定的,所以低12位就被用作页面的标志位。这里主要看低3位,即U,W,P三个标志位。
p:代表页面是否有效,若为1,表示页面有效。否则,表示页面无效,不能映射页面,否则会发生错误。
W:表示页面是否可写。若为1,则页面可以进行写操作,否则,页面是只读页面,不能进行修改。
U:表示用户程序是否可以使用该页面。若位1,表示此页面是用户页面,用户程序可以使用并且访问该页面。若为0,则表示用户程序不能访问该页面,只有内核才能访问页面。
上面的页面标志位,可以有效的保护系统的安全。由于操作系统运行在内核空间(微内核除外,其部分系统功能在用户态下进行)中运行,而一般的用户程序都是在用户空间上运行的。所以用户程序的奔溃,不会影响到操作系统,因为用户程序无权对内核地址中的内容进行修改。这就有效的对操作系统和用户程序进行了隔离,加强了系统的稳定性。
在整个地址的中间的地址部分[UTOP,ULIM),用户程序和内核都可以访问,但是这部分内存是只读的,不可以修改,在这部分内存中,主要存储内核的一些只读数据。可能GDT什么的一些表就存在这部分地址空间中。
接下去的低位内存中,存的就是给用户程序使用的地址了。
剩下3个函数:
第一个:要求把pages结构体所在的页面和虚拟地址UPAGES相互映射。这里只要计算出pages结构体的大小,就可以通过page_insert()进行映射了。
int perm = PTE_U | PTE_P;
int i=0;
n = ROUNDUP(npages*sizeof(struct PageInfo), PGSIZE);
for(i=0; i
第二个:把虚拟地址[KSTACKTOP-KSTKSIZE, KSTACKTOP)映射到以bootstack为起点的物理地址(bootstack实际存储的是其虚拟地址,需要转换位物理地址),这是地址之间的静态映射,所以用boot_map_region()就可以了
perm =0;
perm = PTE_P |PTE_W;
boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, ROUNDUP(KSTKSIZE, PGSIZE), PADDR(bootstack), perm);
第三个也是一个地址的静态映射,是把地址从[KERNBASE, 2^32)映射到[0, 2^32 - KERNBASE)。这里唯一的一点麻烦就是32位机子没有办法表示2^32,要通过一定的方法来得到size。
int size = ~0;
size = size - KERNBASE +1;
size = ROUNDUP(size, PGSIZE);
perm = 0;
perm = PTE_P | PTE_W;
boot_map_region(kern_pgdir, KERNBASE, size, 0, perm );
整个lab2的基础部分就完成了,感觉没什么东西,却做了好久……
接下来是最后几个question:
We have placed the kernel and user environment in the same address space. Why will user programs not be able to read or write the kernel's memory? What specific mechanisms protect the kernel memory?
上面说过,主要是靠PTE_U来保护。
What is the maximum amount of physical memory that this operating system can support? Why?
由于在内存中,UPAGES总共有4M的内存来存储pages,也就是总共可以存4M/8Byte=0.5M个页面,总共的内存大小为0.5M*4K=2G,所以总共2G内存最大。
How much space overhead is there for managing memory, if we actually had the maximum amount of physical memory? How is this overhead broken down?
2G内存的话,总共页面数就是0.5M个,pages结构体(PageInfo)的大小就是0.5M*8Byte=4M,page director是4K, 由于pages的数目为0.5M,所以page table是0.5M*4byge=2M,所以总共是6M+4k
一般来说,32位系统的话,能放得最大内存应该是4G的。这里计算出来2G主要原因是UPAGES的内存范围只有4M,如果UPAGES扩展到8M,那么,这样系统就可以极限寻址到4G。