Memory Management Unit

来自osdev
Zhang3讨论 | 贡献2022年4月2日 (六) 03:44的版本
跳到导航 跳到搜索

MMUMemory Management Unit-内存管理单元是许多计算机的组成部分,它们处理内存地址转译(translation),内存保护(protection)以及不同计算机体系结构的其它特定目的。(译者注:本文尝试不翻译Page Directory和Page Table这两个名词,看是否更容易阅读理解)

地址转译

MMU对计算机的主要服务是内存地址转译。 内存地址转译是将虚拟地址转换为物理地址的过程。 我们可以称呼这个过程为虚拟地址被 “映射(mapped)” 到物理地址。 这使我们能够以自己的方式创建内存模型。 也就是说,我们可以重新排列内存使它们“看起来” 是我们想要的排序。

例如,在创建Higher Half Kernel时使用此技术。 内核在位置x加载,但是当分页管理被初始化时,MMU被告知将位置x映射到0xC0000000。 然后,这会产生内核实际上0xC0000000处的效果。

保护

因为我们可以让内存看起来像我们想要的排序,我们可以让每个进程觉得它自己是机器上唯一的进程。 此外,因为进程只能看到它拥有的内存,所以它不能修改或复制任何其它应用程序的内存。 这意味着,如果应用程序错误,那么只有“它”自己将失败,其它应用不受影响。

关于当代架构中的内存管理单元和虚拟内存系统的论述

这里对使用虚拟地址空间作为一般规则所涉及的理论进行一下非常简单的概述。 文章不关注任何一种架构,而是寻求使用MMU对通用CPU进行建模。

关于虚拟内存系统的一般论述

通常,芯片组 (主板) 往往具有N个字节的物理内存。 物理内存是 “真实” 内存,所有处理器都应该全局可见。 在正常操作下,或者更确切地说,当CPU在没有打开其分页功能内存管理单元的情况下运行时,CPU遇到的任何地址都将绕过(P)MMU并直接进入地址总线。

我想直接进入TLB(Translation Lookaside Buffer-转译后备缓冲)的概念,以及 “分页” 的工作原理等内存管理知识。 当今的许多处理器体系结构都指定了一组分页行为,当OS软件激活处理器的PMMU时,处理器将表现出这些行为。 但是什么*是*内存管理单元(Memory Management Unit)?内存管理单元其实是地址变换信息的高速缓存。 当处理器允许在机器上使用多个“虚拟”、独立的地址空间,使得CPU看到一段可以映射到任何物理页面的 “虚拟” 内存时,必须有某种形式的表,或其它记录-每个虚拟页面应使处理器最终在地址总线上访问哪个物理帧。

明确一点: 具有提供虚拟内存的MMU的处理器具有 “地址转换” 的片上缓存(on-chip cache)。 每个 “转译的记录record/条目entry” 都告诉CPU一个虚拟地址到一个物理地址的映射。 让我们将此片上缓存成想象为以下形式的大查找条目数组:

// TLB的抽象模型。

typedef uintptr_t vaddr_t;
typedef uintptr_t paddr_t;

// 用于将建模硬件TLB中的条目标记为已设置为有效转译的标志。
#define TLB_ENTRY_FLAGS_INUSE

struct tlb_cache_record_t
{
   vaddr_t entry_virtual_address;
   paddr_t relevant_physical_address;
   uint16_t permissions;
};

// 硬件转译后备缓冲区(TLB - Translation Lookaside Buffer)的实例。
struct tlb_cache_record_t   hw_tlb[CPU_MODEL_MAX_TLB_ENTRIES];

你的处理器的TLB本质上是一个条目的哈希查找表,它告诉每个页面指的是什么物理地址。 启用分页时,每个地址引用都会发送到TLB进行查找。 CPU在内部做这样的事情:

// TLB查找的模型例程。

