一个操作系统的设计与实现——第8章 内存管理系统

计算机上的任何程序,包括操作系统自己,都需要使用内存。因此,操作系统需要实现内存管理系统,以进行内存的分配和回收。

在我们的操作系统中,内存管理系统由两部分组成:页分配器与页回收器。本章将实现这两个部分。

8.1 从虚拟地址到物理地址

回顾CPU对内存地址的转换过程:

  1. 使用段寄存器中的段选择子,在GDT中找到一个段描述符,从中取得段基址
  2. 将段基址与偏移地址相加,得到虚拟地址
  3. 取CR3中的页目录表地址,并取虚拟地址的最高10位作为页目录表索引值,从页目录表中取得页表地址
  4. 取虚拟地址的中间10位作为页表索引值,从页表中取得页地址
  5. 取虚拟地址的最低12位,将其与页地址相加,得到物理地址

我们的操作系统使用的是平坦模型,所有的段基址都是0,所以上述第1、2步对地址没有任何影响。

当分配新的虚拟页地址时,上述第3、4步都有可能找不到页表(PDE的P位为0)或页(PTE的P位为0)。当PDE的P位为0时,需要分配一页作为页表,并将其物理地址和属性填入PDE;当PTE的P位为0时(对于新分配的虚拟页地址,这个条件一定成立),需要分配一页,并将其物理地址和属性填入PTE。这样一来,从虚拟地址到物理地址的转换就畅通了。

另一方面,无论是虚拟页地址还是物理页地址,怎么知道哪些地址是可用的呢?这就需要构造两个布尔数组,分别用于记录虚拟页地址与物理页地址的使用情况。布尔数组在实现上可以使用位图优化。

分页模式的特点是:虚拟地址连续而物理地址可以不连续。于是,在分配虚拟地址时,需要找N页连续可用的地址;在分配物理地址时,可以分N次进行,每次找一页可用的地址,然后将虚拟地址与物理地址建立联系。

综上,想要分配N页内存,需要依次进行以下步骤:

  1. 在虚拟地址位图中找到N个连续可用位,将其设定为已使用。将找到的位图索引转换为虚拟页地址
  2. 循环N次,每次在物理地址位图中找到1个可用位,将其设定为已使用。将找到的位图索引转换为物理页地址,然后,将当前的虚拟页地址与物理页地址建立联系

同理,想要回收N页虚拟内存,需要依次进行以下步骤:

  1. 将虚拟页地址转换为位图索引,将虚拟地址位图中的对应位设定为可使用
  2. 循环N次,每次从当前的虚拟页地址得到物理页地址,将其转换为位图索引,将物理地址位图中的对应位设定为可使用。然后,将当前的虚拟页地址对应的PTE清零

8.2 PDE和PTE的虚拟地址

想要在虚拟地址和物理地址之间建立联系,就需要设定虚拟地址对应的PDE和PTE。那么,PDE和PTE的地址是什么呢?

这个问题乍一看很简单:页目录表地址在CR3中,而PDE和PTE的索引值在虚拟地址中,只需要将其取出,手动完成查找PDE和PTE的过程,就可以了。这个方案看似很有道理,但问题是:分页模式下,所有的地址都是虚拟地址,而CR3、PDE、PTE中存放的都是物理地址,就算已知页目录表的地址是0x100000,页表的地址是0x101000,也不能使用这两个物理地址,必须使用虚拟地址。那怎么办呢?

事实上,只需一行非常精妙的代码就能解决这个问题,它位于本章代码8/Mbr.s的第32行:

mov dword [0x100ffc], 0x100003

这行代码看上去很普通,它将页目录表的最后一个PDE指向页目录表自己的物理地址。

这样做有什么用呢?再次回顾从虚拟地址到物理地址的转换过程,其需要且必须经历三个步骤:

  1. 找到一个PDE,取得其中的页表地址
  2. 找到一个PTE,取得其中的页地址
  3. 将页地址与偏移地址相加,得到物理地址

现在需要的是PDE和PTE的地址,即第1步和第2步的结果。但上述三个步骤是不能暂停,也不能去除的。不过,由于最后一个PDE指向的是页目录表自身,所以,如果使用这个PDE,就能在页目录表中空兜一次,从而消耗掉一个步骤。

具体来说,如果虚拟地址的最高10位全为1,那么上述过程就会变成这样:

  1. 空兜
  2. 找到一个PDE,取得其中的页表地址
  3. 将页表地址与偏移地址相加,得到PTE地址

进一步的,如果虚拟地址的最高20位全为1,那么上述过程就会变成这样:

  1. 空兜
  2. 空兜
  3. 将页目录表地址与偏移地址相加,得到PDE地址

综上,从虚拟地址获取其对应的PDE和PTE的方法如下:

  1. 构造这样的地址:0xfffff000 | (虚拟地址 >> 22 << 2),就能访问或修改PDE
  2. 构造这样的地址:0xffc00000 | (虚拟地址 >> 12 << 2),就能访问或修改PTE

8.3 内存管理系统的实现

8.3.1 位图的实现

想要实现内存管理系统,就需要先实现出位图。

请看本章代码8/Bitmap.h

第5~9行,定义了位图结构体。位图结构体由指向位图的指针和位图的长度构成。

第12~16行,声明了位图的各种函数。

