MIT JOS lab2内存管理实验记录

本次Lab主要完成JOS中关于虚拟内存映射、页表管理和分配的几个函数,旨在加深对内存管理的认识。

代码链接

https://download.csdn.net/download/qhaaha/13741540

“兵欲善其事,必先利其器“,在开始完成exercise之前,先从inc/mmu.h中几个比较底层的宏和函数看起,方便以后coding。(可跳过,有书签索引。)

准备阶段

1.基本概念

首先,地址映射有以下虚拟地址—线性地址—物理地址的结构:

在这里插入图片描述

分段机制把逻辑地址转换成线性地址,而分页则把线性地址转换成物理地址。

在这里插入图片描述

这是线性地址的格式,mmu.h中画出来的,里面还定义了一些基本的宏。

2. 内存的初始化和映射

三种地址
逻辑地址:程序看到的地址,其实是一个相对地址,加上段基址就可以变为线性地址。
线性地址:也即虚拟地址,在实验说明中说道:…setting all segment base addresses to 0 and limits to 0xffffffff. Hence the “selector” has no effect and the linear address always equals the offset of the virtual address. 意思是将所有的段基址设置为0,也就是说线性地址永远等于段偏移—即逻辑地址。
物理地址:实际存储的物理地址,具体表示形式不是这个实验所关心的,需要知道的是20个二进制位就可以表示出一个物理地址。
再配上前面贴过的图:

在这里插入图片描述

