Memory Management Unit

来自osdev
Zhang3讨论 | 贡献2022年2月2日 (三) 00:00的版本 (创建页面,内容为“'''MMU''' 或 '''内存管理单元'''是许多计算机的组成部分,它们处理内存地址转换(translation),内存保护(Protection)以及不同计算机体系结构的其他特定目的。 == 地址转换 == MMU对计算机的主要服务是内存地址转换。 内存地址转换是将虚拟地址转换为物理地址的过程。 我们可以说虚拟地址被 “映射” 到物理地址。 这使我们能够以自己的方式创建内…”)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航 跳到搜索

MMU内存管理单元是许多计算机的组成部分,它们处理内存地址转换(translation),内存保护(Protection)以及不同计算机体系结构的其他特定目的。

地址转换

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

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

保护

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

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

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

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

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

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

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

// Abstract model of a TLB.

typedef uintptr_t vaddr_t;
typedef uintptr_t paddr_t;

// Flag to mark an entry in the modelled hardware TLB as having been set for use as a valid translation.
#define TLB_ENTRY_FLAGS_INUSE

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

// Instance of a hardware Translation Lookaside Buffer.
struct tlb_cache_record_t   hw_tlb[CPU_MODEL_MAX_TLB_ENTRIES];

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

// Model routine for a TLB lookup.

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中页表中的此信息; 你对物理RAM的写入不会影响片上TLB。 除非你告诉处理器从其TLB中刷新0xC0103000的TLB条目,否则下次引用0xC0103000时,CPU将查看TLB,然后继续发送TLB *说* 地址对应的物理地址0x11807000。

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

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

然后我们的模型:

// Modelled function for a flush of the TLB modelled earlier on.

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,直到将其取下,你实际上已经将自己 “困” 在虚拟地址空间中。 除非你可以通过编辑页表来编辑该虚拟地址空间,并使虚拟地址的TLB条目无效,否则你无法更改内核可以在RAM中读取/写入的位置。 启用分页意味着你的所有数据和指令提取地址将首先通过TLB。

TLB *是* MMU。 明白这一点。 MMU是 *TLB*。 你构建的页表在任何方面都不是CPU的MMU的一部分。 实际上,许多体系结构甚至都不会查看你的软件构造表。 他们只会看TLB。 那么,如果处理器不遍历软件构建的页表,TLB如何填充条目?你将条目放入TLB。 就是这样。

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

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

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

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

  (1) 处理器将陷入操作系统,并要求其手动搜索自己的地址空间转换记录,并手动加载TLB,其中输入错误的虚拟地址。
  (2) 处理器将启动 “Page table Walk 页表行走”,在其中自动从通常由OS指定的RAM中的特定地址开始行走表。(提示: CR3)。

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

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

理论具体化: 看看x86 “自我引用页面目录技巧”

因为关于这个的问题总是被问到的,所以最好简单地非常清楚地解释一下,然后把它排除掉。

本文的这一部分专门研究x86-32体系结构,并试图解释 “自引用页面目录” 的技巧。 在x86-32上,处理器在翻译故障模型之后,在 *两者* 都没有在TLB中找到条目 *和* 没有在软件构建的页表中找到翻译条目,因为它会为你行走,因为它就像那样。 像任何其他具有MMU模型的体系结构一样,该模型具有硬件辅助的TLB加载 (即,处理器为你遍历页表),你需要给处理器顶层表的地址,该表开始描述地址空间的翻译,这样它就知道从哪里开始行走。 这基本上是页面目录在cr3中的物理地址。 处理器行走,当它这样做时,它将它找到的地址处的数据 * 解释 * 为页面目录条目或页表条目。

请非常扎实地理解: RAM中的字节不是有目的的; 它们本质上并不意味着任何含义。 页表只是RAM中的字节。 你很可能决定为网卡提供一个页表以进行DMA传输。 就像你可以将网络帧的地址放入CR3并让处理器进行遍历一样。 你的大错CR3值很有可能具有,在右偏移量处与其相邻,设置正确的位 (当前,写入等),以及一些帧地址,使得CPU甚至可以一直走到它所看到的页表,并找到一个条目来翻译你的故障地址。 这并不意味着CPU解释为翻译数据的翻译数据确实是翻译数据。 CPU在RAM中获取你的字节地址,然后从那里走,* 解释 * 这些字节作为翻译信息。

以以下示例为例: 你在物理地址0x12345000处有一个页面目录。 这个页面目录当然会有1024个条目,编号为0到1023。 因为我想跳到主题的肉,让我们想象一下,这个页面目录在0x12345000的最后一个条目,索引1023,指向0x12345000。

即,pdir[1023] == 0x12345xxx,其中 'xxx' 表示许可位。 让我们还想象一下,pdir[0] 指向0x12344000处的帧,处理器将其解释为页表。 该表当然拥有1024页表条目。 当然,它们将虚拟地址0x0到0x3FFFFF映射到RAM中的各个帧。 所以现在,我们的示例页面目录如下所示:

[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.

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

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

现在,我们已经牢牢掌握了页表行走的工作原理以及 “解释” 的思想,让我们模拟自引用页面目录条目的行走。

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

现在,我们了解了如何使用自引用页表技巧来读取和写入当前页目录,接下来我们将研究如何在当前过程中读取和写入页表。 注意: 0xFFFFF000是任何程序的有效地址,除非您将其映射为主管,或者换句话说,未设置用户位。 否则,用户空间中的程序将能够编辑自己的页表。 想象一个程序决定开始将其地址空间中的页面映射到物理地址0x100000的内核? 现在很可能决定将RAM中的内核归零。

另见

External Links