int tlb_lookup(vaddr_t v, paddr_t *p)
{
   for (int i=0; i<CPU_MODEL_MAX_TLB_ENTRIES; i++)
   {
      if (hw_tlb[i].flags & TLB_ENTRY_FLAGS_INUSE && hw_tlb[i].entry_virtual_address == v)
      {
         *p = hw_tlb[i].relevant_physical_address;
         return 1;
      };
   };
   return 0;
}

如果TLB包含该虚拟地址的条目,则返回该虚拟地址的记录物理地址。 CPU *不关心* 内存中真实转译的实际状态! *你* 负责确保处理器TLB中的信息正确无误。 让我们假装处理器的TLB中有一个条目,该条目将虚拟地址0xC0103000记录为指向物理地址0x11807000。 假设你的内核已更改了RAM中Page Table中的此信息; 你对物理RAM的写入不会影响片上TLB。 除非你告诉处理器从其TLB中刷新0xC0103000的TLB条目,否则下次引用0xC0103000时,CPU将查看TLB,然后继续发送TLB *表达出* 地址对应的物理地址0x11807000。

处理器架构通常会提供指令,集体还是逐个或者按CPU设计人员决定的方式使TLB条目无效,。 让我们尝试对TLB刷新进行建模理解。 在我们的模型CPU架构中,有一个指令可以使软件发出将一个虚拟地址无效化的操作。 它被称为: TLBFLSH。 一个操作系统会通过这样做在我们的模型架构上调用这个:

asm volatile ("TLBFLSH   %0\n\t"::"r" (virtual_address));

然后我们的模型:

// 概念模型函数,用于刷新前面模型的TLB。

void tlb_flush_single(vaddr_t v)
{
   for (int i=0; i<CPU_MODEL_MAX_TLB_ENTRIES; i++)
   {
      if (hw_tlb[i].flags & TLB_ENTRY_FLAGS_INUSE && hw_tlb[i].entry_virtual_address == v)
      {
         ht_tlb[i].flags &= ~TLB_ENTRY_FLAGS_INUSE;
         return;
      };
   };
}

现在请理解,只要你启用其MMU,CPU将*始终*在将地址发送到地址总线之前使用TLB。 也就是说,一旦启用MMU,直到将其取下,你实际上已经将自己“困”在虚拟地址空间中。 除非你可以通过编辑Page Table来编辑该虚拟地址空间,并使虚拟地址的TLB条目无效,否则你无法更改内核可以在RAM中读取/写入的位置。 启用分页意味着你的所有数据和指令提取地址将首先通过TLB。

TLB *是* MMU。 明白这一点。 MMU只考虑*TLB*。 你构建的内存Page Table在任何方面都不是CPU的MMU的一部分。 实际上,许多架构甚至都不会查看你的软件构造表。 他们只会看TLB。 那么,如果处理器不递进搜索(Walk)软件构建的Page Table,TLB如何填充条目?这需要你将条目放入TLB。 就是这样。

MMU实现有两种主要类型; 或者更确切地说,大多数MMU实现可以在以下两大类中找到: (1) 那些需要软件来手动编辑处理器的片上TLB缓存条目并亲自确保*完全*一致性的MMU, 和 (2) 那些只需要软件来使陈旧的条目无效,并且自愿进行的MMU,在要转译虚拟地址时,会竭尽全力搜索新条目。

那些扫描某种形式的OS构造表的MMU实现称为启用了 “硬件辅助TLB加载(Hardware Assisted TLB-Loading)” 的MMU。 通常,CPU无需自行决定要为你填充哪些TLB条目。那是你的工作。 但是有些cpu甚至可以持续扫描软件Page Table并自动获得新的转换。 现在是时候解释什么是 “转译故障(Translation Fault)” 了。

转译故障是指处理器在其片上TLB中搜索虚拟地址的转译记录而找不到虚拟地址时发生的情况。 请注意,我并不是说转译故障是在CPU搜索了TLB *和* 软件构造的Page Table时。 在当前代码中没有引用的虚拟地址的TLB条目时,就会发生原始转译故障(original translation fault)。

