文章目录
指导书梳理
内核程序启动
mips_init(u_int argc, char **argv, char **penv, u_int ram_low_size)
- 参数是由 bootloader 传递给内核的,其中的
ram_low_size
参数指定了硬件可用内存大小
- 参数是由 bootloader 传递给内核的,其中的
物理内存管理
链表宏
-
LIST_ENTRY(type)
,作为一个特殊的类型出现,例如可以进行如下的定义:LIST_ENTRY(Page) a;
它的本质是一个链表项,包括指向下一个元素的指针
le_next
,以及指向前一个元素链表项 le_next 的指针le_prev
。le_prev 是一个指针的指针,它的作用是当删除一个元素时,更改前一个元素链表项的 le_next- 黑框框起来的是一个元素(结构体)
- 蓝&橙部分是一个
LIST_ENTRY(type)
类型的链表项,是元素(结构体)的一个字段
-
C 语言并没有泛型的语法,因此需要通过宏另辟蹊径来实现泛型
虚拟内存管理
两级页表结构
-
对于一个 32 位的虚存地址,从低到高从 0 开始编号,其 31-22 位表示的是一级页表项的偏移量,21-12 位表示的是二级页表项的偏移量,11-0 位表示的是页内偏移量
-
页表项组成
在 MOS 中,每个页表均由 1024 个页表项组成,每个页表项由 32 位组成,包括 20位物理页号以及 12 位标志位。
高 6 位硬件标志位用于存入 EntryLo 寄存器中,供硬件使用,低 6 位软件标志位不会被存入 TLB 中,仅供软件使用。当页表项需要借助 EntryLo 寄存器填入 TLB 时,页表项会被右移 6 位,仅将高 20 位物理页号以及 6 位硬件标志位填入 TLB 使用
访问内存与 TLB 重填
- TLB组成:每个 TLB 表项都有两个组成部分,包括一组 Key 和两组 Data
- TLB 事实上构建了一个映射 < VPN, ASID > ⟶ T L B \stackrel{TLB}{\longrightarrow} ⟶TLB< PFN, N, D, V, G >。
EntryHi、EntryLo0、EntryLo1
都是 CP0 中的寄存器,只是分别对应到 TLB 的 Key与两组 Data,并不是 TLB 本身。
- EntryLo0、EntryLo1 拥有完全相同的位结构, 分别存储 Key 对应的偶页与奇页
- 4Kc 中的 TLB 采用奇偶页的设计,使用 VPN 中的高 19 位与 ASID 作为
Key,一次查找到两个 Data(一对相邻页面的两个页表项),并用 VPN 中的最低 1 位在两个 Data 中选择命中的结果。因此在对 TLB 进行维护(无效化、重填)时,除了维护目标页面,同时还需要维护目标页面的邻居页面
- 4Kc 中的 TLB 采用奇偶页的设计,使用 VPN 中的高 19 位与 ASID 作为
- Key——EntryHi
- VPN(Virtual Page Number)
- 当 TLB 缺失(CPU 发出虚拟地址,TLB 查找对应物理地址但未查到)时,EntryHi 中的 VPN **自动(由硬件)**填充为对应虚拟地址的虚页号
- 当需要填充或检索 TLB 表项时,软件需要将 VPN 段填充为对应的虚拟地址
- VPN(Virtual Page Number)
- Data——EntryLo
-
PFN:Physical Frame Number
- 软件通过填写 PFN,接着使用 TLB 写指令,才可以将此时 EntryHi 中的 Key与 EntryLo 中的 Data 写入 TLB
-
D:Dirty
事实上是可写位。当该位为 1 时,对应的页可写;否则对相应页的任何写操作都将引发 TLB 异常。
-
G:Global
如果该位为 1,则 CPU 发出的虚拟地址只需要与该表项的 VPN 匹配,即可与此 TLB 项匹配成功(不需要检查 ASID 是否匹配)
-
TLB 相关指令
- tlbr:以 Index 寄存器中的值为索引,读出 TLB 中对应的表项到 EntryHi 与 EntryLo0、EntryLo1
- tlbwi:以 Index 寄存器中的值为索引,将此时 EntryHi 与 EntryLo0、EntryLo1 的值写到索引指定的 TLB 表项中
- tlbwr:将 EntryHi 与 EntryLo0、EntryLo1 的数据随机写到一个 TLB 表项中(使用 Random 寄存器来“随机”指定表项)
- tlbp:根据 EntryHi 中的 Key(包含 VPN 与 ASID),查找 TLB 中与之对应的表项,并将表项的索引存入 Index 寄存器(若未找到匹配项,则 Index 最高位被置 1),前后都应各插入一个 nop 以解决数据冒险
TLB的维护
具体来说,维护 TLB 的流程如下:
- 更新页表中虚拟地址对应的页表项的同时,将 TLB 中对应的旧表项无效化
- 在下一次访问该虚拟地址时,硬件会触发 TLB 重填异常,此时操作系统对 TLB 进行重填
时纪
- 注意检查空间是否超出了最大物理地址
- E2_3
-
LIST相关宏操作的(参数)一般都是指针啊啊啊啊!!在使用的是后如果传入的不是指针记得取地址!!
-
从物理地址 0x400000 开始分配物理内存,用于建立管理内存的数据结构,获取方式为
extern char end[]; startAdd = (u_long)end;
-
循环又没写i++
-
- E2_4
- 异常返回值是
-E_NO_MEM
,题目没给出去相关板块找!!(找调用了这个函数的模块,看条件判断部分,就能得知异常返回值)
- 异常返回值是
使用 tlb_invalidate 函数可以实现删除特定虚拟地址的映射,每当页表被修改,就需要调用该函数以保证下次访问相应虚拟地址时一定触发 TLB 重填,进而保证访存的正确性。
然而在 MOS 中,一旦物理页全部被分配,进行新的物理页分配时并不会进行任何的页面置换,而是直接返回错误,即在对应 page_alloc 函数中返回 -E_NO_MEM。
- E2_6
- 虚拟内存管理中的很多地址相关宏传入参数是u_long而不是指针,使用宏时注意看宏具体的实现
- PTE_V 等并不是有效位的值1/0,而是已经把1/0填在相应位的与地址位数对齐的值(其他位为零),所以可以用 | 实现地址的生成
- 移位运算的优先级比加法运算低
- E2_7
- 只要修改了页表(不管是改页面映射关系还是改权限),就需要调用 tlb_invalidate 函数删除特定虚拟地址的映射,不然TLB和页表信息会不同步
- va 是二级页表页面的虚拟地址!不是数据页
- 调试出错,发现makefile错了,追根溯源,发现应该是添加注释的时候输了中文空格,造成文件出错了……
exam前准备
提醒
- 注意有效位
PTE_V
的检验
参数、宏、函数
缩写对照
- Pde 一级页表项类型
- Pte 二级页表项类型
地址相互转换相关
- kseg0 处虚地址 与 物理地址 的转换用的是宏(PADDR、KADDR),其他基本上都是函数(pa2page等)
从地址中获取信息
都是宏、在mmu.h
中
函数作用
-
pgdir_walk
VSpage_lookup
都在相应的页表中找到对应的页表项,将其地址赋给*ppte
pgdir_walk
还可以创建新的页表项page_lookup
重点在返回虚拟地址映射到的页控制块
Exam翻车分析
题目理解出现偏差——理解错题意&以为实现了自映射机制
-
理解错题意 :*pte是页表项本身的虚拟地址,而题目里的va指的是给定的虚拟地址,要根据这个va范围内的虚拟地址到页表中去找相应的物理页框们,这个范围不是对页表项本身虚拟地址范围的限制
-
以为实现了自映射:lab2中的内核页表并没有实现自映射机制,所以va连续且递增并不代表它们对应的pte也是连续且递增的,因此不能把va转换成pte后比较
-
正确的几个pte
-
把va的上下界转换成pte,然后用来限制遍历的pte:因为没实现自映射,pte不像va一样连续且递增,所以pte上下界并不与va的上下界对应,导致有些正确pte会被判为不在范围内
【疑问】页表在虚拟内存中不应该是连续的吗,这样怎么保证其连续性?
展开
*ppte
表达式*ppte = (Pte*)(KADDR( PTE_ADDR(*pgdir_entryp) + (PTX(va)<<2) )) = (Pte*)( page2pa(pp) + PTX(va)<<2 + 0x80000000 )
然而,根据映射机制【3】
*ppte = (Pte*)PTbase + VPN(va) = (Pte*)(PTbase + (VPN(va)<<2)) = (Pte*)(PTbase + ( (PDX(va)<<10 + PTX(va)) <<2)) = (Pte*)( PTbase + PDX(va)<<12 + PTX(va)<<2 )
两者应该相等,那怎么保证下式成立呢?pp是alloc随便分配的页面啊?
page2pa(pp) + 0x80000000 = PTbase + PDX(va)<<12
Lab2、内核页表没有实现页表自映射,因此【3】不成立
【延伸】页表到底存储在哪?
页目录和页表都存储在kseg0。因为页表是内核数据结构,需要存储在内核这部分的区域中。内核在读写页表时,就是通过虚拟地址中的kseg0段进行的。
在lab3中,会为每个进程创建一个页表并利用kuseg实现自映射,在完成自映射后,用户进程就可以通过kuseg访问内存中自己的页表,而内核通过kseg0管理所有进程的页表
-
在某些操作系统中,会分进程页表和内核页表,可以理解为内核页表用于内存的实际管理,而进程页表是一种方便用户查阅的机制。
MOS的设计中,内核自身没有页表
代码编写(指针使用)不够规范
- 指针解引用前必须先验证其非NULL,野指针会导致卡死