也就是说,JOS中需要完成的是线性地址到物理地址的映射,即paging mechanism部分。JOS采取的是二级页表结构。什么是页表?
页表
内存空间被分成同样大小的”页“,以页为单位进行线性地址到物理地址的映射,映射的表叫做页表。32位寻址的计算机中,页的大小一般为2^12 = 4096个bytes(后面会看到这么做的好处)。
不难看出,最低的12位就是页内地址,不需要参与映射,我们要做的就是把线性地址的高20位映射到内存对应的物理基地址处就可以啦。一种简单的方法是一个大小为2^20的条目数组(连续内存空间),原则上数组的每个条目是(4B,32位)(虽然理论上只需要每个条目20位的物理基地址就可以完成映射,但是实际应用中还需要12位的诸如页面是否存在等的属性信息)。这样,当我们拿到一个线性地址,只需要用前20位做索引“查表“,找到物理基地址,再加上后12位的偏移量就可以啦。这就是单层页表结构,如下图1。
双层页表
另外一种设计是采取双层页表结构。第一层页表称为页目录(page directory),用线性地址高10位作为索引,共2^10个条目,每个条目是一张第二层页表(简称页表,page table)的地址,每张第二层页表用中间10位索引,共2^10个条目,代表物理页的基地址。这个过程如下图2(引用自:https://www.cnblogs.com/vinozly/p/5703215.html)。
在这里插入图片描述

图表 1单层页表
在这里插入图片描述

图表 2双层页表

双层页表有哪些好处呢?首先,使用多级页表可以使得页表在内存中离散存储。其次,使用一级页表,需要连续的内存空间来存放所有的页表项;多级页表通过只为进程实际使用的那些虚拟地址内存区请求页表来减少内存使用量。

当然,现在也能明白4096个bytes为一页的巧妙之处:这样在双层页表中,每个页表有10位的索引,每个索引对应条目大小4B,这样一张页表的大小为2^(10 + 2) = 4096个bytes,正好是一页的大小!

3.虚拟地址空间和其中的UVPT(后面会用到一点点,可先略过)

主要根据memlayout.h中的几个宏,看一下虚拟地址空间:
从高地址到低地址列出主要部分
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

看到memlayout.h中这样一段注释:
The page directory entry corresponding to the virtual address range [UVPT, UVPT + PTSIZE) points to the page directory itself. Thus, the page directory is treated as a page table as well as a page directory.
说的是在页目录中对应于线性地址UVPT到UVPT+ PTSIZE这一部分的entry并没有指向其对应的页表,而是指向页目录本身。要想理解这么做的妙处,首先要明白以下几点:
1.一般程序能直接使用到的,都是逻辑地址(本实验中也即线性地址),真正的物理地址对于用户程序(和对内核的大部分也希望如此)是不可见的。
2.当一个进程使用一个逻辑(线性)地址时,操作系统根据该进程的页表自动实现到物理地址的转换。

在这种情况下,页表条目对于使用线性地址的程序而言,是难以访问的到的,因为它无法使用页表的物理地址来访问它,所有地址会被按照线性地址的方式来翻译。
而这种指针自指的方法解决了这个问题,可以看到,虚拟地址uvpt[N],就会被页表自动翻译为页目录中第N张页表的首地址。其实现的原理为:声明uvpt为32位型的指针,uvpt首先被翻译为directory数组的首地址,其每个条目为32位,代表一个页表指针。这样,uvpt[N]就自然而然地被翻译为了第N个条目的值,也就是N号页表的地址!
这样我们就实现了用线性地址来访问页表了。当然,还可以用uvpt[uvpt]来访问“uvpt号页表“—也就是页目录!uvpt[uvpt]实际上对应的线性地址为:uvpt + (uvpt >> 10),这里代码注释里有一个错误,它右移了12位,应该是10位。附上图:
在这里插入图片描述

exercise 1

移步kern/pmap.c。
先看一下i386_detect_memory:这是一个用来在一开始检测物理内存空间的函数,大致就是为了得到这两个全局变量:
在这里插入图片描述

首先通过调用底层的硬件相关函数得到内存信息:
在这里插入图片描述

第一个是基本内存(640KB),中间是扩展内存(就是物理内存条,你假设是1G,它就显1G出来) ,最后是总内存(基本内存加扩展内存)。

可以看到,i386_detect_memory函数中计算出totalmem后:
在这里插入图片描述

计算了页总数和basemem中的页总数,保存在了全局变量中。

然后开始完成练习

1.boot_alloc

然后就是要我们自己完成的boot_alloc函数了,注释已经说了这个函数是“for JOS setting up its virtual memory system”,目标是在jos初始化过程中,分配n个byte的物理内存空间,返回虚拟地址,但它不负责初始化物理内存,也不构建映射,这些由后面的page_alloc完成。

这里用到了虚拟地址空间,如图:
在这里插入图片描述

根据这张图,boot_alloc的工作就迎刃而解了。这里注意几个地址:
KERNBASE:指的是虚拟地址空间中内核部分开始的地方。
end是内核内存中的代码段、bss段等结束的地方,也即使free memory开始的地址,代码中用一个外部变量end[]直接获得,(“magic”)。

boot_alloc中是用一个静态变量nextfree来保存已经分配到的物理内存对应的虚拟地址的,也可以说是下一个空闲内存的内核虚拟地址。
所以我们就只需要按照要求分配内存,增加nextfree,并判断是否超过limit就行了:
在这里插入图片描述

2.mem_init

然后来看mem_init,,这个可以说是这个lab代码的主线任务。它首先调用了i386_detect_memory() 获得剩余的内存空间信息(npages & npages_basemem),然后分配了内核页表(内核逻辑地址到物理地址的映射信息):
在这里插入图片描述

然后有这样一句话
在这里插入图片描述

这句话的原理可以回顾前面准备阶段第二部分的介绍,用到了前面准备阶段第三部分说到的“自指针“方法,显然就是定义那个到自己的”虚拟页表“,同时在状态位上标明状态。

然后到了自己写的部分了,构建npages个PageInfo对象,他们每个对应一个内存页。pageinfo是内核定义的数组用来映射一个物理页,使得在运行过程中内核可以跟踪并管理它们。我的代码:

在这里插入图片描述

3.page_init

然后mem_init调用了page_init,这也是需要我们修改的函数,进去看一下。
我们需要做的是模仿示例的格式初始化内存,在示例中将所有物理内存标记为了free,我们要按照注释的要求修改它。
在这里插入图片描述
在这里插入图片描述

这里要注意扩展内存中已经被用到的pages的获取方法:用以0为参数的boot_alloc函数可以直接获得已经被分配的物理内存结尾地址。
在这里插入图片描述

现在主线任务mem_init已经完成了前半部分,后半部分是虚存与物存映射,要用到几个同样需要我们完成的函数,所以我先接着去做支线任务,把练习1的其他几个函数完成了再回来。

4.page_alloc

page_alloc()函数用来分配一页,从哪分配?肯定是page_free_list中的页。page_free_list是一个PageInfo型的链表,其中每个元素都对应一个物理页,从链表中拿出一页分配出去非常简单,主要问题是memset对应的物理页,需要我们找到该页的物理地址,也就是说需要将PageInfo指针转化为对应的物理页地址。同时要注意到的是,前面已经提到过,在C语言程序中访问一个物理页用到并不是直接的物理地址,我们如果要在page_alloc中访问分配出的物理页,需要将其转化为kern中的虚拟地址,换言之,我们拿到物理地址后还需要做一次kern page的逆映射,找到我们最终要用的地址(逻辑地址)。
实际上,为了方便内核实现这种逆映射,从线性地址0xf0000000开始,直接映射到了从0开始的整个物理内存,这JOS注释中称这种做法非常的“magic”。
可以用到pmap.h中已经为我们准备好的函数帮忙:
在这里插入图片描述

其中page2pa就完成了PageInfo* 到 物理地址的转化,KADDR就是内核页表的“逆映射”。说起来比较绕,上图:

在这里插入图片描述

代码:
在这里插入图片描述

5.page_free

释放一页,这个属实没什么可说的:

在这里插入图片描述

练习1就完成了。

exercise 4

1.pgdir_walk()

pgdir_walk()根据虚拟地址找到对应的页,代码流程注释已经说的很清楚了,有就返回,没有如果create为1就创建,否则NULL,先上代码:
在这里插入图片描述

这里返回值计算的思路:
在这里插入图片描述

(1)pgdir[pdx]通过pdx作索引查找页目录找到了该虚拟地址对应的内核页表。
(2)pgdir[pdx](作为页目录中的一项)有32位,其中除了一些标记位外,有20位是用来存储物理地址的。PTE_ADDR的功能就是通过一个页目录项返回对应的物理地址。
(3)找到页目录的物理地址后,需要找到对应的页表项,这里由于是物理地址,就不能简单的用数组的索引“页表[ptx]”了,只能老老实实加上ptx个32位,这样就得到了要求的页表项的物理地址。
(4)KADDR函数的作用是根据物理地址得到内核页表对应的虚拟地址,前面已经介绍过。
这样我们就得到了最后的返回值了。
如果不存在这一页并且create等于1,那么就调用之前写的page+alloc函数分配一页,这里有一个非常好用的函数page2pa,它能直接将一个pageinfo*型的对象直接转化为需要的20位物理地址。需要注意的是页表项中不仅需要20位物理地址,还要按照要求把几个参数赋值。

2.boot_map_region

boot_map_region这个函数将va 开始的size大小虚拟内存和pa 开始的size大小的物理内存做映射,写入页表项中。查表动作可以借助刚刚完成的pgdir_walk函数:
在这里插入图片描述

这里大胆使用pa开始的size大小物理内存,没有考虑访问权限的问题,调用它的函数要考虑这一点,同时这个函数的参数需要是页对齐的。

3.page_lookup

接下来的page_lookup也很简单,已经完成的pgdir_walk已经将虚拟地址va对应的pte(page table entry)返回,page_lookup主要目的只是更近一步把pte对应的那一页返回而已。
在这里插入图片描述

这里注意这个函数只是“查找”,不会“创建”,要调用第三个参数为0的pgdir_walk。

4.pg_remove

pg_remove删除虚拟内存va与对应的物理页的映射关系。简单来看只需要调用pg_lookup,把找到的pte(保存在第三个参数中)处的内容清0就可以啦,但是注释提醒到还要考虑到问题:
(1)物理页的ref要减一;
(2)如果该物理页ref变为0,释放它;
(3)还要删除TLB,TLB—translation lookaside buffer, 基本就相当于页表中的缓存,用来加快查表速度的。现在,map不存在了,如果这个map被缓存了,自然也要删掉。

对于(2)(3)都不用担心,给出的函数已经帮我们封装好了,Hint里有提示。

在这里插入图片描述

5.page_insert

page_insert将物理页pp(的首地址)对应于虚拟地址va 。
在这里插入图片描述

这里按照提示可以不考虑本来页表中就有这个虚拟地址并且正好映射的就是物理页pp的情况,可以删除之后再添加回来。不过要注意到是:page_remove函数在ref被减少到0时会自动free这一页,为了防止在上述情况下错误地free了这一页的情况,我在remove前先把pp_ref++了。

exercise 5

有了这些个帮助完成内存映射的函数,就可以回到mem_init中完成接下来的部分也就是练习5,这个需要参考上面画出来的JOS虚拟内存图(memlayout.h)。
首先为PageInfo的数组分配物理内存,用bootalloc函数:
在这里插入图片描述

完成pages和pages的副本UPAGES的映射,注意提示中的权限设置:
在这里插入图片描述

然后分配内核栈,对应物理地址bootstack,没什么可说的:
在这里插入图片描述

接下来,前面已经提到过 :“实际上,为了方便内核实现这种逆映射,从线性地址0xf0000000开始,直接映射到了从0开始的整个物理内存”,接下来就完成这一步。
在这里插入图片描述

这样代码部分就完成了。

运行截图

在这里插入图片描述

补充函数

利用page_lookup,函数主要代码如下:
在这里插入图片描述
在这里插入图片描述

运行截图:
在这里插入图片描述

问题回答

(1) 程序中的地址从什么时候开始都是虚拟地址了,请找到那几行代码。

这个问题的答案在内核的第一个文件kern/entry.S中,entry.S是第一个真正意义的C语言函数之前就会执行的代码,也是操作系统0号进程开始的地方。最开始的虚拟地址并不是内核进程虚拟地址空间的虚拟地址,而是一个名叫entry_pgdir页目录下保存的虚拟地址空间,这个页目录在编译时就已经显式地定义完成,仅用于最开始的地址映射。
看entry.S的第57行,这里将entry_pgdir的物理地址装载进寄存器cr3中,前面已经提到过说cr3寄存器的作用:保存当前页目录物理基地址,查表时使用。不难看出从这里开始已经是虚拟地址了。
在这里插入图片描述

不过这里的虚拟地址并不是大部分内核代码中使用的内核虚拟地址空间,可以看到entry.S执行结束后调用了i386_init函数,其中的mem_init函数完成了内核页表的构建(不同于entry_pgdir,这个是运行时构建的),mem_init的第222行通过修改cr3寄存器完成了页表的切换。由此之后的内核进程使用内核虚拟地址空间。
在这里插入图片描述

(2) mem_init()函数中 kern_pgdir 的虚拟地址是多少?物理地址呢?在我们还未完成本

次 lab 之前,为什么我们已经可以使用虚拟地址了?
在mem_init 中加一行打印一下虚拟地址,然后用之前完成的showva2pa函数看看对应的物理地址:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可以看到虚拟地址是0xf011 b000, 对应的物理地址为 0x11b000。
实际上这就是free memory开始的第一个页(如下图),也是内核data和text段结束之后的地方,物理地址为虚拟地址减去kernbase。
在这里插入图片描述

上述物理地址是kern_pgdir唯一的存储位置,但是能够访问到它的虚拟地址却不止物理地址+kernbase这一个,在准备阶段—第三部分中我详细写了UVPT开始PTSIZE大小的虚拟地址空间保存的内容,结论是这PTSIZE虚拟空间对应的是页目录中所有的页表(总大小2^10 * PGSIZE,刚好是PTSIZE的大小)。所以kern_pgdir在这个空间内自然也有对应的一个虚拟页,在准备阶段–第三部分已经计算过,就是UVPT + UVPT>>10这个虚拟地址。
经过计算这个虚拟地址为0xef7bd000,可以用monitor验证一下,果然也是kern_pgdir。
在这里插入图片描述

综上,kern_pgdir物理地址为0x11b000,虚拟地址为0xf011 b000和0xef7b d000。

在我们还未完成本次 lab 之前,为什么我们已经可以使用虚拟地址了?
这个问题的答案在前面已经提到的kern/entrypgdir.c中,entry_pgdir是在内核开始阶段使用的页目录,看这段注释:
在这里插入图片描述

它完成了虚拟地址 [kernbase,kernbase+4MB) 到物理地址 [0,4MB) 的简单映射,这些内存空间足以帮助内核最开始的C语言程序(需要使用虚拟地址)完成真正内核页表的构建。entry_pgdir是这样显式构建出来的:
在这里插入图片描述

…等等

所以在内核页目录被构建之前,我们也可以使用这4MB的虚拟地址空间了。

(3) 哪一行代码使得本次 lab 所构建的虚拟内存系统真正被使用?请指出它的位置。

mem_init 中的这一行: 在这里插入图片描述

(4) 此操作系统可支持的最大物理内存是多少?为什么?

(错误的理解)前面提到过在i386_detect_memory中计算了物理内存的大小,以kbytes为单位保存在totalmem中,然后根据此计算量总页数npages。
在这里插入图片描述

我选择在这里打印出npages,然后乘以页的大小得到总的物理内存。输出如下:
在这里插入图片描述

总物理内存:32768 * 4096 = 2^27 = 1/8 G, 注意到其实本来也已有输出物理内存的大小,下面一行的131072K,和上面的结果一致。

pages代表的物理内存实际上只到上图中的PHYSTOP,上面还有mem-mapped devices区域:
在这里插入图片描述

**(正确的理解)**本来我是这样写的,后来和别人讨论了之后发现题目中问的是此操作系统可支持的最大物理内存,并不是通过探测环境得到的实际物理内存。
那就换一种思路,看memlayout.h,给pages数组分配到最大空间为PTSIZE
在这里插入图片描述

而每个PageInfo的大小可以通过sizeof打印出来查看,是8bytes,所以最多容纳2^22/8 = 219个PageInfo,每个PageInfo对应一个212大小的物理页,所以最大物理内存为2^31bytes,也就是2G

(5) 请详细描述在 JOS 中虚拟地址到物理地址的转换过程。

JOS采取的是页式内存管理,具体方法为二级页表的映射机制。
线性地址结构:
在这里插入图片描述

二级页表映射方法:
在这里插入图片描述

此图片来自:https://www.cnblogs.com/vinozly/p/5703215.html

在程序中使用一个虚拟地址后,需要对它进行映射到物理地址才能访问数据,映射的过程如上图。当一个进程运行时,寄存器cr3中保存了当前使用的页目录基地址(物理地址),拿到一个虚拟地址后,(会首先查找TLB快表,如果未命中,)需要查询页表:
(1)用高10位作为索引查找页目录表,得到对应页表的基地址(物理地址)
(2)用中间10位作为索引查找该页表,得到对应物理页基地址(物理地址)
(3)加上最后12位的页内偏移,就是该虚拟地址对应的物理地址。

(6) 在函数 pgdir_walk()的上下文中,请说明以下地址的含义,并指出他们是虚拟地址还是物理地址:

a) pgdir
b) pgtab, 其中 pgtab = PTE_ADDR(pgdir[PDX(va)])
c) pg, 其中 pg = PTE_ADDR(KADDR(pgtab)[PTX(va)])

