赞
踩
实验要做的操作系统是32位的,按字节编址
采用链表来管理空闲内存空间
相同类型数据的指针相减得到的是其中间相差的该类型数据个数
对指针进行加的含义并非对地址进行加,而是加对应倍的指针类型数据大小。
操作系统运行在虚拟内存空间中,过程中使用的所有的地址都是虚拟地址。
物理内存与内核虚拟空间实际上就是简单的线性映射。
Thinking2.1 请思考cache用虚拟地址来查询的可能性,并且给出这种方式对访存带来的好处和坏处。另外,你能否能根据前一个问题的解答来得出用物理地址来查询的优势?
Thinking2.2 在我们的实验中,有许多对虚拟地址或者物理地址操作的宏函数(详见include/mmu.h ),那么我们在调用这些宏的时候需要弄清楚需要操作的地址是物理地址还是虚拟地址,阅读下面的代码,指出x是一个物理地址还是虚拟地址。
Thinking2.3 我们在 include/queue.h 中定义了一系列的宏函数来简化对链表的操作。实际上,我们在 include/queue.h 文件中定义的链表和 glibc 相关源码较为相似,这一链表设计也应用于 Linux 系统中 (sys/queue.h 文件)。请阅读这些宏函数的代码,说说它们的原理和巧妙之处。
原理:宏函数在预处理的时候会将相应的位置替换成对应的宏,然后对其中的一些参数做替换。
具体宏代码解读:
#define LIST_HEAD(name, type) \
struct name{ \
struct type *lh_first; \
}
该宏函数为定义链表头指针结构的函数,参数name为链表头指针名,参数type为链表节点的类型。
#define LIST_HEAD_INITIALIZER(head) {NULL}
该宏函数将一个链表头指针指向一个空链表,用于初始化一个链表
#define LIST_ENTRY(type) \
struct { \
struct type *le_next; \
struct type **le_prev; \
}
定义了链表节点的link结构体,le_prev
为指向前一个节点结构体的link结构体中le_next
指针的指针,le_next
为指向下一个节点结构体的指针。这样方便我们进行对当前结构体的删除操作,只需使用*le_prev = le_next
,就能够将当前节点与链表解除关系
#define LIST_EMPTY(head) ((head)->lh_first == NULL)
链表是否为空,若为空返回1,否则返回0
#define LIST_FIRST(head) ((head)->lh_first)
取出链表的第一个节点
#define LIST_FOREACH(var, head, field) \
for ((var) = LIST_FIRST((head)); (var); \
(var) = LIST_NEXT((var), field))
遍历头指针为head
的链表中的全部节点,field
为节点中的link的结构体
#define LIST_INIT(head) \
do{ \
LIST_FIRST((head)) = NULL; \
} while(0)
为链表头指针初始化一个空链表
#define LIST_NEXT(elm, field) ((elm)->field.le_next)
取出链表中当前节点的指向下一个节点的指针
#define LIST_REMOVE(elm, field) \
do{ \
if(LIST_NEXT((elm), field) != NULL) \
LIST_NEXT((elm), field)->field.le_prev = \
(elm)->field.le_prev; \
*(elm)->field.le_prev = LIST_NEXT((elm), field); \
}while(0)
从链表中安全移除节点elm
#define LIST_INSERT_BEFORE(listelm, elm, field) \
do{ \
(elm)->field.le_prev = (listelm)->field.le_prev; \
LIST_NEXT((elm), field) = (listelm); \
*(listelm)->field.le_prev = (elm); \
(listelm)->field.le_prev = &LIST_NEXT((elm), field); \
}while(0)
在链表中listelm
节点前面增加一个节点elm
#define LIST_INSERT_HEAD(head, elm, field) \
do{ \
if((LIST_NEXT((elm), field) = LIST_FIRST((head))) != NULL) \
LIST_FIRST((head))->field.le_prev = &LIST_NEXT((elm), field);\
LIST_FIRST((head)) = (elm); \
(elm)->field.le_prev = &LIST_FIRST((head));
}while(0)
在链表头加一个节点
#define TAILQ_HEAD(name, type) \
struct name{ \
struct type* tqh_first; \
struct type** tqh_last; \
}
定义队列的头节点指针结构体,该结构体中tqh_first
为指向队首节点的指针,tqh_last
为指向队尾节点
#define TAILQ_ENTRY(type) \
struct{ \
struct type* tqh_next; \
struct type** tqh_prev; \
}
定义节点的link结构体,其中tqh_next
为指向下一个结构体的指针,tqh_prev
为指向前一个结构体的link结构体中的tqh_next
指针的指针。与上面链表定义类似
Thinking2.4 我们注意到我们把宏函数的函数体写成了 do { /* … */ } while(0)的形式,而不是仅仅写成形如 { /* … */ } 的语句块,这样的写法好处是什么?
辅助定义复杂的宏,避免引用时出错,保证调用该宏时,所有的语句都能在被调用处执行,例如如下宏定义:
#define DO() \
do1(); \
do2();
若我们这样调用:
if(start) DO()
那么,可能就会出现问题,do2();
在判断条件的控制域之外了,一定会被执行。
避免;
使用出现问题,使用上一个宏定义(可能有人习惯在写一个函数后面加上分号),若我们调用函数这样写:
if(start) DO();
这是我们习惯的写法,但是会编译报错,因为多了个分号……你可能会说定义宏的时候不要在最后加分号就好了呗,但是当我们定义一个复杂功能函数的宏函数时,很可能会在每一个语句后面都加一个分号,与其小心翼翼,不如使用一个规范的形式避免这种情况。
避免空宏引起的Warning
内核中由于不同架构的限制,很多时候会用到空宏,为避免编译时空宏报错,我们使用一个do{} while(0)
即可实现,但是这个我暂时没有接触到。
Thinking2.5 注意,我们定义的 Page 结构体只是一个信息的载体,它只代表了相应物理内存页的信息,它本身并不是物理内存页。 那我们的物理内存页究竟在哪呢?Page 结构体又是通过怎样的方式找到它代表的物理内存页的地址呢? 请你阅读 include/pmap.h 与 mm/pmap.c 中相关代码,并思考一下。
pages
数组首地址做差,得到该页对应的物理页号,然后将物理页号左移12位就得到了其对应的物理内存页的首地址。该函数的实现为pmap.h头文件中的page2pa(struct Page *pp)
。Thinking2.6 请阅读 include/queue.h 以及 include/pmap.h, 将Page_list的结构梳理清楚,选择正确的展开结构(请注意指针)。
Thinking2.7 在 mmu.h 中定义了 bzero(void *b, size_t) 这样一个函数,请你思考,此处的b指针是一个物理地址, 还是一个虚拟地址呢?
虚拟地址
原因解释:
mm/pmap.c文件中给出的alloc()
函数中使用了这一函数,通过阅读alloc()
函数,我们能够注意到有这样几行代码:
alloced_mem = freemem;
//...
if(clear) {
bzero((void *)alloced_mem, n);
}
//...
这里面的指针为alloced_mem
,顺藤摸瓜我们能找到其原始值为freemem
,分析这个freemem
变量,他是一个静态变量,并且通过这段下面这段代码我们注意到,在第一次使用freemem
对其进行了初始化,初始化为end的值。
那么继续摸瓜,这个end是在tools/scse0_3.lds链接文件中赋值的,其值为0x80400000,这个值为我们在Lab1中加载内核之后设置的结束地址,是虚拟地址。
Thinking2.8 了解了二级页表页目录自映射的原理之后,我们知道,Win2k内核的虚存管理也是采用了二级页表的形式,其页表所占的 4M 空间对应的虚存起始地址为 0xC0000000,那么,它的页目录的起始地址是多少呢?
Thinking2.9 注意到页表在进程地址空间中连续存放,并线性映射到整个地址空间,思考:是否可以由虚拟地址直接得到对应页表项的虚拟地址?上一节末尾所述转换过程中,第一步查页目录有必要吗,为什么?
Thinking2.10 观察给出的代码可以发现,page_insert会默认为页面设置PTE_V的权限。请问,你认为是否应该将PTE_R也作为默认权限?并说明理由。
Thinking 2.11 思考一下tlb_out汇编函数,结合代码阐述一下跳转到NOFOUND的流程?从MIPS手册中查找tlbp和tlbwi指令,明确其用途,并解释为何第10行处指令后有4条nop指令。
将需要查找的虚拟页号和相应的ASID数据传给CP0_ENTRYHI
寄存器,然后根据CP0_ENTRYHI
寄存器中的数据在TLB中查找与其相对应的表项,将查找结果记录在CP0_INDEX
寄存器中。再将CP0_INDEX
寄存器中的数据与0比较,若小于0则说明在TLB中没查到对应的表项,跳转至NOFOUND标签处,恢复CP0_ENTRYHI
的值,返回程序。
tlbp
:在TLB转换表中查找与CP0_ENTRYHI
寄存器数据相匹配的表项,并将表项索引记录在CP0_INDEX
寄存器中,若未查到,将CP0_INDEX
寄存器中的数据最高位置为1。
tlbwi
:根据现在CP0_ENTRYHI
、CP0_ENTRYLO0
、CP0_ENTRYLO1
、CP0_PAGEMASK
寄存器来填写CP0_INDEX
指定的TLB表项。
TLB查找匹配的表项后会将索引写入CP0_INDEX
寄存器,而下一条指令又需要读该寄存器,会产生写后读的冲突,所以需要等待四个时钟周期。
Thinking2.12 显然,运行后结果与我们预期的不符,va值为0x88888,相应的pa中的值为0。这说明我们的代码中存在问题,请你仔细思考我们的访存模型,指出问题所在。
Thinking2.13 在X86体系结构下的操作系统,有一个特殊的寄存器CR4,在其中有一个PSE位,当该位设为1时将开启4MB大物理页面模式,请查阅相关资料,说明当PSE开启时的页表组织形式与我们当前的页表组织形式的区别。
emmm感觉好多东西学完感觉也不是很难的,但当时觉得都挺难的,我就记录一下吧
该结构体的形式扩展开来如下:
//头节点:
struct Page_list{
struct Page* lh_first;
}
//页面信息节点:(若是空闲页面则在链表中)
struct Page{
struct {
struct Page* le_next; //指向后一个页结构体节点的指针
struct Page** le_prev; //指向前一个页结构体节点的pp_link->le_next的指针
} pp_link;
u_short pp_ref; //记录该物理页面引用次数
}
链表的整体结构大抵如下图所示:(注意指针的指向)
这里的链表用于空闲页面的管理,其中每个节点都表示一个相应的空闲物理页表信息。
所有的物理页面都有各自的页面信息结构体,在实验要设计的操作系统中使用了一个结构体指针pages
(当然也可以视为数组啦),根据物理页面的物理地址pa
将相应信息结构体按顺序存放在该指针开始的空间里,具体来讲就是:根据物理页面的物理地址pa
计算出来物理页号ppn
,那么其信息结构体就可以通过pages[ppn]
(这是个页虚拟地址)来进行访问喽!!
在头文件中定义了几个函数和宏:
page2ppn(struct Page *pp)
页面信息结构体指针(虚拟地址) → \rightarrow → 物理页号
page2pa(struct Page *pp)
页面信息结构体指针(虚拟地址) → \rightarrow → 页物理地址
pa2page(u_long pa)
页物理地址 → \rightarrow → 页面信息结构体指针(虚拟地址)
page2kva(struct Page *pp)
页面信息结构体指针(虚拟地址) → \rightarrow → 页虚拟地址
PADDR(kva)
内核虚拟地址 → \rightarrow → 物理地址 ,直接最高位清零(- 0x80000000)
KADDR(pa)
物理地址 → \rightarrow → 内核虚拟地址,直接最高位置一(+0x80000000)
PTE_ADDR(pte)
页表项值 → \rightarrow → 页物理地址(不一定只有这一个功能 ⇒ \Rightarrow ⇒低12位都置0)
PDX(va)
虚拟地址 → \rightarrow → 页目录号
PTX(va)
虚拟地址 → \rightarrow → 页表索引
tlb_invalidate(pgdir, va)
刷新TLB,TLB中存放页目录项
mips_detect_memory();
只是初始化几个内存的参数
maxpa
内存大小basemem
本实验与maxpa
一致npage
物理页数
=
m
a
x
p
a
÷
= maxpa\div
=maxpa÷页大小mips_vm_init();
为页目录、页结构体和envs分配了物理空间,并建立了页结构体PAGES虚拟内存空间和pages物理内存空间的页表映射关系,以及ENVS虚拟内存空间和envs物理内存空间的也表映射关系。
page_init();
初始化页结构体,建立空闲页面管理的链结构体
虚拟内存空间中的结构如图所示:
这部分的学习记录写在了博客里。
Pte //Page table entry 页表项
pgdir //Page directory 页目录
Pde //Page directory entry 页目录项
几个宏函数的理解:
#define LEAF(symbol) \
.global symbol; \
.align 2; \
.type symbol, @function; \
.ent symbol, 0; \
symbol: .frame sp, 0, ra
#define END(function) \
.end function \
.size function, .-function
LEAF和END是汇编文件中常见的宏,在我们实验的操作系统汇编代码里几乎都有这两个宏作为文件的开头和结尾。LEAF被用来定义一个简单的例程,即不调用其他例程,其中几个伪指令的解读如下:
.globl symbol
:声明symbol为全局变量,该变量名要包括在模块的符号表内,而且名字在整个程序范围内必须是唯一的
.align 2
:按字对齐
.align
:Align next data item on specified byte boundary (0=byte, 1=half, 2=word, 3=double)
.type symbol, @function
:标识函数名称
.ent symbol, 0
:标识函数的起始点
.end
:指出函数结尾,用于调试
.size function, .-function
:在函数表中,function和所用指令的字节数一同列出
mtc0 s, <n>
:把寄存器s中的数据传递给CP0寄存器<n>mfc0 d, <n>
:把CP0寄存器<n>中的数据传递给寄存器dTLB转换表中每一项含有一个页的虚拟地址(VPN即虚拟页号)和一个物理页地址(PFN即物理页帧号)。所以TLB中是直接拿虚拟页号来查找的,并且不是按照索引,而是逐项比较VPN,是一种内容寻址的存储器。当查找到相符的VPN,则取出PFN。(PFN与标志位一同存储一同返回,标志位能让操作系统指定某一页为只读或者指定某页的数据是否可以高速缓存)。
现代MIPS CPU大多采用双倍存储,每一个TLB项容纳一对相邻的虚拟页面对应的两个单独的物理地址。如图是一个TLB数据项:
TLB相关的CPU控制寄存器
CP0_ENTRYHI
(关键字域):记录VPN2和当前进程的ASID
VPN2
:32-13= 19位(低0-12都是0,略去无可厚非,第13位略去是为什么呢?因为我们上面说的双倍存储,所以在两个页面里选就成了)
ASID
:地址空间标识符,标识这个表项是属于哪一个进程的
CP0_ENTRYLO0/CP0_ENTRYLO1
(输出域):
ps:目前为止还用不到这些,等我日后总结吧
CP0_INDEX
:TLB表项的索引
TLB控制指令
tlbwi
:write TLB entry at index
根据现在CP0_ENTRYHI
、CP0_ENTRYLO0
、CP0_ENTRYLO1
、CP0_PAGEMASK
寄存器来写CP0_INDEX
指定的TLB表项。
tlbp
:TLB lookup
搜索虚拟页号和ASID与当前CP0_ENTRYHI
中的值相匹配的TLB项,并把该项的索引保存到CP0_INDEX
寄存器,若没有查到,则将CP0_INDEX
寄存器最高位置1,这样就相当于一个负数,标识未查到。
从写本次lab2课下实验的过程中就能明显感受到这次实验的难度陡然增大,搞懂(自我感觉的搞懂。。。可能只是一知半解吧)本次实验我大概用了30多个小时。感觉这一部分的填写并不难,因为助教给了详细的步骤说明,几乎每一行代码都有相应的提示,但是要搞懂整个的内存管理模型结构还是很耗费精力的(脑子疼wwww),即使到现在还是有一部分问题没有解决,希望在之后的代码能找到这些问题的答案,也希望下次的代码能读得顺利一些。
自映射在哪里呢??内核虚拟空间中并没有页目录自映射。是之后会在用户空间里看到吗?
te TLB entry at index
根据现在CP0_ENTRYHI
、CP0_ENTRYLO0
、CP0_ENTRYLO1
、CP0_PAGEMASK
寄存器来写CP0_INDEX
指定的TLB表项。
tlbp
:TLB lookup
搜索虚拟页号和ASID与当前CP0_ENTRYHI
中的值相匹配的TLB项,并把该项的索引保存到CP0_INDEX
寄存器,若没有查到,则将CP0_INDEX
寄存器最高位置1,这样就相当于一个负数,标识未查到。
从写本次lab2课下实验的过程中就能明显感受到这次实验的难度陡然增大,搞懂(自我感觉的搞懂。。。可能只是一知半解吧)本次实验我大概用了30多个小时。感觉这一部分的填写并不难,因为助教给了详细的步骤说明,几乎每一行代码都有相应的提示,但是要搞懂整个的内存管理模型结构还是很耗费精力的(脑子疼wwww),即使到现在还是有一部分问题没有解决,希望在之后的代码能找到这些问题的答案,也希望下次的代码能读得顺利一些。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。