根据所讨论的处理器架构,此转译故障将导致以下两种反应之一:

  (1) 处理器将陷入操作系统对应功能,输入错误的虚拟地址,并要求其手动搜索自己的地址空间转换记录,并手动加载TLB。
  (2) 处理器将启动“Page table Walk(页表递进搜索)”,在其中自动从通常由OS指定的RAM中的特定地址开始搜索表。(提示: CR3寄存器。译者注:Intel架构正是第二种方式)。

也就是说,并非所有处理器架构都会为你扫描转译信息。 在一些会搜索软件构造的转译表的架构上,这些表的格式往往非常严格地指定: “每个位应该在哪里,物理地址应该在哪里,X必须在Y点,等等”。 一个例子是著名的x86处理器架构。 确实存在处理器架构,当虚拟地址(vaddr)没有TLB条目时,它将立即中断操作系统; 在这种情况下,操作系统可以自行决定将特定于过程的转译信息保留在哪种格式中; 没有规范会告诉你如何格式化每个进程的转译信息。 你负责跟踪进程虚拟地址空间,并负责扫描有关转译故障的信息。

现在我们应该已了解MMUs是如何工作的了。 接着,我们需要了解虚拟地址空间的概念,以及为什么需要使TLB条目无效等问题。 请注意,有些架构具有一些非常时髦的translation-table/page-table格式: 例如PowerPC。 它使用哈希表代替TLB,这与x86Page Table完全不同。 这篇维基百科文章 (http://en.wikipedia.org/wiki/Memory_management_unit) 讨论了不同处理器的MMU实现。

理论具体化: 看看x86 “Self-referencing Page Directory trick”

因为关于这个的问题总是被问到的,所以最好简单地非常清楚地解释一下,然后把它排除掉。(译者注:本章说明了用一个指向Page Directory自身的引用,实现向MMU写入Page Directory)

本文的这一部分专门研究x86-32体系结构,并试图解释 “Self-referencing Page Directory” 的技巧。 在x86-32上,没有在TLB中找到条目*和*没有在软件构建的Page Table中找到转译条目*两者*都会发生转译故障。 然后MMU就会为你进行递进搜索(Walk),这看起来挺好。 像任何其它具有MMU模型的架构一样,该模型具有硬件辅助的TLB加载 (即,处理器为你遍历Page Table),你需要将顶层表的地址给处理器,该表开始描述地址空间的转译,这样它就知道从哪里开始搜索。(译者注:这里的顶层,是指TLB中的条目,比如Page Directory Entry还有下级条目,这其实构成了一个分层树形结构) 这基本上就是Page Directory在CR3中的物理地址。 处理器进行遍历搜索,当它这样做时,它将它找到的地址处的数据*解释*为Page-Directory Entries或Page Table Entries。

要明确一点: RAM中的字节不是有目的的; 它们本质上并不意味着任何含义。 而Page Table只是RAM中的字节。 你很可能决定为网卡提供一个Page Table以进行DMA传输。 你也可以将网络帧的地址放入CR3并让处理器进行遍历一样。 一个完全错误的CR3值很有可能具有,与其相邻正确的offset,设置了正确的位 (PRESENT位,WRITE位等等),以及一些帧地址,使得CPU甚至可以一直递进搜索它所看到的Page Table,并找到一个条目来转译你的故障地址。 这并不意味着CPU转译出的数据确实是正确转译数据。 CPU在RAM中获取你的字节地址,然后从那里走,*解释* 这些字节作为转译信息。

如以下示例: 你在物理地址0x12345000处有一个Page Directory。 这个Page Directory当然会有1024个条目,编号为0到1023。 我直接跳到关键主题,让我们想象一下,这个Page Directory在0x12345000的最后一个条目,索引1023,指向0x12345000。

即,pdir[1023] == 0x12345xxx,其中 'xxx' 表示permission位。(译者注:这里最好结合分页管理来阅读) 让我们还想象一下,pdir[0] 指向0x12344000处的帧,处理器将其解释为Page Table。 该表当然拥有1024Page Table条目。 当然,它们将映射虚拟地址0x0到0x3FFFFF以及更多帧。 所以现在,我们的示例Page Directory如下所示:

[Page directory at 0x12345000]:
entry 0000 | phys: (0x12344 << 12) | perms 0bxxxxxxxxxxxx |
entry ...
entry ...
entry 1023 | phys: (0x12345 << 12) | perms 0bxxxxxxxxxxxx |

[Page table at 0x12344000 that pdir[0] points to]:
entry 0000 | phys: (0x34567 << 12) | perms 0bxxxxxxxxxxxx |
entry ...
entry ...
entry 512  | phys: (0x72445 << 12) | perms 0bxxxxxxxxxxxx |

此设置是这样的: pdir[0] == 0x12344xxx, pdir[0], ptab[0] == 0x34567xxx, pdir[0], ptab[512] == 0x72445xxx, pdir[1023] == 0x12345xxx.

让我们模拟一个Page Table,找出物理地址条目{pdir[0],ptab[512]} 映射到什么; 比如,虚拟地址0x200000映射到什么物理地址。

  1. 打开分页时,处理器遇到引用0x200xxx的指令。 此引用必须通过MMU。
  2. 假设在TLB中找不到此条目。 故障#1发生。 在x86-32时,这会导致处理器遍历Page Table,而不是陷入OS中断。
  3. CPU获取虚拟地址0x00200xxx并将其拆分为10位,10位和12位。
  4. CPU现在知道它必须索引到CR3指向的Page Directory的条目0x0,然后索引到CR3中Page Directory指向的Page Table的条目0x200 (512)。
  5. CPU开始Page Table递进搜索(Walk)。 操作系统已事先将0x12345xxx写入CR3。 CPU假定这是有效Page Directory的地址,并将 (0x12345000 + 0x0 * sizeof(pdir_entry_t)) 作为uint32_t取值请求发送到地址总线上。 计算到在地址总线上发送的物理地址0x12345000。
  6. CPU从数据总线上的内存控制器中获取4个字节,也就是将刚刚从我们的Page Directory索引0中获取的4个字节解释为Page Directory Entry。
  7. CPU看到此Page Directory Entry为0x12344xxx,如上面的示例所示。 'xxx' 是权限位。 CPU会检查这些,因为这我们只是这样假设的,所以它们是正确的,并且存在条目。
  8. CPU现在,在验证了权限位之后,将从Page Directory Entry中提取物理地址。 它将得到0x12344000。
  9. CPU现在明确0x12344000是Page Table的开始,决定通过计算与基数0x12344000的偏移量在该Page Table中进行索引。 它需要获得索引0x200。然后计算 :( 0x12344000 (0x200 * sizeof(ptab_entry_t)),并将结果发送到地址总线上。
  10. 内存控制器返回0x12344800处的4个字节。
  11. CPU将其解释为Page Table条目,并检查位。 再一次,因为我们只是做模拟,所以权限位被证明是有效的。
  12. CPU现在处于叶子级别,并提取虚拟地址0x200xxx的帧地址。 原来是0x72445000。
  13. CPU从TLB中逐出一些条目,以便为将当前地址空间中的0x200000映射到帧0x72445000的新转译数据腾出空间。
  14. 程序执行继续。 请注意,二号转译故障,人们称之为x86页面故障,从来没有发生过,因为CPU *确实* 从其递进搜索中找到了转译。

现在,我们已经牢牢掌握了Page Table Walk的工作原理以及 “解释” 的思想,让我们模拟自引用Page Directory Entry的递进搜索(walk of a self-referencing page directory entry)。

  1. 打开分页时,处理器遇到引用0xFFFFFxxx的指令。 此引用必须通过MMU。 这也是我们的自我引用条目,正如你从上面的示例页面表设置中看到的那样。
  2. 假设在TLB中找不到此条目。 故障 #1发生。 在x86-32时,这会导致处理器遍历Page Table,而不是陷入OS中断。
  3. CPU获取虚拟地址,0xFFFFFxxx并将其拆分,10位,10位和12位。
  4. CPU现在知道它必须索引到CR3所指向的Page Directory的条目0x3FF (1023),然后索引到CR3中Page Directory所指向的Page Table条目0x3FF (1023)。
  5. CPU开始对Page Table进行Walk。 操作系统已将0x12345xxx写入CR3。 CPU假定这是有效Page Directory的地址,并将 (0x12345000 0x3FF * sizeof(pdir_entry_t)) 作为uint32_t取值请求发送到地址总线上。 计算到在地址总线上发送的物理地址0x12345FFC。
  6. CPU从数据总线上的内存控制器获取4个字节,并正确地将刚刚从我们的Page Directory索引1023中获得的4个字节解释为Page Directory Entry。
  7. CPU看到此Page Directory Entry为0x12345xxx,如上面的示例所示: 此条目引用了自身! 但是CPU不会对此进行检查。它只是检查权限,并准备将Page Directory项中的该地址解释为Page Table的物理地址。 ‘xxx’是permission位。 CPU会检查这些,因为我们只是这样假定的,所以它们是正确的,并且存在条目。
  8. CPU现在,在验证了权限位之后,将从Page Directory Entry中提取物理地址。 由于这里我们使用了自我引用技巧,它将获得0x12345000,并准备从Page Directory中*再次*读取,而不是从Page Table中读取。 现在,它将*解释*Page Directory的字节为Page Table。
  9. CPU现在确信0x12345000是Page Table的开始,决定通过计算与基数0x12345000的偏移量来索引到该Page Table中。它需要获得0x3FF索引的值。 然后计算 :( 0x12345000 + (0x3FF * sizeof(ptab_entry_t)),并将结果发送到地址总线上。
  10. 内存控制器返回0x12345FFC处的4个字节。
  11. CPU将其解释为Page Table条目,并检查位。 就像上次一样,由于我们正在读取相同的字节,因此权限被证明是有效的。
  12. CPU现在处于叶子级别 (或它认为),并提取虚拟地址0xFFFFFxxx的帧地址。 结果是0x12345000,因为Page Directory (CPU当前将其解释为Page Table) 中的条目0x3FF (1023) 指向Page Directory0x12345000。
  13. CPU从TLB中逐出一些条目,以便为将当前地址空间中的0xFFFFF000映射到帧0x12345000的新转译数据腾出空间。
  14. 程序执行继续。 请注意,第二个转译故障,人们称之为x86页面故障,从来没有发生过,因为CPU*确实*从其Walk中找到了转译。
  15. 更重要的是,发送地址0xFFFFF000的程序是关于接收物理帧0x12345000的数据,因为CP递进搜索到了那个范围,并且有一个TLB条目将其映射为这样。 该程序现在只需访问来自0xFFFFFxxx的偏移量,就可以从Page Directory读写。 访问0x12345000将获得Page Directory Entry0。访问0x12345004将获得条目1,依此类推。

现在,我们了解了如何使用自引用Page Table技巧来读取和写入当前page directory,接下来我们将研究如何在当前进程中读取和写入page table。 注意: 0xFFFFF000是任何程序的有效地址,除非你将其映射为SUPERVISOR,或者换句话说,未设置USER位。 从另一方面说用户空间中的程序将能够编辑自己的Page Table。 想象一个程序决定开始将其地址空间中的页面映射到物理地址0x100000的内核会怎么样? 这很可能导致将RAM中的内核归零。

另见

外部链接