a)pgdir是传进来的参数,虚拟地址,表示当前使用的页目录基地址
b)pgtab是一个物理地址,表示参数va(虚拟地址)对应的第二级页表的基地址。它得到的方式是:先用va的高10位(PDX(va))作索引找到pgdir的对应条目,32位的条目由20位的物理地址和12位的标记位组成,PTE_ADDR将获取其中的物理地址。
c)pg也是一个物理地址,表示va对应的物理页的基地址。它是通过同b)类似的过程查找第二级页表得到的:为了使用索引,首先将刚才的patab反映射到内核虚拟地址(+kernbase),然后用va中间10位作索引查找到对应条目,然后取出20位表示的物理地址。

(7) 画出本次 Lab 结束后虚拟地址空间与物理地址空间的映射关系,地址空间表示图中应至少包含 kern_pgdir 与 pages,展示越多的细节越好。

本次实验的最后一道题,正好可以作为整个实验的一个回顾。为了画出映射图,不妨先看一下截止到mem_init结束后,已经分配出来的物理页的情况,在mem_init的最后加上这样的代码,将打印出所有物理页的使用情况:
在这里插入图片描述

输出如下:
在这里插入图片描述

先简单看一下,还是这张图
在这里插入图片描述

0x0 – 0x1000区域1个物理页,已经用掉的Base memory,这不是虚拟地址可以映射到的区域,这道题中不重点考虑。
0x3be000 -0x400000区域66个物理页,主要是page_alloc分配出的空间,截止到目前主要是已经分配出的页表页。因为我们page_free_list是按照地址顺序倒着插的链表,所以就从高到低分配了。
中间部分0xa0000 – 0x15c000,这其实是两个区域,0xa0000到0x100000(640K~ 0x100000)的I/O hole,如上图; 以及紧接其后的extended memory中我们已经分配出去的空间0x100000~ 0x15c000。后者又可以分为3个部分,一是内核固有的data段和text段,截止到0x11b000;二是kern_pgdir占据1个物理页;三是pages数组——用于关联物理页的PageInfo型数组,它占据了64个物理页(相关大小信息可以从boot_alloc的参数中获悉)。刚刚好,65 * PGSIZE = 0x15c000 – 0x11b000。

