操作系统lab2实验报告
实验文档-lab2
一、思考题汇总
思考1:
请思考cache用虚拟地址来查询的可能性,并且给出这种方式对访存带来的好处和坏处。另外,你能否能根据前一个问题的解答来得出用物理地址来查询的优势?
答:从原理上,将cache设置为用虚拟地址来查询是可行的,这就相当于将cache从页表的后方挪移到页表的前方。这种方式的好处在于如果cache命中能大大加快访存效率,但一旦未命中,则会形成一个没有cache的实地址访问结构,由于短板效应,从页表被引出的实地址访问内存会变成一个很慢的过程,从而使效率大大降低。
若采用物理地址查询,访问的效率与时间会相对稳定,不会因为是否命中cache,tlb等导致访存效率减慢。
思考2:
在我们的实验中,有许多对虚拟地址或者物理地址操作的宏函数(详见include/mmu.h ),那么我们在调用这些宏的时候需要弄清楚需要操作的地址是物理地址还是虚拟地址,阅读下面的代码,指出x
是一个物理地址还是虚拟地址。
int x;
char *value = return_a_pointer();
*value = 10;
x = (int) value;
答:由代码得,x
为value
这一指针变量转换为int
型而来,C语言中指针所表示的地址均为虚拟地址,因此x
也是一个虚拟地址。
思考3:
我们在 include/queue.h 中定义了一系列的宏函数来简化对链表的操作。实际上,我们在 include/queue.h 文件中定义的链表和 glibc 相关源码较为相似,这一链表设计也应用于 Linux 系统中 (sys/queue.h 文件)。请阅读这些宏函数的代码,说说它们的原理和巧妙之处。
答:这些宏函数实现的都是对链表的一些基本操作,包括对链表的头插,尾插,中间插入,删除某个节点,遍历等等功能。本操作系统中的链表结构为:链表头是一个结构体,其中存储了头指针,其内置一个结构体,其中包含了指向下一节点的指针和指向上一节点内指向下一节点的指针的指针,从而形成链表。
这种结构的巧妙之处在于让C语言这个本没有“泛型”的语言实现了类似“泛型”的功能,因为指向前方与后方的指针被结构体内置起来,其与链表每个节点中的数据类型相对独立,可以在任何数据类型中得到很好的使用。
其次,前指针不指向上一节点而是指向其后指针,从而对链表指针的操作变得更加容易,灵活,从而是许多对链表的基本操作更加容易。
思考4:
我们注意到我们把宏函数的函数体写成了 do {/* ... */} while(0)
的形式,而不是仅仅写成形如 { /* ... */ }
的语句块,这样的写法好处是什么?
答:这样能将宏函数封装为一个整体,使其不再是一条条孤立的语句,而是一条语句,从而增加了宏函数的可移植性。
思考5:
注意,我们定义的 Page 结构体只是一个信息的载体,它只代表了相应物理内存页的信息,它本身并不是物理内存页。 那我们的物理内存页究竟在哪呢?Page 结构体又是通过怎样的方式找到它代表的物理内存页的地址呢? 请你阅读 include/pmap.h 与 mm/pmap.c 中相关代码,并思考一下。
答:pmap.h中有如下代码:
extern struct Page *pages;
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;
}
不难得到,page2ppn
函数得到这个Page结构体相对于页表的项数,而page2pa
函数则是利用这个偏移进而通过移位等操作来得出这个Page结构体所对应的物理内存页地址。
思考6:
请阅读 include/queue.h 以及 include/pmap.h, 将Page_list的结构梳理清楚,选择正确的展开结构(请注意指针)。
A
struct Page_list{
struct {
struct {
struct Page *le_next;
struct Page **le_prev;
}* pp_link;
u_short pp_ref;
}* lh_first;
}
B
struct Page_list{
struct {
struct {
struct Page *le_next;
struct Page **le_prev;
} pp_link;
u_short pp_ref;
} lh_first;
}
C
struct Page_list{
struct {
struct {
struct Page *le_next;
struct Page **le_prev;
} pp_link;
u_short pp_ref;
}* lh_first;
}
答:由阅读代码易得,正确的展开结构为C。
思考7:
在 mmu.h 中定义了 bzero(void *b, size_t)
这样一个函数,请你思考,此处的b指针是一个物理地址, 还是一个虚拟地址呢?
答:C语言中的指针均为虚拟地址,因此b为虚拟地址。
思考8:
了解了二级页表页目录自映射的原理之后,我们知道,Win2k内核的虚存管理也是采用了二级页表的形式,其页表所占的 4M 空间对应的虚存起始地址为 0xC0000000,那么,它的页目录的起始地址是多少呢?
答:
0xC0000000 =
0b1100|0000|00//00|0000|0000|//0000|0000|0000
由自映射算法,页目录的起始地址为:
ad + ad >> 10 =
0b1100|0000|00//11|0000|0000|//0000|0000|0000 =
0xC0300000
所以,页目录的起始地址为0xC0300000。
思考9:
注意到页表在进程地址空间中连续存放,并线性映射到整个地址空间,思考:是否可以由虚拟地址直接得到对应页表项的虚拟地址?上一节末尾所述转换过程中,第一步查页目录有必要吗,为什么?
答:通过计算可以得到,查页目录是有必要的,因为页目录可以有效的减少页表的内存空间开销,且页目录中的valid位可以判断所查的页表是否有效,避免了无效查询耗费资源。
思考10:
观察给出的代码可以发现,page_insert 会默认为页面设置PTE_V的权限。请问,你认为是否应该将PTE_R 也作为默认权限?并说明理由。
答:我认为应当要设置权限,因为PTE_R与PTE_V相似,都是页面的一个特征属性,在insert时应当对其做好设置以方便其他的函数进行采用。
思考11:
思考一下tlb_out 汇编函数,结合代码阐述一下跳转到NOFOUND的流程?从MIPS手册中查找tlbp和tlbwi指令,明确其用途,并解释为何第10行处指令后有4条nop指令。
答:NOFOUND指未找到TLB入口错误提示,在寻找TLB入口未找到时,便会跳到NOFOUND字段并执行相应操作。
在MIPS手册中,tlbp
指寻找TLB中的页表项,tlbwi
指向TLB中写入一个页表项信息。
根据《计算机组成》课程中的知识,连续出现4条nop指令的原因很可能是本操作系统的CPU没有针对tlbp
指令作出相应的转发处理,因此只能通过暂停来解决指令间数据冒险。
思考12:
显然,运行后结果与我们预期的不符,va值为0x88888,相应的pa中的值为0。这说明我们的代码中存在问题,请你仔细思考我们的访存模型,指出问题所在。
答:在我们的Start.S中,kernel mode cache被禁止,而物理地址位于kuseg,虚拟地址更新后则进入缓冲区,因而发生如上的错误。
思考13:
在X86体系结构下的操作系统,有一个特殊的寄存器CR4,在其中有一个PSE位,当该位设为1时将开启4MB大物理页面模式,请查阅相关资料,说明当PSE开启时的页表组织形式与我们当前的页表组织形式的区别。
答:PSE主要用于处理36位地址线中多出来的四位。当开放PSE时,页面扩大,原有的线性地址左移,这样的好处是表可以减少一级,但缺点是页面过大时会造成浪费。
二、实验难点图示
难点1:
链表的操作,宏函数编写
如上图所示,此链表的next指针指向下一个节点,prev指针指向上一个节点的next指针,因此在进行链表操作时,一定要理清指针之间的连接关系,避免出错。上图为LINK_AFTER的连接示意图,其中蓝色是需要新加入的线。
LINK_TAIL需要执行遍历的操作,可以用已提供的宏函数FOR_EACH来代替for进行遍历到尾部,再执行插入操作。
难点2:
页面与地址的转化
在本次实验中涉及到许多的页面与地址的转化,其中用到许多已经定义的函数,现整理如下:
page2pa:得到某个page结构体的物理地址
/* Get the physical address of Page 'pp'.
*/
static inline u_long
page2pa(struct Page *pp)
{
return page2ppn(pp) << PGSHIFT;
}
pa2page:得到某个物理地址所对应的Page结构体
/* Get the Page struct whose physical address is 'pa'.
*/
static inline struct Page *
pa2page(u_long pa)
{
if (PPN(pa) >= npage) {
panic("pa2page called with invalid pa: %x", pa);
}
return &pages[PPN(pa)];
}
page2kva:得到某个Page结构体的内核虚拟地址
/* Get the kernel virtual address of Page 'pp'.
*/
static inline u_long
page2kva(struct Page *pp)
{
return KADDR(page2pa(pp));
}
PPN:得到某个虚拟地址的页号
#define PPN(va) (((u_long)(va))>>12)
PADDR:将某个内核虚拟地址转化为物理地址
// translates from kernel virtual address to physical address.
#define PADDR(kva) \
({ \
u_long a = (u_long) (kva); \
if (a < ULIM) \
panic("PADDR called with invalid kva %08lx", a);\
a - ULIM; \
})
KADDR:将某个物理地址转化为内核虚拟地址
// translates from physical address to kernel virtual address.
#define KADDR(pa) \
({ \
u_long ppn = PPN(pa); \
if (ppn >= npage) \
panic("KADDR called with invalid pa %08lx", (u_long)pa);\
(pa) + ULIM; \
})
本次实验的前半部分涉及了许多对这类函数的应用,熟练掌握这类函数实现各类地址查询是本次实验的一大难点。
难点3:
页表结构与自映射机制
在Ex2.5中,需要补全的代码涉及到页目录,页目录项,页表,页表项等。我们首先需要将虚拟地址按照规定的结构拆分,得到页目录项,页表项以及偏移,并随着二级页表结构逐层向下查找,最终在根据页表查询的基地址加上偏移得到相应的物理地址。
三、体会与感想
这次的实验可以说让我感受到了很大的跨度和极高的挑战度。在上学期的计算机组成部分,我的存储方面的知识掌握的并不是很好,因此在操作系统实验遇到这方面的内容时,无疑是花费了大量的时间与精力。
结合理论课上学习到的知识,我对这些知识做了进一步树立,用于应对这一次的实验内容,并通过补全代码进一步强化我对cache,MMU,TLB等概念的理解与掌握。
实验中遇到了很多的问题,大多表现为看到一个需求不知道怎么下手,不敢下手,总而言之还是对内存的知识掌握不够熟练所致,因此在进行接下来的实验时,也要时刻对内存的知识来回顾。
在lab2中,老师反复强调的“读代码”问题终于表现出来,虽然lab2的所有代码补全基本全部集中在pamp.c这一个函数中,但是在地址转化等操作中,所用到的函数和宏函数却遍布于实验操作系统的每一个文件中。我在课下实验用的方法是将代码下载到实体机,在自己的电脑上进行补全后再誊写到虚拟机中,中间大量利用了编辑器强大的搜索功能。但即使是这样,我仍然在寻找函数时遇到了很多困难。所以我明白了:如果想要让自己的实验进行更加顺利,需要对系统中的每一个代码都进行理解性阅读,搞清楚这个代码封装了哪些函数,主要用于实现哪些功能,只有这样,才能在使用函数时能够得心应手,节省时间。
还有一个算是遗留性的问题,那就是我发现我的C语言基础不牢靠,在面对lab2众多的指针时有些吃不消。看来C语言还是得重修多练习。
四、残留难点
对我而言,这次实验中残留的难点主要是有关物理地址与虚拟地址的区别,我在这次实验中仅仅是默认指针型的地址变量为虚地址,u_long型的变量(即某个指针所指向的变量)为实地址,但对于许多有关地址的函数,如果未加注释的话,我仍然很难很快地判断出他们的地址类型。