lab2实验报告
实验思考题
2.1
请思考cache用虚拟地址来查询的可能性,并且给出这种方式对访存带来的好处和坏处。另外,你能否能根据前一个问题的解答来得出用物理地址来查询的优势?
cache用虚拟地址查询时可能的,只要CPU(程序)可以用虚拟地址取到正确物理地址中的数据,其中间经过的cache使用的地址并无大碍。
使用虚拟地址的cache时,优点在于查询cache前不用访问页表进行地址转换,缺点在于一旦cache中数据缺失,更新cache时需要访问页表,时间开销更大;同时多个程序的虚拟地址可能相同,安全性以及数据正确性比较难以保障,异或每个程序单独cache会对数据共享造成影响。
运用物理地址查询的优势便在于数据的安全性和方便共享。
2.2
请查阅相关资料,针对我们提出的疑问,给出一个上述流程的优化版本,新的版本需要有更快的访存效率。(提示:考虑并行执行某些步骤)
访问TLB出现缺失时,要更新TLB,但在更新TLB时访问页表的过程中我们需要的物理地址就已经可以获得了。因此可以并行更新TLB和查询cache。
2.3
在我们的实验中,有许多对虚拟地址或者物理地址操作的宏函数(详见include/mmu.h ),那么我们在调用这些宏的时候需要弄清楚需要操作的地址是物理地址还是虚拟地址,阅读下面的代码,指出x是一个物理地址还是虚拟地址。
int x;
char *value = return_a_pointer();
*value = 10;
x = (int) value;
x的值是虚拟地址
2.4
我们在 include/queue.h 中定义了一系列的宏函数来简化对链表的操作。实际上,我们在 include/queue.h 文件中定义的链表和 glibc 相关源码较为相似,这一链表设计也应用于 Linux 系统中 (sys/queue.h 文件)。请阅读这些宏函数的代码,说说它们的原理和巧妙之处。
- 这些宏函数用于创建及操作链表,包括了创建链表头、初始化链表头、清空链表、头插入、尾插入、中间项前后的插入、删除特定元素的操作。其链表头定义为了结构体,存储内容为头指针,而后对于每个节点,在其内部嵌套结构体用于存储下一项的地址和前一项中存放下一项的指针的地址,从而实现了链表的功能。
- 其巧妙之处之一是通过运用结构体定义实现了链表内容的可变性。链表头实现了自定义名称;链表节点可以是任意类型,其中存放前后节点信息的部分用结构体封装后作为链表节点的一部分而在使用宏时完全不受其他部分影响。
- 巧妙之处之二时链表节点信息中存放了前一项中存放下一项的指针的地址,从而更加方便了插入和移出节点的操作。这样可以在只知道一个链表节点的情况下对链表进行插入或删除节点操作。
2.5
我们注意到我们把宏函数的函数体写成了 do { /* ... */ } while(0)的形式,而不是仅仅写成形如 { /* ... */ } 的语句块,这样的写法好处是什么?
这样写可以保证宏在替换到程序当中时不会产生歧义或错误,使其是一个完整的语句块,相当于一条语句而不是多条语句,用起来更加方便。
2.6
注意,我们定义的 Page 结构体只是一个信息的载体,它只代表了相应物理内存页的信息,它本身并不是物理内存页。 那我们的物理内存页究竟在哪呢?Page 结构体又是通过怎样的方式找到它代表的物理内存页的地址呢? 请你阅读 include/pmap.h 与 mm/pmap.c 中相关代码,给出你的想法。
根据pmap.h中关于转换物理地址的函数代码:
static inline u_long
page2ppn(struct Page *pp)
{
return pp - pages;
}
/* Get the physical address of Page 'pp'.
*/
static inline u_long
page2pa(struct Page *pp)
{
return page2ppn(pp) << PGSHIFT;
}
可以发现计算物理地址主要用到的数据是指针pp相对于pages的偏移量。
阅读pmap.c可以发现pages定义为Page结构体的指针,容易知道其为一个数组的头指针,数组中的每一项顺序对应一块内存区域,通过辨别在数组中的项数即可对应物理内存页。
2.7
请阅读 include/queue.h 以及 include/pmap.h, 将Page\_list的结构梳理清楚,选择正确的展开结构(请注意指针)。
其为C:
struct Page_list{
struct {
struct {
struct Page *le_next;
struct Page **le_prev;
} pp_link;
u_short pp_ref;
}* lh_first;
2.8
在 mmu.h 中定义了 bzero(void *b, size_t) 这样一个函数,请你思考,此处的b指针是一个物理地址, 还是一个虚拟地址呢?
根据pmap.c中对这个函数的应用我们可以知道b指针是一个虚拟地址。
2.9
了解了二级页表页目录自映射的原理之后,我们知道,Win2k内核的虚存管理也是采用了二级页表的形式,其页表所占的 4M 空间对应的虚存起始地址为 0xC0000000,那么,它的页目录的起始地址是多少呢?
0xC0300000
2.10
注意到页表在进程地址空间中连续存放,并线性映射到整个地址空间,思考:是否可以由虚拟地址直接得到对应页表项的虚拟地址?上一节末尾所述转换过程中,第一步查页目录有必要吗,为什么?
可以直接由虚拟地址得到页表项地址,取消二级页表的设定,只使用一级页表;或是通过转换计算亦可。
设置页目录可以节约页表的内存开销,十分必要。其同样可以用于判断也表内内容是否有效。
2.11
思考一下tlb_out 汇编函数,结合代码阐述一下跳转到NOFOUND的流程?从MIPS手册中查找tlbp和tlbwi指令,明确其用途,并解释为何第10行处指令后有4条nop指令。
回答见下注释
#include
#include
#include
LEAF(tlb_out)
//1: j 1b
nop
//把CP0_ENTRYHI原有值存储到$k1中
mfc0 k1,CP0_ENTRYHI
//把a0中值存放到CP0_ENTRYHI;
//CP0_ENTRYHI存放了虚拟地址空间及其标志位
mtc0 a0,CP0_ENTRYHI
nop
//查询CP0_ENTRYHI中虚拟地址是否存在TLB中:
//如果有则把匹配项的index保存到Index寄存器中;
//没有匹配则置Index的最高位为1.
tlbp
//nop用于等待tlbp执行完毕(流水线暂停)
nop
nop
nop
nop
//读取改写后的CP0_INDEX到$k0
mfc0 k0,CP0_INDEX
//如果$k0中值小于0,即CP0_INDEX最高位置1,即TLB缺失
//则跳转到NOFOUND
bltz k0,NOFOUND
nop
//清空CP0_ENTRYHI和CP0_ENTRYLOW
mtc0 zero,CP0_ENTRYHI
mtc0 zero,CP0_ENTRYLO0
nop
//更新TLB
tlbwi
NOFOUND:
mtc0 k1,CP0_ENTRYHI
j ra
nop
END(tlb_out)
2.12
显然,运行后结果与我们预期的不符,va值为0x88888,相应的pa中的值为0。这说明我们的代码中存在问题,请你仔细思考我们的访存模型,指出问题所在。
运用va2pa()只是获得了va对应物理内存中的页的首地址,并未获得实际的va对应的pa地址,即未考虑页内偏移量。
2.13
在X86体系结构下的操作系统,有一个特殊的寄存器CR4,在其中有一个PSE位,当该位设为1时将开启4MB大物理页面模式,请查阅相关资料,说明当PSE开启时的页表组织形式与我们当前的页表组织形式的区别。
当开启PSE时,页的划分与当前基本一致,但在一级页表中增加使用了一位用于标识,用于区分从一级页表中寻找得到的地址是二级页表的入口地址还是4MB大小的页面地址。由于一级页表中一项能够映射到的地址本就是4MB,所以整体结构变化不大。
实验难点图示
- 实验前半程我们填写了链表操作用的宏、内存初始化、空闲链表建立和页面的分配和删除。其中最难的个人认为是理解为链表定义的一系列宏。每个链表节点存放了*le_next和**le_prev,这样方便了对链表的改变,但理解它到底怎么用花费了大量时间,主要在于理解这个指针的指针。用图来表示一下,同列的变量共用一块存储空间:
(this是本节点、pre是上一节点,表示法与c语言语法不一致)
这样在改变链表时,运用本节点的le_prev可以直接对上一节点的le_next存储值进行改变。 - 实验后半程完成了页表的创建、物理内存与虚拟内存建立映射和更新页表。个人认为难点在于辨别物理地址、虚拟地址和空闲链表中节点的地址。
其中空闲链表中节点的地址是page数组中的一项,其相对数组头的偏移量与物理地址相对首地址的位移相对应;
物理地址和虚拟地址则通过所学知识在不同区域用不同方法转换。
重点就在于区分操作数到底拥有的是哪个地址。
实验感想
综述
本次实验相比于前几次有了明显的难度提升,花费在其上的时间成倍提升,从周一下午开始几乎一直工作到周三中午。自己对于前半部分实验(2.1-2.4)部分所花费的时间明显多于后部分,一方面在于阅读代码是积累的过程,前面要阅读更多代码,后面则是应用,另一方面是自己对于指针的操作不够熟练。
体会
- 有时候需要更多相信自己
自己总是在思考上花费大量时间而不敢动手写代码。总是怕自己有些部分没想对,抑或是理解错误。预设的一些变量没有用到则会更加害怕。但一路写下来也并没有出现太多错误。 - 操作系统启动是一层套一层慢慢向上累加
在对页面操作时,我们用到了两套函数,一套在没有空闲链表时使用,按位操作,一套则是在为自己创造环境之后按页面进行操作。这让我想起了真正系统启动时的不同阶段。操作系统真的是在一点点给自己创造工作环境。