物理空间搞清楚之后,还需要进一步看一下虚拟地址到物理地址的映射关系,口说无凭,我写了kern_va_travel函数,它的功能是遍历内核的虚拟地址空间,将已经有的映射关系打印,为了更清晰,如果是连续的映射就合并起来,只整体打印一次。这里用到了page_lookup函数,代码见下:

在这里插入图片描述

输出结果:
在这里插入图片描述

这样看起来就清晰明了了,逐条解读:
(1)Map : va 0xef000000 ~ 0xef03f000 to pa 0x11c000 ~ 0x15b000
还是参考准备阶段—第三部分的虚存图,这一部分正好对应UPAGES部分,是pages的副本,对应的物理地址也正好是extended memory的最后64个物理页。
在这里插入图片描述

(2)Map : va 0xef7bc000 ~ 0xef7bc000 to pa 0x3fd000 ~ 0x3fd000
Map : va 0xef7bd000 ~ 0xef7bd000 to pa 0x11b000 ~ 0x11b000
Map : va 0xef7bf000 ~ 0xef7c0000 to pa 0x3ff000 ~ 0x3fe000
Map : va 0xef7c1000 ~ 0xef7ff000 to pa 0x3fc000 ~ 0x3be000
这4条对应虚拟地址UVPT向上的PTSIZE大小,前面已经多次提到映射到的是所有页表的物理地址,可以看到,基本都是UVPT中的高地址拥有页表完成了映射,这与实际相符。大部分已用页表是通过page_alloc分配到的物理页,至于分配顺序就与page_free_list的插入顺序有关了;同时0xef7bd000就是前面提到的UVPT[UVPT],恰好指向了内核页目录的物理地址0x11b000。

(3)Map : va 0xefff8000 ~ 0xeffff000 to pa 0x10f000 ~ 0x116000
这是内核栈,这里分配了8页的大小,映射到的物理地址属于低地址空间的内核data段。
在这里插入图片描述

(4)Map : va 0xf0000000 ~ 0xf7fff000 to pa 0x0 ~ 0x7fff000
这个就不用说了,是remapped physical address的区域,映射整个物理内存。

已经把所有映射都找出来了,那么就开始画图!
这里我省去了最高处的device区段的内存空间。
在这里插入图片描述

到此为止本次实验就做完了。

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值