数据结构
这里讲的内容主要是queue.h里的内容。
值得注意的主要是pp_link,在pp_link里有一个struct Page ** le_prev,这个指针指向的是前一个结点的le_next,这样就可以通过后一个结点,修改前一个结点的连接值。
示意图如下:
知道了队列的结构,再结合已有的代码,想必你就能比较简单的完成queue.h的内容了。
一点注意事项
1、所有带env的东西,似乎都不是这一个lab需要理解的,所以就不说了,我写的时候也不太懂,以后lab如果弄明白了在回来补充(如果我还记得)
2、GitHub上有一些前辈留下的代码可供参考,大家可以观摩学习。
3、咱们的指导书是从低端向上,基础函数讲完在将上层的,我是从main函数开始写,遇到一个函数写一个函数,从顶端向下来说明的,希望能从一个不同的角度展现lab2的构建。这其中有一些函数,我觉得比较简单,或者和已经写过的函数有异曲同工的关系,就不再多笔了。
4、在这个实验中每个函数操纵的究竟是物理内存还是虚拟内存,每个变量对应的是物理内存还是虚拟内存都需要小心分析。
5、一定好好看实验代码的注释,有助于理解代码。
内存管理
建立并且使用虚拟内存,主要包括
页表构建
1、
mips_detect_init
中初始化内存大小等数据
2、
mips_vm_init()
在lab2中初始化二级页表,在lab3中还会初始化进程,这里先不提,先看初始化页表的过程。
首先要分配一页的物理内存用来储存页目录项,完成这一句便是用pgdir=alloc(……);
。
在这个函数是在页表尚未建立之前使用的,分配的物理内存也都与内核虚拟地址相对应,可以用宏PADDR()、KADDR()
将这个函数中涉及的物理地址,虚拟地址相互转换。这里先提一句,我们虚拟内存分配是使用的2G/2G模式,也就是说,有2G的虚拟内存始终属于内核管理,剩下的2G才是用户进程管理。
另外还要说一句bzero(void *b, size_t len)
。这个函数内容为
void bzero(void *b, size_t len){
void *max;
max=b+len;
while(b+3<max){
*(int *)b=0;
b+=4;
}
while(b<max){
*(char *)b++=0;
}
}
这里我们发现一个问题,明明接收到的b是一个虚拟地址,在没有页表的情况下,是如何根据b访问到b对应的物理地址呢?
这里就涉及到一些底层知识,详细内容可以参考《See MIPS Run Linux》,简单说,访问tlb和mmu翻译地址是硬件自动完成的,而一个虚拟地址究竟通过tlb/mmu还是用其他方式(比如kseg0,kseg1)都是硬件决定的。尽管此时页表没有建立,但是只要保证访问的虚拟地址范围在kseg0,kseg1中就可以。
3、
分配了一个页目录的内存之后,我们又建立一个数组pages
并分配了物理内存,pages这个数组代表的是全部的物理页框。**注意接下来区分物理内存和虚拟内存十分重要。**我们用页框和页面区分一下。
4、
接下来mips_vm_init
用到的函数是boot_map_segment
在boot_map_segment中核心函数是boot_pgdir_walk。
这个函数创建(create==1的话)并返回va对应的页表项地址。在这个函数中,pgdir是页表入口,va是目标虚拟地址,create决定了是否创建新的页表。
首先我们得到页目录项的地址,根据页目录项得到页表地址。
pgdir_entryp = pgdir + PDX(va);
pgtable = (Pte *)KADDR(PTE_ADDR(*(pgdir_entryp)));
之所以能够这样做,是因为boot_pgdir_walk
涉及的地址都是内核虚拟地址,所以可以用宏KADDR()
,PADDR()
在物理地址和虚拟地址之间转换。
接下来,我们检查*(pgdir_entryp)中的权限位,根据需要创建新的页表。
if(((*(pgdir_entryp)) & PTE_V) == 0){ //检查有效位,等于0显然是无效的
if(creat==1){
pgtable = alloc(BY2PG,BY2PG,1); //分配一个新的页框
(*pgdir_entryp) = PADDR(pgtable);
*pgdir_entryp = PTE_V|PTE_R|(*pgdir_entryp);//写入物理地址和权限信息。
}else{ //无效,又不能创建个新的,只能返回NULL了
return NULL;
}
}
最后找到页表项,并返回pgtable_entry
pgtable_entry = pgtable + PTX(va);
return pgtable_entry;
5、
知道了boot_pgdir_walk,再回来看boot_map_segment。
这个函数在物理地址和虚拟地址之间建立映射关系。换句话说就是填充了虚拟地址va对应的页表项和页目录项。va,pa分别为映射的虚拟地址和物理地址,size为映射的范围,perm为权限设置。
这个过程既需要设置页目录项,也需要设置页表项。结合代码说明一下。
//前面的参考实验代码,比较简单,直接从step2开始
for(i=0;i<size;i=i+BY2PG){//
//计算出页表项对应的起始地址
va_temp = va + i;
/*获得页表项,create参数设为1,也就是说,
*如果va_temp在pgdir中对应的页表为空,就新建一个页表。
*并且在boot_pgdir_walk中设置页目录项*/
pgtable_entry = boot_pgdir_walk(pgdir,va_temp,1);
/*设置页表项内容和权限位*/
*pgtable_entry = pa+i;
*pgtable_entry = (*pgtable_entry) | (perm|PTE_V);
}
对虚拟地址[va, va+size)中每4KB内存,都会调用pgtable_entry=boot_pgdir_walk(pgdir,va_temp,1)。这样每4KB内存就获得了一个页表,pgtable_entry对应页表项地址。然后由于pa,va是线性映射,应该向页表中写入的地址是pa+i。再设置好权限位。
最后,在向页目录项中设置权限位(页目录项内容在boot_pgdir_walk中已经设置好了)。
6、
了解了boot_map_segment
之后我们在回到mips_vm_init
,此时我们有
pages = (struct Page *)alloc(npage * sizeof(struct Page),BY2PG,1);
n=ROUND(npage*sizeof(struct Page), BY2PG);
boot_map_segment(pgdir, UPAGES, n, PADDR(pages), PTE_R);
这里再提醒一次,alloc是分配物理内存的函数。
然后看第三句,这里只在一部分虚拟内存和物理内存之间建立了映射关系,显然虚拟地址空间中PAGES
部分是用来储存pages
数组的,因此我们把UPAGES
映射到PADDR(pages)
上。
后面的step3,是在lab3中才能用到,这里不做叙述。
7、
结束了mips_vm_init
,就到了最后的page_init()
了。
这个函数把没有使用过得物理页框都放到一起,方便页面管理。还记得物理页框都用什么标记出来吗?我们现在的物理页框都放在一个数组pages
里面,按顺序插入就可以了。
首先调用了LIST_INIT初始化了一个链表,这个链表用来储存空页面。
用pages储存所有页面,由于页和内存线性映射,因此可以用一个循环把已经分配的页面(freemem之下的)都做标记。然后把没有分配的页面放到list中去。
至此我们就完成页表系统的建立。
PS、
我们可以看出,我们并没有把整个页目录和物理地址的映射关系建立起来,仅仅是建立了UPAGES
这一小部分,因为虚拟地址和物理地址的映射关系本身就是不断变化的,因此剩下的虚拟地址等需要用的时候再去建立与物理地址的映射。
页分配
主要实现是page_alloc
。在页表建立之后,我们可以用这个函数分配一个物理页框,注意这里是拿到一个物理页框,但是没有建立页框和虚拟地址的映射关系。
1、
从free_page_list头部获得一个空页表。
/*申请页表*/
ppage_temp = LIST_FIRST(&page_free_list);
/*假如没有申请到,返回错误*/
if (ppage_temp == NULL){
return -E_NO_MEM;
}
2、
然后我们便要将这个页面清零,显然要通过bzero
完成。
/*因为bzero接受的是一个虚拟地址,
*所以先获得这个页面对应的虚拟内核地址,
*然后调用bzero清零。*/
u_long pa = page2kva(ppage_temp);
bzero((void *)pa, BY2PG);
/*从自由页框链表中移除这一页框*/
LIST_REMOVE(ppage_temp,pp_link);
*pp = ppage_temp;
return 0;//成功就返回0
这里我们需要说明一下如何通过pages
直接操作物理页框的内容。
pages
是按照线性映射把整个物理内存分成了多个页框,一起放到这个数组里,这些物理内存总共只有64MB大小(至少在这次实验是这样的,可以回去看mips_detect_init
),而通过内核虚拟地址可以直接访问低512MB的虚拟地址。这意味着尽管没有页表,我们可以用内核虚拟地址访问到全部的物理内存。
然后去看看include/pmap.h
里面定义了有关page
想必不难理解了吧。
3、
总之,拿到ppage_temp对应的虚拟地址后,把这部分内存清零,然后从free_page_list中移除这个页,最后把结果赋给*pp
页插入
page_insert
。我们刚才用page_alloc
分配了一个物理页框,但是呢,还没有把它加入到页表中,没有虚拟地址和这个页框映射起来(内核虚拟地址不算),所以这个函数就是把虚拟地址和物理地址映射起来的,好让虚拟地址映射到我们刚才分配的物理页框上,再具体点,就是把对应的页表项填上。
1、
为了找到页表项,我们要用到pgdir_walk()
,这个函数和boot_page_walk有点像,功能也是一样,都是根据虚拟地址分配页面并返回页表项(这里是把页表项地址给ppte)。但是它是用于二级页表已经建立完成之后的内存分配函数。
/*获得页目录项*/
pgdir_entryp = pgdir + PDX(va);
/*获得页表基地址(内核虚拟地址)*/
pgtable = (Pte *)KADDR(PTE_ADDR(*pgdir_entryp));
之所以能这么做,就像前面说的,都是因为内核虚拟地址直接映射到低512MB内存,而总内存只有64MB。
然后检查是否可用这个页目录项
/*如果这个页目录项不可用*/
if(((*pgdir_entryp) & PTE_V) == 0){
if(create==1){
/*尝试分配一个页表给这个页目录项*/
if(page_alloc(&ppage)!=0){
/*失败,返回错误*/
*ppte = NULL;
return -E_NO_MEM;
}
/*成功,那就增加一个页面引用,设置相应页目录项*/
ppage->pp_ref++;
*(pgdir_entryp) = page2pa(ppage);
*pgdir_entryp = (*pgdir_entryp) | PTE_V |PTE_R;
pgtable = KADDR(PTE_ADDR(*pgdir_entryp));
}
else{
/*不准新建页表,页目录项又不可用,只能这样*/
*ppte = 0;
return 0;
}
}
设置好页目录项,拿到了页表基地址,直接去找页表项
*ppte = pgtable + PTX(va);
return 0;
3、
回到page_insert
,在拿到页表项之后,我们检查页表项内容。
/*把`va`对应的页表项储存到`pgtable_entry`中*/
pgdir_walk(pgdir, va, 0, &pgtable_entry);
/*检查这个页表项是否可用*/
if(pgtable_entry != 0 && (*pgtable_entry & PTE_V)!=0){
/*如果可用,看看是否映射到别的物理地址*/
if(pa2page(*pgtable_entry)!=pp){
/*里面放的不是我们想要的页框,
*那就先把原来的页框删了,然后继续*/
page_remove(pgdir, va);
}else{
/*如果里面储存的就是我们想要映射的那个页框,
* 那就更新tlb,返回*/
tlb_invalidate(pgdir, va);
*pgtable_entry = (page2pa(pp)|PERM);
return 0;
}
}
4、
这里我们来提一下page_remove
,这个函数把虚拟地址原本映射关系从页表中删除。这里我们发现,这个函数会调用tlb_invalidate
来更新tlb,这是因为它对页表项进行了改写。
还有一个函数page_lookup
,它可以查找一个虚拟地址的页目录项,返回这个虚拟地址对应的页框。这个函数利用了pgdir_walk
,但是create参数设为0,避免分配多余的页框作为页表。
5、
调用完page_remove
,回到了page_insert
,下一步要更新tlb,这个时候我们发现,我们在page_remove
里面已经更新了tlb了,为什么还要再次更新呢?仔细想想,调用page_remove
必须先确定页表项可用但是不符合我们要映射的物理地址。也就是说还可能有页表项压根不可用的情况,如下
/*比如页目录项不可用,create==1就会有*/
pgtable_entry == 0;
/*页表项缺失就会有*/
(*pgtalbe_entry & PTE_V) == 0;
这样就必须在page_insert
中更新tlb了。更新两次没有问题,这和tlb_invalidate
有关,看下面。
6、
然后我们来看tlb_invalidate
,这个函数其实是tlb_out
。
tlb_out是一个汇编函数,进入tlb_out,首先就是设置协处理器。关于协处理器的知识,可以参照See MIPS Run Linux这本书TLB章节。前两个指令读取并写入cp0的$10,其中$a0是进入函数时的参数。
然后的tlbp指令,这个指令会把检查TLB,如果TLB中有项和EntryHi寄存器匹配,就把Index寄存器设置为对应的项,如果没有匹配的项,就把Index最高位设置为1(也就是变成负数)。这里要注意的是,TLB内部是流水线式的,遍历全部的TLB需要一定时间,如果tlbp后直接读取,就可能在Index设置好之前就读出数据,所以必须添加足够的指令把它和tlbwi隔开。
接下来我们读取Index的值,如果小于零,显然没找到,那就跳转到NOFOUND,恢复遍历之前的状况,跳转回之前函数。
如果找到了,那就写入TLB,tlbwi把ENTRYHI和ENTRYLO0写入Index所指的项,在这里,就是把这一项清零了。
最后我们还是会进入NOFOUND,把一开始ENTRYHI数值重新输入回去,恢复现场,跳转回去。
到这里,tlb_out()所做的所有工作就都完成了
7、
好了,了解了tlb_out(),我们回到tlb_invalidata()。
这个函数就是用tlb_out()把va对应的tlb项清空,所以即使连续调用两次,也没有问题,毕竟只是清空。
8、
了解了tlb_invalidata,我们再往前回到page_insert。
清空了tlb,我们重新获取页表项填写。
/*获取页表项,这里要考虑到存在页目录项不可用,
*页表缺失的可能性,设置create为1*/
if(pgdir_walk(pgdir, va, 1, &pgtable_entry)!=0){
/*如果获取不了,就返回错误*/
return -E_NO_MEM;
}
/*设置页表项*/
*pgtable_entry = (page2pa(pp)|PERM);
pp->pp_ref++:
注意
这里tlb只清空不写入,所以就很恶心,一旦虚拟地址在tlb中被清空过一次,就再也向里面写入了,因为如果写入,会产生tlb缺失,在这个lab是没法处理的,只能不断循环,需要有重写机制才能再次使用。
结语
这个东西时我在做lab2的时候写的,还是希望能帮到学弟学妹,但是计算机这个东西很多时候你看再多的东西也不如自己真正做一遍理解的通透,我的内容可以参考,但是千万别自己不动手,一定要自己亲自去做一下。
如果我写的有疏漏,希望能帮忙指出,不胜感激。
另外如果有什么问题,也可以问我,我会在力所能及的范围内回复。
补充
1、
在lab2,所有分配出去的虚拟内存都是kseg0中的内存,但是我们在建立映射的时候却必须建立整个内存虚拟内存的映射关系。所以在mips_vm_init()中有一句
boot_map_segment(pagdir,UPAGES,n,PADDR(pages),PTE_R);
尽管UPAGES不是kseg0中的虚拟地址,也是可以建立内存映射关系的。这里我们可以看出,虚拟地址空间中不同的地址被映射到了相同的物理地址上。