Lab02
思考题
thinking 2.1
1.虚拟地址
2.虚拟地址
thinking 2.2
1.使用宏实现链表,不仅可以实现空页的链表管理,也可以实现空进程的链表管理。使用同一套宏可以实现多个链表的管理,大大提高了可重用性。
2.性能:
-
单向链表
插入操作相对较为简单,因为不需要维护`le_prev`指针,但是性能相差不大 删除操作单向链表更复杂,时间复杂度为`O(n)`,双向链表复杂度为`O(1)`
-
循环链表
插入操作简单,可以在`O(1)`时间内插入链表尾部,双向链表插入尾部复杂度为`O(n)` 删除操作与单向链表相同,时间复杂度为`O(n)`
thinking 2.3
C:
1 |
|
thinking 2.4
boot_map_segment()
:
boot_map_segment(pgdir, PAGES, n, PADDR(pages), PTE_R);
boot_map_segment(pgdir, ENVS, n, PADDR(envs), PTE_R);
在mips_vm_init()
中被调用了两次,分别将pages
数组的物理地址映射到以虚拟地址UPAGE
为起始地址的内核页表,以及将envs
数组的物理地址映射到以虚拟地址UENV
为起始地址的内核页表。
boot_pgdir_walk
:
pgtable_entry = boot_pgdir_walk(pgdir, va_temp, 1);
由boot_map_segment
调用。
thinking 2.5
1.ASID:
在不同的进程中,有一部分虚拟内存是所有进程共享的,因此相同的虚拟内存,在不同的进程中回映射到不同的物理地址。为了区分不同进程的虚拟地址,引入了不同进程互不相同的ASID,在查找TLB时,需要传入虚地址和对应的ASID,这样才能正确查找到对应的物理地址。因此ASID是不可缺少的。
2.在不刷新旧的ASID值情况下,由于EntryHi寄存器中ASID值共6位,因此最多可以容纳64个不同的地址空间。
thinking 2.6
tlb_invalidate
调用tlb_out
根据当前进程的状态,以虚拟地址和进程的ASID
值为参数调用tlb_out
完成对TLB
对应内容的清除
1 |
|
thinking 2.7
PTbase
对应第PTbase>>12
个页表项,每个页表项大小为8B
,则偏移地址为(PTbase>>12)<<3
,
则二级页表基地址为PTbase|(PTbase>>12)<<3
。
同理可得,页目录基地址为PTbase|(PTbase>>12)<<3|(PTbase>>21)<<3
,
而映射到页目录自身的页目录项地址为PTbase|(PTbase>>12)<<3|(PTbase>>21)<<3|(PTbase>>30)<<3
。
thinking 2.8
x86架构的内存管理分为两个部分,分段和分页。
分段提供了一种隔离每个进程或者任务代码、数据和栈模块的机制,保证多个进程或者任务能够在同一个处理器上运行而不会互相干扰。
分页机制实现了传统请求调页的虚拟内存系统,在这种系统中,程序的执行环境块按需要被映射到物理内存中。分页机制同样可以用来隔离多个任务。
对分页和分段机制进行不同的配置,可以分别支持简单的单任务系统、多任务系统或者使用共享内存的多处理器系统。
x86用到三个地址空间的概念:物理地址、线性地址和逻辑地址。而MIPS只有物理地址和虚拟地址两个概念。
实验难点
物理内存管理
页式内存管理
MOS结构中采取页式管理结构,将物理内存总64MB按4KB一页分成16K页。
在物理内存管理中,通过pages
结构体数组对这16K页进行管理。
Page
结构体中并没有记录太多复杂的信息,其中pp_ref
记录了该物理内被映射的次数,pp_link
是为了后续采取链表结构管理空闲链表所定义。每个Page
结构体对应的物理页并不需要记录,因为结构体与物理页一一对应,通过(p - pages)<<12
即可计算得到。
链表管理空闲内存
宏定义
对于空闲链表,MOS采取链表结构管理,并定义了一系列的宏便于操作。
理解并应用链表宏是物理内存管理的难点,下面把一些常用或者较为重要宏里的参数用Page
结构体对应的变量代换以便于理解。
1 |
|
通过转换以后可以很容易得到用于管理空闲链表的结构体定义如下:
1 |
|
宏函数
在了解了数据结构以后,便可以着手编写宏函数了。
LIST_INSERT_AFTER
由于双向链表的结构,LIST_INSERT_AFTER(listelm, elm, field)
的实现并不难,先了解三个参数对应的含义:
listelm
为链表中的元素,即用于定位的元素,在此函数中,新的元素需要被插入到该元素之后elm
即为新的需要被插入到链表中的元素field
是定义的用于链表管理的entry
,在链表管理中,即为Page
结构体中的pp_link
同样为了便于理解,把宏里的参数用Page
结构体对应的变量代换得到代码:
1 |
|
如图所示:
LIST_INSERT_TAIL
这个相对于前一个函数复杂的地方在于,首先需要判断链表是否为空,若非空则需要从头指针遍历直到最后一个节点,然后再仿照前一个函数将新元素插入尾部元素之后。而遍历是需要循环变量的,在编写这个函数时以为不可以在宏里定义新的变量,因此用LIST_NEXT((page), pp_link)
作为循环变量,代换后代码如下:
1 |
|
在写这个函数的时候有一个小问题,在找到最后一个元素后调用
LIST_INSERT_AFTER
会出错,应该是尾部元素之后的空指针导致的,但是具体问题还是没有发现。
相关函数
主要是page_init(), page_alloc(), page_free()
三个函数,在理解和编写完宏函数之后,以及指导书中给定框架的帮助下,这些函数并不难完成。
需要注意的是,调用page_alloc()
后,通常需要给新的Page
的pp_ref++
;而调用page_free()
前,需要给该Page
的pp_ref--
。
虚拟内存管理
在 R3000 上,虚拟地址映射到物理地址,随后使用物理地址来访存。与我们实验相关的映射与寻址规则(内存布局)如下:
- 若虚拟地址处于 0x80000000~0x9fffffff (kseg0),则将虚拟地址的最高位置 0得到物理地址,通过 cache 访存。这一部分用于存放内核代码与数据结构。
- 若虚拟地址处于 0xa0000000~0xbfffffff (kseg1),则将虚拟地址的最高 3 位置0 得到物理地址,不通过 cache 访存。这一部分用于映射外设。
- 若虚拟地址处于 0x00000000~0x7fffffff (kuseg),则需要通过 TLB 来获取物理地址,通过 cache 访存。
kseg0
对于处于kseg0
内的虚拟地址,我们可以直接用如下两个宏来访问对应物理地址:
1 |
|
可以看到,在该区间内的虚拟和物理地址映射较为简单,就是最高位置1置0的区别。在转换之前,是有判断机制的,即kva
应当大于0x80000000
,否则不能使用该宏转换;pa
对应的物理页框不能大于npage
,其本质也就是pa
要小于最大物理内存64MB
,否则这个物理地址也是不合法的。
而我们的实验重点在于kuseg
段,需要填写二级页表,并通过TLB
来访问物理地址。
kuseg
二级页表的填写
这是虚拟内存管理的重点,也是个人认为整个lab2中最难的部分。涉及到二级页表的相关知识,以及虚拟地址和物理地址的区别和联系。
首先借助指导书中的图便于理解二级页表的结构:
整个二级页表的填写分为两个部分,使用两套配对的函数,时间分界点在于page_init()
,但是整体过程是类似的:
- 有变量
va, pa, pgdir, pgdir_entry, pgtable, pgtable_entry
,对应关系如上图所示,这些变量都是指明对应的地址,并不是地址里存的实际值 - 整个填写二级页表的流程大体如下:
- 计算一级页目录入口
pgdir_entry = pgdir + PDX(va)
- 由一级页目录入口获得二级页表基地址
pgtable = KADDR(PTE_ADDR(*pgdir))
- 计算二级页表入口
pgtable_entry = pgtable + PTX(va)
- 为二级页表入口设置对应的物理地址页号
*pgtable_entry = PPN(pa) or *pgtable_entry = PPN(page2pa(page))
- 计算一级页目录入口
- 在
page_init()
函数之前,使用boot_pgdir_walk()
和boot_map_segment()
函数完成填写,在其之后则使用pgdir_walk()
和page_insert()
函数完成填写。其中都是由后者调用前者,前者完成图示中的前三步,由后者完成第四步。
TLB的填写
所有的在低 2GB 空间的访存操作都需要经过 TLB。
TLB的重填本身是比较复杂的,不过本实验中并没有要求这个内容,只是要求完成tlb_out
函数,这在理解TLB重填的过程后并不难,这里就不再展开了。但是理解TLB重填的过程还是十分重要的,在后续的实验中还会多次涉及。
实验心得与总结
只能说lab2的难度比lab1又高了一个台阶,开启了真正的地狱模式。
即使是在lab2课下拿到了满分之后,对于二级页表映射,虚拟地址和物理地址还是不能完全搞明白。
直到lab3做了一部分以后,可能和lab2对照着看,终于算是理清了头绪。