接下来,请看本章代码8/Bitmap.hpp

bitmapInit函数用于初始化位图。this->__size的单位是位,所以,初始化位图长度时,如果使用的单位是字节,就需要乘以8。函数中使用的memset函数实现于本章代码8/Memory.hpp中。

bitmapGet函数与bitmapSet函数分别用于读取位和设置位。

bitmapAllocate函数用于分配一段连续的位。实现中,0表示可用而1表示不可用。所以,当找到一段连续的0后,应将这些0转变为1。需要注意的是:第40行使用的是无限循环,这是因为我们的操作系统不考虑分配失败,而是假定位图所管理的资源是无限的。

bitmapDeallocate函数用于回收一段连续的位。

8.3.2 页分配器的实现

请看本章代码8/Memory.h

这个头文件中声明了内存管理系统的各种函数。

接下来,请看本章代码8/Memory.hpp

第7~8行,定义了两个位图。__pMemoryBitmap供物理地址使用,__vMemoryBitmap供虚拟地址使用。

memoryInit函数用于初始化这两个位图。在我们的操作系统中,由于不考虑物理内存的实际大小,故物理内存位图固定使用一页;虚拟地址空间的理论大小为4G,但出于简化考虑,实现中也只使用一页位图。一页位图可以管理0x8000个页,即128M内存。位图的地址从0x7e00~0x9ffff都可用,可以随便选。

memset函数与C语言标准库的同名函数等价。

__allocateAddr函数用于分配地址。其先调用bitmapAllocate函数以分配位,然后将得到的位图索引转换为地址。

__installPage函数用于在虚拟地址与物理地址之间建立联系。

第36~37行,使用上文中的公式取得PDE指针和PTE指针。

第39行,判断PDE的P位,如果为0,说明当前虚拟地址没有对应的页表,此时需要分配一页,并将其物理地址与属性填入PDE。

第41行,分配一页,并将其物理地址与属性填入PDE。这里使用的页属性是0x7,表示存在、可读写、所有特权级均可访问。

第42行,将新分配的页表清零。页表的虚拟地址可由PTE地址去除偏移量得到。

第45行,将页的物理地址与属性填入PTE。这里使用的页属性也是0x7

第47行,执行invlpg (虚拟内存地址)指令。这条指令有什么用呢?原来,CPU内部为虚拟地址到物理地址的转换提供了缓存,以加速这一过程。这个缓存被称为快表(Translation Lookaside Buffer,TLB)。然而,如果页目录表或页表发生了变化,与之相关的TLB缓存就失效了,但CPU作为缓存的使用者,无法感知到此事。因此,CPU要求操作系统在修改页目录表和页表时主动刷新TLB。

刷新TLB的方法有两种:

  1. 加载CR3。这样做将直接清空整个TLB
  2. 使用invlpg (虚拟内存地址)指令。这样做将刷新这个虚拟地址的TLB缓存(如果有)

__allocatePage函数用于分配页,它是__allocateAddr函数与__installPage函数的封装。对于虚拟地址,需要调用一次__allocateAddr函数,得到pageCount页连续的虚拟地址;对于物理地址,需要调用pageCount__allocateAddr函数和__installPage函数,每次分配一页物理地址,并将其与虚拟地址建立联系。

allocateKernelPage函数是__allocatePage函数的封装。其将虚拟地址的分配起点设为0x66600000,这是一个随便写的地址,只要大于等于0x100000即可;物理地址的分配起点设为0x200000,这个地址需要大于等于0x102000,因为0x100000的前两页已经被使用了。

8.3.3 页回收器的实现

请看本章代码8/Memory.hpp

__deallocateAddr函数用于将待回收的页地址转换为位图索引,然后调用bitmapDeallocate函数,将其从位图中删除。

__uninstallPage函数比__installPage函数简单的多,其用于将待回收的虚拟地址所在的PTE清零,并刷新TLB。

__deallocatePage函数用于回收页。它是__deallocateAddr函数与__uninstallPage函数的封装,且操作步骤与__allocatePage函数完全相反。

第90行,将虚拟页地址从位图中删除。

第92行,循环pageCount次,每次循环回收一页物理地址,并断开虚拟地址与物理地址之间的联系。

第94行,使用上文中的公式取得PTE指针,然后读取PTE,并去除低12位上的属性,就得到了页的物理地址。

第96行,将物理页地址从位图中删除。

第97行,断开虚拟地址与物理地址之间的联系。

deallocateKernelPage函数是__deallocatePage函数的封装,其使用的参数与allocateKernelPage函数一致。

8.3.4 杂项

请看本章代码8/Mbr.s

第32行,将页目录表的最后一个PDE指向页目录表自己的物理地址。这样做的意义已经在上文讨论过。

接下来,请看本章代码8/Int.s

在上一章中,intTimer函数包含了一段打印数字的代码,在本章中已经将其删除;与之配套的extern printInt声明也已删除。

接下来,请看本章代码8/Kernel.s

第10行,调用memoryInit函数,完成内存管理系统的初始化。

8.4 测试

本章代码8/Kernel.c测试了allocateKernelPage函数与deallocateKernelPage函数,请读者自行分析输出结果。

posted @ 2023-11-12 09:48  樱雨楼  阅读(29)  评论(0编辑  收藏  举报