xv6实验2-内存管理单元(文档)

介绍

这个实验,需要写操作系统的内存管理单元代码。内存管理单元有两个组件,第一个组件是内核的物理内存分配器,以便于内核可以分配和释放内存。分配器以4096字节为单位,称为页表。你的任务是维护一个记录已分配和空闲物理页表的数据结构,还有多少进程在共享已分配的页表。同时也要写进程来分配和释放内存页

第二个组建是虚拟内存,虚拟内存是把内核使用的虚拟地址映射到用户程序的物理内存地址。x86硬件内存单元(MMU)在指令执行内存时映射,查询一组页表。修改JOS已经提供的代码去实现内存管理单元

开始

在这个和接下来的实验中,你会构建你的内核。我们也提供一些额外的代码。为了拿到源代码,使用Git提交你在lab1中修改的,拉取最新的代码,然后创建一个本地分支lab2

cd jos # 进入到jos目录
git pull # 拉取最新代码
git checkout -b lab2 origin/lab2 # 切换到lab2

git checkout -b命令做了两件事:1. 根据origin/lab2创建一个本地分支lab2,2. 修改了本地目录到lab2分支。Git也允许通过命令git checkout branch-name直接切换到一个存在的分支

需要合并lab1中已经提交的代码

git merge lab1

在某些情况下,Git可能不能自动合并,这种情况下git merge会告诉你哪些文件冲突了,你需要手动解决冲突,然后提交git commit -a

lab2包含下面几个新的源码文件

inc/memlayout.h
kern/pmap.c
kern/pmap.h
kern/kclock.h
kern/kclock.c

memlayout.h描述了虚拟地址空间的结构,虚拟地址空间必须通过修改pmap.c文件去实现.memlayout.pmap.h定义了PageInfo的结构体,可以使用PageInfo结构体定位到哪些物理内存是空闲的。kclock.hkclock.c操控着PC电池供电的时钟和CMOS RAM硬件,BIOS记录了PC容纳的物理内存总量和其他信息。pmap.c代码需要读取这些设备,以便于计算出有多少物理内存,但是这部分代码已经完成了,你不需要关心CMOS是怎么工作的

值得一提的是memlayout.hpmap.h,因为这个实验要求使用和理解这两个文件中包含的大量定义,你可能想阅读inc/mmu.h,因为它也包含了大量关于这个实验的定义

实验要求

这个就不翻译了,大概就是实验的要求,要求完成所有常规练习和至少一个挑战问题,挑战问题完成越多越好。然后还有答案的文件要求,反正就是跟评分相关的,就没有必要翻译了

提交程序

这部分也不翻译了,也是跟评分相关,就是教你怎么上传代码和评分

物理页管理

操作系统必须跟踪物理RAM的空闲和使用内存。JOS用页面粒度(page granularity)管理计算机内存,以便于它可以使用MMU来映射和保护每一块分配的内存

现在需要实现物理页分配器。它在PageInfo结构体中用一个链表结构记录了空闲页面(与xv6不同的是,它没有嵌入到空闲页自身中去),每个对应一个物理页。在实现其他虚拟内存之前需要实现物理页分配器,因为页表管理代码需要分配物理内存来保存页表

进行练习1

整个6.828实验都需要你做一些尝试性的工作来理解你需要做什么。这个作业没有描述所有你需要添加到JOS的代码的细节。在JOS源码组件中查找你需要修改的注释。这些注视包含一些提醒和注意的地方。你也需要看下JOS相关的组件,Intel手册,也可能是6.004或6.033的笔记(最后两个应该是学校相关的课程)

虚拟内存

在开始之前,熟悉一下x86的保护模式内存管理架构:段和页转换

进行练习2

虚拟地址、线性地址和物理地址

在x86系统中,虚拟地址是由段描述符和段偏移值组成的。线性地址是虚拟地址通过段转换得到的,物理地址是线性地址通过页转换得到的,物理地址是最终通过硬件总线到达RAM的地址

          Selector  +--------------+         +-----------+
          ---------->|              |         |           |
                     | Segmentation |         |  Paging   |
Software             |              |-------->|           |---------->  RAM
            Offset   |  Mechanism   |         | Mechanism |
          ---------->|              |         |           |
                     +--------------+         +-----------+
            Virtual                   Linear                Physical

一个C指针是虚拟地址的偏移组件。在boot/boot.S中,已经初始化了全局描述符表(GDT),通过把段基地址设为0且把取址最大值设为0xffffffff来关闭全局描述符表,因此段描述符没有作用,线性地址始终是虚拟地址的偏移值。在lab3,必须要用分段来设置优先权,但是对于内存转换来说,我们可以忽略,仅仅需要关注页转换

回到lab1的第三部分,已经初始化了一个简单的页表,以便于内核可以运行在0xf0100000链接地址,尽管实际上物理内存地址0x00100000是仅仅在ROM BIOS之上。这个页表仅仅只映射了4MB的内存大小。在JOS的虚拟地址空间结构中,将会映射物理内存的前256MB内存到虚拟地址,虚拟地址起始地址为0xf0000000

进行练习3

