第二十三讲:内存管理
第二十三讲:内存管理
在我们的进程内部,地址空间从地址0x400000
开始,一直延伸到进程内存的最大地址(至少 2GB)。但是我们的计算机在物理上没有足够的内存让每个进程都拥有自己的2GB,并且显然它们在使用的内存中不能重叠,因此我们的进程所看到的内存逻辑视图和内存的物理布局。该转换部分由处理器处理,部分由操作系统处理。
逻辑地址:进程地址空间内的地址。有时逻辑地址也称为虚拟地址。
物理地址:计算机内存单元所见的地址,范围从 0 到可用 RAM 的数量。
内存管理的目标如下:
- 允许每个进程有自己统一的地址空间
- 保护进程的内存免受彼此的影响:一个进程不应该能够访问另一进程的内存空间。
- 地址空间分配的灵活性,允许操作系统充分利用系统有限的内存。
从逻辑地址到物理地址的映射是由称为内存管理单元或 MMU 的 CPU 组件完成的。由于几乎每次内存访问都需要转换,因此将 MMU 紧密集成到 CPU 中并且转换速度尽可能快非常重要。另一方面,MMU 必须是可编程的,因为操作系统需要控制如何将内存分配给进程。
内存管理方案
- 扁平内存:一次只能运行一个进程
- 重定位寄存器:多进程,无保护或共享
- 有限制的搬迁:多进程,更好的保护
- 分区
- 分页
- 多级分页
- 分段+分页
扁平内存方案
最简单的内存模型是平面方案:虚拟地址直接映射到物理地址,无需转换。这通常意味着一次只能运行一个进程。如果多个进程正在运行,那么它们每个都会获得一个单独的物理内存“片”(例如,一个进程的地址空间从 0x10000 开始,另一个进程从 0x110000 开始,等等),并且没有任何保护措施阻止一个进程访问另一个进程的内存。这种方法被早期的家用计算机使用:DOS、早期版本的 Mac OS 和 Windows 等。如今,它只适用于单一用途的系统:嵌入式系统和微控制器,其中没有操作系统,只有一个进程。
请注意,如果允许运行多个进程,则必须对它们进行编译,以便将每个进程加载到单独的内存区域中(或者必须将它们编译为“位置无关”,并且加载器必须负责将它们放置在不同的内存区域中)。
分页
分页机制试图通过将每个进程的地址空间映射到物理内存的非连续区域来避免内存碎片问题。例如,逻辑地址 0x100 可能会映射到物理 0x400,但逻辑地址 0x101 可能会映射到物理 0x2301,这是物理内存的完全不同的部分。
物理内存分为固定大小的区域,称为帧。对于每个进程来说,逻辑内存也类似地分为页,其中页大小等于帧大小,并且每个进程的页大小是固定的并且相同。通常页面大小由 CPU 架构固定且不可更改。每个逻辑地址分为两部分:页和偏移量(其中偏移量在 0 到页大小 - 1 的范围内)。每个进程都有自己的页表,而不是重定位寄存器,该页表为每个进程指定映射到哪个物理帧。逻辑到物理的映射过程如下
- 提取逻辑地址的表示页面的部分。
- 在页表中查找页来获取帧。
请注意,与任何其他转换一样,此转换必须由 CPU 上的 MMU 完成,并且对进程本身必须是不可见的。操作系统在调度进程运行时为进程设置页表。因为所有页面的大小相同,所以将页面分配给帧比分区简单得多:操作系统只需要为每个帧维护它是空闲的还是正在使用的,然后当请求帧时收到后,第一帧免费即可满足。
页面大小通常是 2 的幂,因此与页面/帧相对应的地址部分只是一些高位。例如,假设我们有一个 1Kb 地址空间(10 位地址),页大小为 256。这意味着每个进程有 4 页,每页 256 字节,因此页表有 4 个条目。每个地址的高 2 位指定页/帧,而低 8 位给出偏移量:
这也体现了分页的另一个特点,即物理地址空间的大小可以大于逻辑地址空间。逻辑地址为 10 位,但物理地址必须至少为 11 位,才能保存帧 0 到 7。
与传统缓存一样,我们可以通过查看其命中率和命中/未命中访问时间来了解 TLB 如何影响内存访问时间。假设 TLB 的命中率为 80%(80% 的内存访问都在 TLB 内找到),并且 TLB 命中的内存访问时间为 20ns,而主内存访问的内存访问时间为 100ns。那么我们的有效访问时间为
0.8 * (20 + 100) + 0.2 * (20 + 100 + 100) = 140 ns
“命中”访问需要 20 ns 来搜索 TLB,然后需要 100 ns 来执行“实际”内存访问。未命中需要 20 ns 来搜索 TLB 并失败,需要 100 ns 来访问页表,然后需要 100 ns 来进行“真正”访问。
分页和分段的区别在于:
页数通常较大,有数千个,而段数通常较小,为 4-8 个。
每个页面都具有相同的大小,而段则根据其用途而具有不同的大小。因此,段到物理内存的分配更加复杂,更接近于分区。
页是从逻辑内存到物理内存的低级转换,而段旨在反映进程内存空间的语义组织:有一个堆段、一个可执行代码段、一个堆栈段等
内存保护
每个页表条目可以附加一组保护位,描述该页内允许哪种类型的内存访问:读/写或只读。某些系统允许将页面标记为不执行,这意味着如果指令指针指向此类页面,CPU 将产生错误。同样,写入只读页也会产生进程(操作系统)故障。此外,页面还有有效的比特位,代表页面是否存在。
多级分页
现代机器的物理内存空间比较大,如果使用单张页表进行管理,则页表中可能会出现上百万个页表项,这会很难管理。现代操作系统往往会使用多级页表进行管理,而不是仅仅使用一张页表。我们将从二级页表来进行理解,以此可以推广到更深层次的页表。
假设我们有16位的地址,单个页面的大小位4KB(即页偏移为12位, )。剩下的4位被分成两部分,2位用于页表1,2位用于页表2。
地址内容 | 页表1的序号 | 页表2的序号 | 页偏移 |
---|---|---|---|
比特位 | 2 | 2 | 12 |
页表1有四个一级表项,每个条目都是一个指向二级页表的指针,每个二级页表也包含4个表项。
|
|
为了对地址执行转换,例如 1001101101001110b = 39758
,我们将地址拆分为表1索引、表2索引和偏移量:
地址 | 10 | 01 | 101101001110 |
---|---|---|---|
页表1索引 | 页表2索引 | 页偏移 |
在表1中,我们查看条目10b = 2
。条目2指向0x300
处的2级页表。在此表中,我们查看条目01b = 1
。条目1包含0x1 = 0001b
。我们用0001b
替换原始地址的两个页表部分,得到物理地址0001101101001110b = 6990
。
表1的每个条目有效地管理大小为16KB(4*4KB=16KB)的页。如果我们愿意,我们可以允许1级条目直接映射到这种更大的页。这将提供一个具有两种页面大小的分页系统:大小为 4KB 的普通页面和大小为 16KB的大页。 x86-64内存架构在其多级分页方案支持混合的页面大小。
多级分页看起来可能会让内存访问变得更慢,但是各级页表都缓存在TLB中,因此内存访问的速度并没有差多少。TLB对于使性能至关重要。
反向页表
我们可以使用一种反向页表,其存储的是每个帧来源自哪个进程,而不是每个进程存储一个页表。当系统收到内存访问的请求时,MMU必须在帧表中搜索到拥有特定帧的页和进程,然后执行转换。线性搜索帧表会太慢,因此使用以进程ID和页号为键的哈希表来加快搜索速度。
反向页表的优点是整个系统只有一张页表,页表的条目数由物理内存大小决定。对于普通页表,所有页表的总大小大致与正在运行的进程数成正比,而不是与物理内存总量成正比。
帧 | 进程ID | 页 |
---|---|---|
0 | 12 | 3 |
1 | 147 | 5 |
2 | 6 | 2 |
3 | 135 | 0 |
段页式内存管理
将分段与分页结合起来也是一种对于内存的管理方法。这提供了分段的语义优势以及分页的效率和灵活性优势。
程序的逻辑地址由段和段偏移组成,其中段偏移又分为页和页偏移。段表存储每个段的长度和页表基地址,每个段实际上都有自己的页表。地址转换的过程如下:
- 从地址中提取处段信息。
- 查询段表中各个段的信息,检查段的偏移是否超过了段的长度。如果偏移量超过了范围,返回内存错误。
- 使用段表项中的页表地址来查询该段的页表
- 在页表中查询页以找到页帧。
- 用帧替换地址中的段和页;地址的页偏移部分保持不变。这就得到了物理地址。
例如,考虑一个具有256字节页和4个段的16位地址空间。每个段的最大长度为16384字节 (2^14)。地址排列为:
段 | 段偏移 | |
---|---|---|
页 | 页偏移 | |
2比特 | 6比特 | 8比特 |
段表的内容如下所示:
索引 | 长度 | 页表地址 |
---|---|---|
0 | 1024 | |
1 | 300 | |
2 | 2000 | |
3 | 4000 |
段1的页表包含两个条目:
页索引 | 帧 |
---|---|
0 | 3(00000011b ) |
1 | 6(00000110b ) |
转换地址0x410c (= 0100000100001100b
), 步骤如下:
- 将地址拆分为段和段偏移:
01_00000100001100
。段01
=1。 - 在段表中查找段1。根据段长度 (300) 检查段偏移 (
00000100001100
= 268)。偏移量在段内,因此继续。 - 使用段表的页表基数查找段1的页表。
- 将段偏移分为页和页偏移:
000001_00001100
。页=1,页偏移=12。 - 在页表中查找第1页,得到其帧:
00000110
。 - 将逻辑地址(即高 8 位)中的段和页替换为帧,产生物理地址
00000110 00001100
= 0x60c。
虚拟内存
一个进程的地址空间的每个页/段使用的频率是不同的。如果一个页面不经常使用,则可以将它们移动到磁盘,从而允许该页被另一个进程使用。当页面写入磁盘时,其页表项被标记为无效。当收到对该页面的访问时,我们首先从磁盘重新加载该页面,然后再次尝试访问。由于进行重新加载,该页目前有效,内存访问将会成功。
我们尽量避免在不需要时将内存页写入磁盘。如果页面自上次加载以来没有被修改过,那么我们不需要将其同步到磁盘。因此,页表为每个页面存储一个脏位,指示自上次加载以来该页面是否已被修改。该位在加载页面时被清除,并在对该页面内的地址进行写入时设置。
当我们需要将页面调出到磁盘时,如果设置了脏位,则在重新分配帧之前将页面的内容写入磁盘。如果未设置脏位,则无需将任何内容写入磁盘,该帧可供立即使用。
x86-64 Linux内存管理
x86-64在不同的抽象级别设计了三种地址:
- 逻辑地址:进程看到的地址,分为段和偏移。段始终为 16 位,而偏移量为 32 位。
- 线性地址:64位无符号整数,可用于寻址整个地址空间。 MMU 使用上述分段方案将逻辑地址转换为线性地址。
- 物理地址:内存芯片视角的地址。 MMU 通过应用分页方案将线性地址转换为物理地址。
x86-64使用的是段页式内存管理方案。分段和分页机制可以通过配置MMU进行启动或者关闭。也就是说,仅仅使用分段机制,仅仅使用分页机制或者将内存地址直接映射到物理地址页都可以实现。Linux在支持配置分段分页的系统上使用纯分页的机制,尽管对分段的支持仍然存在。
Intel可以使用最多5级分页,默认页面大小为4KB,单个页面的大小可以更大,4MB甚至1GB。混合不同大小的页使得分页方案更接近于分区。
顶级页目录的地址始终由控制寄存器CR3
指向。如果设置了CR0
的相关位(分页和保护),则启用分页,否则忽略CR3
的值。每当 CR3
的值发生更改时,CPU将自动刷新TLB。
每个页表条目由一个页地址(可以是页的物理地址,也可以是下一级页表的地址)和一组比特位组成:
- 页面大小:默认为4KB,经过设置,也可以达到到4MB。
- 已访问:每当访问页面时由进程设置。
- 禁用缓存:如果设置,处理器将永远不会将此页的内存加载到缓存中。
- 直写:如果设置,则启用缓存直写
- 用户/管理员:决定该页面的权限级别。操作系统内核拥有的页面应该设置主管位;仅允许在特权模式下运行的进程访问主管标记的页面。
- 只读/读写:如果设置,则该页面是可读写的
- 存在位:如果设置,该页面实际上位于物理内存中。如果未设置,访问页面将导致页面错误,操作系统可以捕获该页面错误并使用该错误(例如)从磁盘加载页面。
- 剩下三位供操作系统使用。
页表的最低级别(条目存储实际物理地址)存储一组略有不同的位:
- 脏数据:如果页面被修改则设置。
x86-64 上的 Linux 中的 64 位地址被划分为:
页表内容 | 全局目录页 | 上层目录页 | 中间层目录页 | 页表 | 页偏移 |
---|---|---|---|---|---|
比特数 | 9bits | 9bits | 9bits | 9bits | 12bits |
多级分页(无分段)
x86-64使用多级分页,通常有四级。顶级页面非常大,通常有多个GB。顶级页面可以映射到较低级别的页面,也可以映射到整个连续的内存区域。 Linux 禁用分段系统,更喜欢对所有内存组织使用分页。
最初Linux采用三级方案,省略了上层目录页。四级方案于2007年并入内核。2017 年,合并了可选的五级分页方案,支持高达 128 PB 的地址空间。请注意,仅使用 48 位地址。每个地址的剩余 16 位当前未使用,并从第 47 位开始进行符号扩展。物理地址在转换后为 52 位宽;转换后,高 36 位被 40 位帧替换。
每个页索引是一个表的索引,其中包含下一个较低表的地址以搜索后续索引。任何表条目也可以为空,表明该地址无效。进程的页面全局目录的位置存储在 CR3 寄存器中(因此,进程切换只需要更新该寄存器的值。)
页表条目
每个页表条目都定义为 pte_t、pmd_t、pud_t 和 pgd_t 类型的结构体。这些结构的定义相同,但为了将来的可扩展性而被赋予不同的名称。除了以下页表的位置之外,每个条目还存储一组状态和保护位(其中大部分对应于上述位):
- Present:页面加载到内存中(而不是交换到磁盘)。
- protnone:页面已加载但由于某种原因无法访问
- 读/写:页面可以写入
- 用户:可从用户空间访问页面。 (内核将其某些页面映射到进程地址空间供其自己使用,但不允许进程访问这些页面。)
- 脏:自上次加载以来页面是否被修改过。
- 已访问:自上次加载以来页面是否已被访问(读取或写入)。
内核还维护一个反向页表,将帧映射到进程/页面,以实现快速反向转换。
保留帧
内核维护一个保留帧列表,这些帧永远不应该分配给页面。这些包括内核本身使用的帧,以及用于必须存在于固定地址的事物(例如内存映射 IO 设备)的帧。例如,在某些视频模式下,视频 RAM 直接映射到一系列物理地址;显然我们不希望任何进程的页面分配到这个范围内,因此包含这些地址的帧被标记为保留。
大页内存
x86-64 默认内存页面大小为 4KB,但可以使用大小为 2MB、4MB 和 1GB 的页面。
附录
课程原文: https://staffwww.fullcoll.edu/aclifton/cs241/lecture-memory-management.html