BUAA_OS lab2实验报告
一、思考题
思考2.1
请思考cache用虚拟地址来查询的可能性,并且给出这种方式对访存带来的好处和坏处。另外,你能否能根据前一个问题的解答来得出用物理地址来查询的优势?
我认为cache使用虚拟地址查询在理论上是可行的,但实际上要有很多顾虑。使用虚拟地址和好处:若cache能命中,则不需要经过物理地址就能得到数据,提高性能。坏处:对于不同进程来说,相同的虚拟地址可能映射到不同的物理地址,为获得自己进程对应的此虚拟地址上的数据,防止进程间相互干扰,可能需要额外思考区分方式,比如增加表示进程的标志位(这导致占用空间变大)、或者为每个进程分配一个cache(这不可能);cache未命中时,需要访问页表加载数据,而页表一般较大,这会需要更多的时间。
使用物理地址的优势:cache未命中时加载速度相对较快;每个地址只对应一个数据,更加安全,且使得进程间的数据可以共享。
思考2.2
在我们的实验中,有许多对虚拟地址或者物理地址操作的宏函数(详见include/mmu.h ),那么我们在调用这些宏的时候需要弄清楚需要操作的地址是物理地址还是虚拟地址,阅读下面的代码,指出x是一个物理地址还是虚拟地址。
int x;
char *value = return_a_pointer();
*value = 10;
x = (int) value;
x是虚拟地址(c语言中指针指向的地址都是虚拟地址)。
思考2.3
我们在 include/queue.h 中定义了一系列的宏函数来简化对链表的操作。实际上,我们在 include/queue.h 文件中定义的链表和 glibc 相关源码较为相似,这一链表设计也应用于 Linux 系统中 (sys/queue.h 文件)。请阅读这些宏函数的代码,说说它们的原理和巧妙之处。
-
LIST_HEAD
创建链表头,名称、数据类型都可以根据需求设置 -
LIST_HEAD_INITIALIZE
初始化链表头,可用于直接重置链表 -
LIST_ENTRY
链表第一个节点,其中*le_next
指向下一个节点,**le_prev
指向前一个节点的*le_next
-
LIST_EMPTY
判断链表是否为空 -
LIST_FIRST
返回指向链表第一个节点的指针 -
LIST_FOREACH
用于遍历链表 -
LIST_INIT
创建新链表 -
LIST_INSERT_AFTER
在某结点后插入新节点 -
LIST_INSERT_BEFORE
在某结点前插入新节点,需要操作需要操作新节点两个指针,原前面结点的le_next
指针,原后面结点的le_prev
指针 -
LIST_INSERT_HEAD
在链表头插入新节点,需要首先判断原头结点是否为空,若为空,直接将新节点作为头结点,不空则需要设置原头结点的le_prev
指针 -
LIST_INSERT_TAIL
在链表尾部插入新节点 -
LIST_REMOVE
移除节点,若被移除的节点位于链表尾部,则只需设置其前面节点的指针,否则还需要设置其后面节点的le_prev
指针
巧妙之处:节点中链接前后的指针与平时所用链表的指针方式不同,这使得在清空链表、节点插入和删除时更方便。宏定义使得链表的几乎所有操作都可以调用相同的宏函数实现,便于使用。将指向链表的头结点的指针包括在一个结构体中,而没有直接传递和使用链表头结点,可能也具有保护作用。
思考2.4
我们注意到我们把宏函数的函数体写成了 do { /* ... */ } while(0)的形式,而不是仅仅写成形如 { /* ... */ } 的语句块,这样的写法好处是什么?
保证宏替换时其内容仍是一个完整的语句块,其中定义的临时变量只在这个代码块中有效,防止其与外部产生冲突。
不直接使用{}
包围可以防止语法错误,因为日常c语言书写语句时需要在语句后写分号,调用宏函数也是如此,do while句式后面是需要分号的,但如果宏定义中只是简单的大括号括起来,其后面再加分号就会出现语法错误。
思考2.5
注意,我们定义的 Page 结构体只是一个信息的载体,它只代表了相应物理内存页的信息,它本身并不是物理内存页。 那我们的物理内存页究竟在哪呢?Page 结构体又是通过怎样的方式找到它代表的物理内存页的地址呢? 请你阅读 include/pmap.h 与 mm/pmap.c 中相关代码,并思考一下。
物理内存页空间由pmap.c文件中的mips_vm_init
函数初始分配,是pages
指针指向的位置。物理内存页地址 = 物理页号 * 页面大小,在我们的操作系统中是物理页号左移12位。对于Page结构体,可以调用pmap.h头文件中的page2pa
函数,以结构体的指针为参数获得物理内存页地址。
思考2.6
请阅读 include/queue.h 以及 include/pmap.h, 将Page_list的结构梳理清楚,选择正确的展开结构(请注意指针)。
C
思考2.7
在 mmu.h 中定义了 bzero(void *b, size_t) 这样一个函数,请你思考,此处的b指针是一个物理地址, 还是一个虚拟地址呢?
b指针是虚拟地址。在pamp.c的*alloc
函数中调用了这一函数,b对应的是变量alloced_mem
,是虚拟地址。
思考2.8
了解了二级页表页目录自映射的原理之后,我们知道,Win2k内核的虚存管理也是采用了二级页表的形式,其页表所占的 4M 空间对应的虚存起始地址为 0xC0000000,那么,它的页目录的起始地址是多少呢?
页表大小为4M,因此起始位置对应的物理页号为0xC0000000 / 4M = 0x300,页目录起始地址为0xC0000000 + 0x300 * 4K = 0xC0300000.
思考2.9
注意到页表在进程地址空间中连续存放,并线性映射到整个地址空间,思考:是否可以由虚拟地址直接得到对应页表项的虚拟地址?上一节末尾所述转换过程中,第一步查页目录有必要吗,为什么?
由虚拟地址可以直接得到对应页表项的虚拟地址。
第一步查页目录有必要,因为需要根据页目录中存储的信息判断二级页表是否有效(其余11个标志位同样需要检查)。
设置二级页表也是有必要的。查找资料后我了解到,尽管二级页表甚至比一级页表多了4KB存储页目录的内存消耗,但二级页表在进程上远比一级页表更节省空间。具体为:一级页表需要连续存储,这意味着只能使用基地址+偏移
的方式查找页表。因此为防止空间不够,一个进程开始时就需要把所有一级页表都加载到内存中,即使并不是所有地址都映射到了页表上。而二级页表机制中只需要连续存储4KB的页目录,之后页表在使用到的时候再动态分配,因此节约了内存。
思考2.10
观察给出的代码可以发现,page_insert 会默认为页面设置PTE_V的权限。请问,你认为是否应该将PTE_R 也作为默认权限?并说明理由。
我认为不应该。是否赋予写入权限应该有执行插入的程序决定,如果确实有写入的需要,可以通过perm
变量传参。
思考2.11
思考一下tlb_out 汇编函数,结合代码阐述一下跳转到NOFOUND的流程?从MIPS手册中查找tlbp和tlbwi指令,明确其用途,并解释为何第10行处指令后有4条nop指令。
grep查找调用这一汇编函数的位置,发现在pmap.c文件中,函数如下:
void
tlb_invalidate(Pde *pgdir, u_long va)
{
if (curenv) {
tlb_out(PTE_ADDR(va) | GET_ENV_ASID(curenv->env_id));
} else {
tlb_out(PTE_ADDR(va));
}
}
参数为va
对应的物理地址,因此在调用汇编函数时,a0
寄存器即存储了这一物理地址。
函数首先将CP0_ENTRYHI
中原有的值写入k1
寄存器,然后将a0
寄存器值转存到k1
中,待mtc0
指令退出流水线后执行tlbp
指令查询参数的地址是否存在于tlb中。如果不存在,将Index最高位置1;如果存在,将地址所在项index写入到Index
寄存器中。
由于前一步可能写入Index寄存器,后一步需要获取Index寄存器的数据,因此需要等写入指令出流水线后再执行下一条指令,对于五级流水线,需要暂停4个nop
指令。
之后将刚刚写入的index读出,执行bltz
指令,若index < 0,即最高位置1了,说明tlb确实,跳转到NOTFOUND
函数,恢复CP0_ENTRYHI
,函数执行结束;若index <= 0,说明tlb命中,将EntryHi
和EntryLo
重置为0,之后调用tlbwi
指令将EntryHi
和EntryLo
寄存器的值写入到Index
寄存器存储的index对应的快表位置,函数执行结束。
思考2.12
显然,运行后结果与我们预期的不符,va值为0x88888,相应的pa中的值为0。这说明我们的代码中存在问题,请你仔细思考我们的访存模型,指出问题所在。
观察va2pa
函数:
static inline u_long
va2pa(Pde *pgdir, u_long va)
{
Pte *p;
pgdir = &pgdir[PDX(va)];
if (!(*pgdir & PTE_V)) {
return ~0;
}
p = (Pte *)KADDR(PTE_ADDR(*pgdir));
if (!(p[PTX(va)]&PTE_V)) {
return ~0;
}
return PTE_ADDR(p[PTX(va)]);
}
发现函数中只涉及相对页目录起始地址和相对页表起始地址的偏移,没有页内偏移,因此返回的只是va所在页表对应的物理地址,而不是真正的物理地址。因此之后0x88888的赋值也没有对应到pa
指向的位置。
思考2.13
在X86体系结构下的操作系统,有一个特殊的寄存器CR4,在其中有一个PSE位,当该位设为1时将开启4MB大物理页面模式,请查阅相关资料,说明当PSE开启时的页表组织形式与我们当前的页表组织形式的区别。
若使用PSE技术,需要设置CR4中的第四位,此时也目录中的表项第七位增添一个新标识(PS位),若此位为1,表示这个表项指向一个4MB的大物理页面,否则指向普通的4KB页面。PSE的页目录项只用到了原本20个地址位的高10位,后22位为0恰好表示这是一个4MB对齐的页面。
二、实验难点
1. Page存储结构
2. task2.1
LIST_INSERT_TAIL
是一个难点,它比已给出的LIST_INSERT_HEAD
更复杂
其中遍历查找到最后一个节点可以使用LIST_FOREACH宏函数,其本质上是一个for循环,但考虑到for循环中用于记录循环的i这一变量,在当时我并没有想到如何在宏中定义出这一变量(事实上似乎可以直接定义,因为宏的do{}while块可以保证定义出来的变量不干扰到外部且不受外部干扰),因此最后我使用新节点的next部分作为了循环自增的变量。节点将作为尾节点插入,因此在使用next完成循环查找后只需要最后将next指向NULL即可。
3. task2.5
虚拟地址部分逻辑并不是很难,主要难在对所给出的变量的理解和使用,不过我认为有时候拘泥于所给出的代码和提示,可能会扰乱自己的思路。在此列一下自己了理解的各个变量的意义和使用:
变量名 | 意义 | 用法 |
---|---|---|
pgdir | 页目录首地址(虚) | pgdir + PDX(va)可以获取一级页表项的指针 |
pgdir_entryp | 指向页目录中的页表项 | 取指针指向的内容,调用PTE_ADDR获取二级页表的物理地址 |
pgtable | 指向二级页表首地址 | pgtable + PTX(va)可以获取二级页表项的指针 |
其中需要注意的是,在调用KADDR
、page2kva
获取虚拟地址后,需要使用(Pte *)
进行强制类型转换,否则在之后进行加法时会按照有符号数进行加减。
4. 页表结构与自映射机制
我理解的映射机制是:
va的22-31位用于指示一级页表项在页目录中的偏移,从而查找到一级页表项后,页表项中的内容包括二级页表的起始地址,再根据va的12-21位获得在二级页表内的偏移,从而查找到二级页表项,二级页表项的内容包括了页表在物理内存中的起始地址,最后运用va的0-11位页内偏移,找到虚拟地址对应的页表项。
三、体会与感想
本次lab用时大约15小时,期间经历了无数次理论体系的崩塌与重构,甚至曾以为物理内存就是硬盘上的地址?!
从这次作业开始,需要阅读的代码量陡增,也更感觉吃力了些。存储结构比较抽象,光靠脑子想是不行的,还是画出图来更能帮助理解,这种方法也要运用到之后的学习中。
这次作业也让我意识到“过了”不是真的“过了”,以后做完可能还需要找几个同学一起对一对逻辑,学习的同时也尽量避免自己的操作系统成为bug的舞池。
本次居然又挂掉了一次课上,lab2-2,全程在算task0,就没有想到像其他同学一样枚举。课上的时候可能思维陷入了一个怪圈,甚至一直在想64-9-9-9-12之后剩下的位数怎么办,还列了好多方程,以至于如此接近答案却又没有做出来。可能有时候,取巧也是一种必要的技能。
四、残留难点
由于课上task0没有做出来,后面的task也不知道有没有做对,也没有时间仔细研究后面的问题,这可能成为了永远残留的难点。如果可能,还是希望课程组在之后能开放一下课上内容的测试,哪怕只开放很短的一段时间,毕竟考试的最终目的还是掌握这些知识吧。