一旦使用了保护模式(boot/boot.S中切换的), 所有在CPU中执行的代码都无法使用线性地址或物理地址。所有的内存都只能使用由MMU转换的虚拟地址,这也意味着所有的C语言指针都是虚拟地址

JOS的内核经常需要将地址作为不透明值或数值操作,而不是解引用。有时这些是虚拟地址,有时这些是物理地址,为了帮助记录代码,JOS源码区分了这两种情况:uintptr_t代表不透明值,physaddr_t代表物理地址。这两个类型都是32位整型类型(uint32_t),所以编译器不会阻止你把其中一个类型给另一种类型,因为都是整型,如果尝试解引用,编译器会尝试告警

JOS内核可以解引用uintptr_t通过第一次强制转换给一个指针类型。相比之下,内核不能合理的解引用一个物理地址,因为MMU翻译所有的内存引用。你可以强制把physaddr_t转换成一个指针并且解引用,你能加载并保存到结果地址(硬件不会中断它,因为它是一个虚拟地址),但是你不能获得你想要的内存

总结来说:

C类型 地址类型
T* 虚拟地址
uintptr_t 虚拟地址
physaddr_t 物理地址

进行问题

JOS内核有时需要读取或修改仅知道物理地址的内存。例如添加一个映射到页表可能需要分配物理内存保存页目录,然后初始化内存。然而内核不能直接使用虚拟地址转换,因此不能直接加载并保存到物理地址。一个原因是JOS重新映射所有物理内存能够帮助内核为已经知道的物理地址读写内存。为了转换内存地址到内核能够直接读写的虚拟地址,内核必须添加0xf0000000到物理地址,以便于在映射区域找到相关虚拟地址。你应该使用KADDR(pa)来做到这一点

JOS内核有时也需要将给定的虚拟地址(保存内核数据结构的内存)转换成物理地址。内核全局变量和由boot_alloc分配的内存在内核加载区域(从0xf0000000开始), 也就是我们要加载所有物理内存。因此,为了把虚拟地址转换成物理地址,内核只用减去0xf0000000。你可以使用PADDR(va)来做到这一点

引用计数

在后面的lab中,我们经常有相同的物理内存映射到不同的虚拟地址。需要给每一个物理页保存一个引用计数,具体是struct PageInfo中的pp_ref字段。当计数器变为0的时候,物理页就可以被释放了,因为不会再使用了。通常,计数器和物理页在所有页表中出现在UTOP下面的次数相等(在UTOP上面使用的通常是在启动时候,由内核分配的内存,永远不会被释放,所以没有必要对其进行引用计数)。还可以用引用计数来跟踪指向目录页的指针数量,进而跟踪页目录对页表的引用次数

小心使用page_allocpage_alloc总是返回引用计数为0的页面,所以只要用页面完成了某件事(比如插入一个页表),就应该增加pp_ref的计数。有时,这些可以用其他函数处理(比如page_insert),有时直接用page_alloc完成

页表管理

现在需要写一些代码来管理页表:插入和移除线性到物理的映射,如果在有必要的时候,创建页表

进行练习4

内核地址空间

JOS把处理器32位线性空间分成了两部分。我们将在lab3中开始加载和运行用户环境(进程),用户环境将控制下半部分的布局和内容,内核总是保留整个上半部分的控制权。分界线由inc/memlayout.hULIM定义,为内核保留了将近256MB给虚拟地址。这也解释了为什么在lab1中,我们需要给内核那么高的线性地址:否则在内核的虚拟地址映射到用户环境就没有足够的空间里

inc/memlayout.h中关于JOS内存布局图是非常有用的

权限和错误隔离

因为内核和用户内存在每一个环境的地址空间都存在,所以在x86的页表中使用权限位,以便于控制用户代码只能使用用户部分的地址空间。否则在用户代码的bug可能会修改内核数据,从而造成崩溃或更多的轻微的错误;用户代码还可能窃取其他环境的私有数据。请注意:可写的权限位(PTE_W)可能影响用户和内核代码

用户环境没有ULIM之上的任何内存的权限,然而内核能读写这些内存。[UTOP, ULIM]范围内的地址,内核和用户环境有相同的权限:他们都可以读,但是不能写这块内存。这部分地址是用来暴露某些只读的内核数据给用户环境的。最后,低于UTOP的地址空间是给用户环境使用的;用户环境将设置此内存的访问权限

初始化内存地址空间

现在你可以设置UTOP上面的地址空间:地址空间的内核部分。inc/memlayout.h展示了你使用的布局。你可以使用刚刚写的函数来设置适当的线性地址到物理地址的映射。

可选的地址空间布局

JOS的地址空间布局不是唯一的。操作系统可能把低线性的地址作为内核空间,高地址作为用户空间。然而,x86内核通常不这么干,因为由于x86向后兼容的特性,比如虚拟的8086模式,是硬连接的方式使用底部的线性空间地址,因此如果内核也映射到这个部分,将无法使用

尽管非常难,但是也有可能将内核设计成不为自己保留固定区域的线性或虚拟地址空间,而是有效利用用户进程不受整个4GB地址空间的限制,地址空间-仍然能够保护内核和用户进程不受影响

上一篇:《Android高性能编程》| 每日读本书


下一篇:Dubbo 在 K8s 下的思考