Skip to content

Chapter 5 Virtual Memory

虚拟内存(Virtual Memory)是在 paging 基础上进一步建立的内存管理机制。它让进程看到一个连续、私有、可扩展的虚拟地址空间,而实际物理内存只需要保存当前真正会被访问的那部分页面。

Virtual Memory 的核心目标

  • 分离逻辑地址与物理地址:程序使用 virtual address,硬件和内核负责映射到 physical frame
  • 突破物理内存容量限制:程序的虚拟地址空间可以大于实际物理内存
  • 提高并发度:每个进程只装入当前需要的页,更多进程可以同时驻留
  • 减少 I/O:不必一次性把整个程序装入内存
  • 支持共享与保护:共享库、共享内存、权限控制都可以通过页映射完成
  • 优化 fork():通过 copy-on-write 避免立即复制整个父进程地址空间

1 Virtual Address Space

Virtual memory is larger than physical memory

虚拟内存本质上只是地址范围,其所有存储内容最终都由物理内存(及磁盘后备存储)支撑。

通常栈的虚拟地址空间设计为从最大逻辑地址开始并向下增长,而堆则向上增长。

一个进程的虚拟地址空间通常按程序逻辑组织为 code、data、heap、shared library、stack 等区域。虽然这些区域在虚拟地址上看起来有固定布局,但它们不一定都已经占用物理内存。

虚拟地址空间支持带空洞的稀疏地址空间,为内存增长、动态链接库等预留空间。

虚拟地址只是进程可能访问的地址范围。只有当某个虚拟页真的被访问,并且内核认为该访问合法时,才需要给它分配或映射物理 frame。

因此 heap 和 stack 中间的 hole、尚未使用的大数组、很少触发的错误处理代码,都不必提前占用物理内存。

共享库也是虚拟内存的重要应用,多个进程可以把同一份只读库代码映射到各自的虚拟地址空间中,但底层指向同一批物理页。

2 Demand Paging

2. 1 Bacis Concepts

Demand paging(请求分页)指页面不在进程启动时全部装入,而是在第一次真正被访问时才装入物理内存。

如果访问的页面无效或出错,则终止当前操作;如果页面有效但不在物理内存中,则将其调入内存,这种情况称为缺页中断(page fault)

已被换出的页面可通过交换操作从磁盘调回,新页面则通过内存映射加载。

该机制无需执行不必要的 I/O 操作,减少了物理内存占用,可支持更多应用同时运行,但单次访问的响应速度会变慢。

Demand paging 和 page fault 的关系
  • Demand paging 是一种内存管理策略,需要时才装入页面。
  • Page fault 是实现该策略的硬件异常机制,访问一个当前不可直接访问的虚拟页时,MMU 把控制权交给 OS。

示例

堆的虚拟大小在调用 brk() 时就会扩容,而物理内存占用(RSS, Resident Set Size)只有在进程访问新内存触发页错误、内核分配物理页后才会扩容。

内存管理单元(MMU, Memory Management Unit)的每个页表项都包含一个 valid–invalid (present) bit,初始状态下均设为无效。地址转换过程中,若页表项为无效状态就会触发页错误。

在 Linux 中,page fault 进入内核后,内核会先查该地址是否落在某个合法的虚拟内存区域(VMA, Virtual Memory Area)中。

若地址不属于任何 VMA,则是非法访问;若属于合法 VMA,则根据 fault 类型完成映射、分配、换入或 COW。

Memory Layout of a C Program

VMA 示意图

Page Table (Mem pages are not all in memory)

  • 惰性交换器(Lazy swapper):非必要不将页交换进内存,处理页的交换器也被称为分页器(pager)
  • 预调页(Preparing):在进程引用页之前,预先将其需要的部分或全部页加载到内存中,可以减少页错误次数,但预测错误会反而浪费 I/O 和内存。

2. 2 Pure Demand Paging

极端情况下,一个进程启动时可以没有任何用户页在内存中。第一条指令、第一次访问数据、第一次访问栈,都可能触发 page fault。

Pure demand paging 的代价

  • 第一次访问每个页面都会 fault
  • 一条指令可能同时涉及多个页面,如指令所在页、数据所在页和页表本身需要访问的页,触发多次页错误
  • 若程序局部性不好,page fault 开销会非常大

Demand paging 之所以可行,是因为程序通常具有很强的locality(局部性)。一段时间内,进程往往集中访问少量页,而不是均匀访问整个虚拟地址空间。

需要的硬件支持
  • Page table entries with valid / invalid bit
  • Backing storage (usually disks)
  • Instruction restart

Zero-fill-on-demand

对匿名内存(如 heap / stack)而言,内核常在第一次访问时才分配物理页,并把页内容清零后交给进程。

这既避免泄露旧数据,也避免进程申请了大量内存但从未使用时浪费物理内存。

Stages in Demand Paging – Worse Case

进程 0 发生页错误后,CPU 先切换到进程 1 执行,待磁盘 I/O 完成后还需等待进程 1 时间片耗尽,再经第二次上下文切换切回进程 0 完成页错误处理并恢复运行,两次上下文切换与磁盘 I/O 叠加,造成了页错误处理的最大延迟。

2. 3 Performance

设 page fault rate 为 \(p\),普通 memory access time 为 \(M\),一次 page fault 的平均处理时间为 \(F\),则:

\[ EAT = (1-p) \times M + p \times F \]

其中

\[ F =\text{page fault overhead} + \text{swap page out} + \text{swap page in} + \text{instruction restart overhead} \]

EAT 示例

假设普通内存访问为 \(200 \, ns\),平均 page fault service time 为 \(8 \, ms\)

\[ EAT = (1-p) \times 200 + p \times 8{,}000{,}000 = 200 + p \times 7{,}999{,}800 \]

若每 1000 次访问有 1 次 page fault,即 \(p=0.001\),则:

\[ EAT \approx 8200 \, ns = 8.2 \, \mu s \]

平均访问时间会比 \(200 \, ns\) 慢约 \(41\) 倍。若要把额外开销控制在 10% 内,fault rate 必须低到约每 400,000 次访问不到 1 次 fault。

Page fault 的代价远高于普通内存访问,因此即使很小的 fault rate 也会显著影响平均访问时间。

Demand Paging Optimizations

  • Swap 空间优化:交换空间 I/O 比文件系统更快,早期 BSD 系统会预拷贝进程镜像到 Swap,后续直接在 Swap 中完成页换入/换出。
  • 只读页丢弃优化:只读文件映射页释放时直接丢弃,无需写 Swap,下次访问再从原文件重加载,仅匿名内存(堆/栈)、脏文件映射页需写 Swap 保存状态。
  • 移动系统适配:不支持 Swap,直接从文件系统按需调页,优先回收只读页,减少闪存写入。

3 Copy-on-Write

Copy-on-Write(COW)用于优化 fork()。父进程创建子进程时,内核并不立刻复制父进程的所有物理页,而是让父子进程暂时共享这些页,并把相关页标成只读。

当某一方尝试写共享页时,会触发 page fault。内核确认这是 COW fault 后,再复制该页,让写入方得到自己的私有副本。

COW 使得 fork() 不必立即复制整个地址空间,若子进程很快调用 exec(),多数页根本不用复制。只有真正被写过的页才会产生复制成本,只读代码页、共享库页可以继续共享。

vfork()

vfork() 是早期没有 COW 或 COW 成本较高时的优化。它让子进程直接共享父进程地址空间,并挂起父进程直到子进程 exec() 或退出。

因为子进程共享父进程的 heap 和 stack,所以 vfork() 使用非常脆弱,子进程不应从调用函数返回,也不应调用普通 exit(),通常应尽快调用 exec()_exit()

4 Page Replacement Algorithms

Demand paging 需要空闲 frame 才能把缺失页装入内存。若没有空闲 frame,OS 就必须选择某个当前在内存中的页作为 victim page,把它换出或丢弃,再把目标页装入该 frame,从而降低开销并避免抖动(thrashing)

Dirty bit 的作用

若 victim page 没有被修改过,可以直接丢弃;如果之后还需要,重新从文件或 swap 读入即可。

若 dirty bit = 1,则说明内存中的内容比磁盘新,换出前必须写回,否则会丢数据。

Page Fault Handler (with Page Replacement)
  1. 找到目标页在 backing store / 文件中的位置
  2. 寻找空闲 frame,若没有空闲 frame,运行 replacement algorithm 选择 victim
  3. 若 victim 是 dirty page,则先写回磁盘
  4. 把目标页读入 frame,更新目标进程 page table,重新执行触发 fault 的指令

注:此时一次页错误可能需要两次页 I/O 操作(换出牺牲页 + 换入目标页),这会增加 EAT。

评估说明

评估页替换算法时,通常给定一个 reference string,只记录访问的 page number,然后计算在固定 frame 数下会发生多少次 page fault。

后面讨论的参考串均为:

7,0,1,2,0,3,0,4,2,3,0,3,0,3,2,1,2,0,1,7,0,1

连续访问同一个已经在内存中的页不会产生新的 page fault。

算法总结
算法 Victim 选择 主要特点
FIFO 最早进入内存的页 简单,但可能出现 Belady's anomaly
OPT 未来最久不用的页 理论最优,只能作为比较基准
LRU 最久没有被访问的页 常用思想,不会出现 Belady's anomaly
Second Chance / Clock FIFO + reference bit LRU 的低成本近似
Enhanced Clock reference bit + dirty bit 同时考虑近期访问和写回成本
LFU / MFU 按访问次数选择 实际系统较少直接使用

4. 1 FIFO

FIFO(First-In-First-Out)总是替换最早装入内存的页面。它不关心页面最近是否被频繁访问,因此实现简单但效果不稳定。

Belady's Anomaly

对某些访问序列,FIFO 在 frame 数增加时反而可能产生更多 page fault,这称为 Belady's anomaly

例如参考串 1,2,3,4,1,2,5,1,2,3,4,5 下,FIFO 可能出现 3 个 frame 比 4 个 frame fault 更少的反常情况。

4. 2 Optimal

OPT(Optimal replacement)替换未来最长时间不会再被访问的页。

OPT 需要知道未来访问序列,因此无法在线实现。但它给出了某个 reference string 上的最小 page fault 数,可用来衡量其他算法离理论最优还有多远。对于当前示例,OPT 可以做到 9 次 page faults。

4. 3 LRU

LRU(Least Recently Used)替换最久没有被访问的页,不存在 Belady 异常。

实现 做法 代价
Counter-based 每次访问把当前时钟写入 PTE counter,替换时找 counter 最小的页 每次替换需要搜索,硬件 / OS 成本高
Stack-based 用链表维护访问顺序,每次访问把页移到栈顶,栈底是 victim 每次访问都要更新链表

4. 4 LRU Approximation

精确 LRU 代价太高,因此真实系统常使用引用位(reference bit)做近似。reference bit 由硬件在页面被访问时置 1,OS 周期性检查并清零。置换时优先选择引用位为 0 的页,但这种方法无法区分引用位为 0 的页之间的访问顺序。

Additional-reference-bits algorithm

可以为每个页维护一个多位历史寄存器,例如 8 bits:

  • 每隔固定时间右移一位
  • 若该周期内被访问过,则把最高位置 1,低位随移位被丢弃

例如:

  • 00000000:最近 8 个周期都没被访问
  • 11111111:最近 8 个周期都被访问
  • 1100010001110111 更新,因为最高位代表最近周期

4. 5 Second Chance / Clock

Second-chance 在 FIFO 队列基础上加入 reference bit:

  • 若队首页 reference bit = 0,直接替换
  • 若 reference bit = 1,说明近期访问过,把它清 0 并移动到队尾 / 跳过
  • 继续检查下一个页

Clock algorithm 用一个环形队列和指针实现 second-chance,因此成本更低。

4. 6 Enhanced Second Chance

增强版 second-chance 同时使用 reference bit 和 modify / dirty bit,把页面分成四类:

类别 含义 替换优先级
(0, 0) 最近未访问,且未修改 最适合替换
(0, 1) 最近未访问,但已修改 可替换,但要写回
(1, 0) 最近访问过,未修改 不太适合
(1, 1) 最近访问过,且已修改 最不适合

算法优先选择编号最低的非空类别,必要时可能绕 clock queue 多圈。

4. 7 LFU / MFU and Buffering

LFU(Least Frequently Used)替换访问次数最少的页,但问题是某些页可能在初始化阶段被频繁访问,之后再也不用,计数却仍然很高。

MFU(Most Frequently Used)则基于相反假设:访问次数少的页可能刚刚被装入,还没来得及被充分使用。但这类 counting-based 算法实际并不常作为主流页替换策略。

Page-buffering algorithms

OS 可以长期维护一批 free frames:

  • fault 时先把目标页读入 free frame,让进程尽快恢复
  • victim page 的写回可以延后到磁盘空闲时做
  • 被释放 frame 的旧内容可暂时保留,若很快又被访问,等价于一次 cache hit
Applications and Page Replacement

OS 的页替换算法是在预测程序未来访问模式,但有些应用比 OS 更了解自己的访问规律,例如数据库。

这类应用若同时使用 OS buffer cache 和自己的 buffer pool,可能出现 double buffering,同一份磁盘页在 OS 和应用中各存一份,浪费内存。某些系统允许应用通过 raw disk mode 绕过 OS buffering、locking 等机制,让应用自己管理缓存。

5 Allocation of Frames

页替换算法决定换掉哪个页,而 frame allocation 决定每个进程最多 / 至少能用多少 frame。

示例

IBM 370 的 SS MOVE 指令可能需要 6 个页面:

  • 指令本身可能跨 2 页
  • source operand 可能跨 2 页
  • destination operand 可能跨 2 页
分配策略 做法 特点
Equal allocation 每个进程分到相同数量 frame 简单,但忽略进程大小差异
Proportional allocation 按进程大小比例分配 frame 更合理,但需要动态调整
策略 Victim 来源 优点 缺点
Global replacement 所有进程的 frame 系统吞吐通常更高,内存利用率更好 一个进程会影响另一个进程的性能
Local replacement 本进程已分配 frame 单个进程性能更稳定 可能导致全局内存利用不足

多数通用系统更偏向 global replacement,但会配合优先级、工作集、cgroup / quota 等机制限制互相干扰。

5. 1 Reclaiming Pages

OS 通常不会等 free-frame list 完全耗尽才开始回收页面,而是设置高低阈值:

  • free memory 低于 minimum threshold 时开始积极 reclaim
  • 回收到 maximum threshold 时暂停 reclaim

What happens if memory is below the minimum threshold?

如果 reclaim 仍无法得到足够内存,系统可能触发 OOM 处理,按 OOM score 选择进程终止。

OOM score 反映进程在低内存情况下被杀掉的可能性,通常与内存占用、进程重要性等因素有关。

5. 2 Major and Minor Page Faults

类型 含义 是否需要磁盘 I/O
Major page fault 页面不在内存中,必须从磁盘读取
Minor page fault 当前映射不存在,但物理页已经在内存中

Minor fault 的例子包括共享库页已经在内存中、页面被 reclaim 但还没真正释放等。

5. 3 NUMA

NUMA(Non-Uniform Memory Access)系统中,不同 CPU 访问不同内存节点的延迟不同。为了提高性能,OS 应尽量:

  • 把线程调度到靠近其内存的 CPU 上
  • 从当前 CPU 所在 NUMA node 的 free-frame list 分配内存
  • 减少线程跨 NUMA domain 迁移

6 Thrashing

6. 1 Basic Concepts

Thrashing 的形成过程

如果进程没有分配到足够的页框,页错误率会变得很高。访问一个新页时必须换入,同时换出当前仍会很快用到的页,形成恶性循环,CPU 利用率下降,内核误以为需要提高 multiprogramming degree,更多进程进入内存,frame 更紧张,thrashing 更严重。

Thrashing 指进程把大量时间花在页面换入换出上,而不是实际执行指令。

Demand paging 能工作是因为程序访问有 locality,thrashing 出现是因为系统无法给所有活跃 locality 提供足够 frame。

解决方案

  • 本地页面置换:一个进程的 thrashing 不会影响其他进程。
  • 进程需要多少 frame 就提供多少
示意图

6. 2 Working-Set Model

Working set 描述进程在最近一段时间内真正活跃使用的页面集合。

Working-Set Model

设 working-set window 为 \(\Delta\),表示最近 \(\Delta\) 次内存引用。

进程 \(p_i\) 的工作集大小:

\[ WSS_i = \text{最近 } \Delta \text{ 次引用中出现过的不同页面数} \]

系统总需求:

\[ D = \sum_i WSS_i \]

\(D > m\),其中 \(m\) 是可用 physical frames 数,则系统可能发生 thrashing。

\(\Delta\) 应该如何选择
  • \(\Delta\) 太小:无法覆盖完整 locality,低估工作集
  • \(\Delta\) 太大:覆盖多个 locality,高估工作集
  • \(\Delta = \infty\):等于整个程序曾经访问过的全部页面,失去动态意义
近似维护 Working Set

精确记录最近 \(\Delta\) 次引用成本很高,OS 可用 timer + reference bit 近似:

  • 硬件在页被访问时设置 reference bit
  • OS 每隔一段时间扫描并保存 reference bit
  • 清零 reference bit,进入下一轮统计
  • 若某页在最近几个周期内至少被标记过一次,则认为它属于 working set

例如 \(\Delta = 10{,}000\) 个时间单位,timer 每 5,000 个时间单位中断一次,则可以为每页保存 2 个历史 bit。若任一 bit 为 1,就近似认为该页在 working set 中。

这种方法不完全精确,因为它只知道某页在某个区间内被访问过,无法知道访问发生在区间内的具体时刻。若改成 10 个历史 bit、每 1,000 个时间单位中断一次,精度会更高。

6. 3 Page-Fault Frequency

Page-Fault Frequency(PFF)比 working-set model 更直接,根据实际 page fault rate 调整 frame 分配。

  • fault rate 太高:给进程更多 frame
  • fault rate 太低:回收部分 frame
  • 没有空闲 frame 可给:挂起 / 换出某些进程

7 Other Considerations

Kernel Memory Allocation

内核内存分配和用户内存分配不同。内核经常需要为各种数据结构分配小块对象,也可能需要为 DMA / device I/O 分配物理连续内存。

Kernel memory allocation 分配大小变化多,需要减少碎片,某些场景要求 physically contiguous,分配 / 释放路径必须很快。

7. 1 Page Size

页大小(Page size)是内存系统中的重要折中。

倾向 原因
小页 内部碎片少、保护粒度细、locality 表达更精确
大页 页表更小、I/O 批量更大、TLB reach 更大、TLB miss 更少

页大小总是 2 的幂,通常在 \(2^{12}\)(4,096 字节)到 \(2^{22}\)(4,194,304 字节)之间,平均来看随着时间增长。

TLB Reach

\[ TLB\ reach = TLB\ entries \times page\ size \]

若 working set 超出 TLB reach,即使数据都在内存中,也可能频繁发生 TLB miss。大页可以提高 TLB reach,但会增加内部碎片。

7. 2 Program Structure

程序访问顺序会直接影响 page fault 次数。

数组访问顺序

假设 int data[128][128],每行刚好存放在一个 page 中,系统有 127 个 frames。

按列访问:

for (j = 0; j < 128; j++)
    for (i = 0; i < 128; i++)
        data[i][j] = 0;

每次内层循环都会跨页访问,可能造成 \(128 \times 128 = 16{,}384\) 次 page faults。

按行访问:

for (i = 0; i < 128; i++)
    for (j = 0; j < 128; j++)
        data[i][j] = 0;

每行只 fault 一次,约 \(128\) 次 page faults。

7. 3 I/O Interlock

用于 I/O 的页面有时必须被锁定在内存中,不能被 page replacement 选为 victim。例如设备正在把数据 DMA 到某个物理页时,该页不能被换出或重新分配。

Windows XP

Windows XP 使用 demand paging,并通过 clustering 在缺页时顺带读入 faulting page 附近的页面,以利用空间局部性。

每个进程有 working set minimum 和 maximum:

  • wsmin:进程保证能保留的最小页数
  • wsmax:进程最多可拥有的页数

当系统 free memory 低于阈值时,Windows 会自动 trim working set,从拥有超过 wsmin 的进程中回收页面。

8 Linux Memory Management

8. 1 Buddy System

Linux 内核使用 buddy system 管理物理页框。它按 \(2^n\) 大小分配连续页块,并通过 buddy 关系快速拆分和合并。

示例

若当前只有一个 256KB 空闲块,而内核申请 21KB:

  1. 256KB 拆成两个 128KB buddy
  2. 一个 128KB 再拆成两个 64KB buddy
  3. 一个 64KB 再拆成两个 32KB buddy
  4. 分配一个 32KB 块给 21KB 请求

  • 优点:快速找到连续物理页,释放后容易合并成大块。
  • 缺点:按 \(2^n\) 向上取整,可能造成 internal fragmentation。

8. 2 Slab Allocator

Buddy system 适合分配连续页块,但内核中还有大量固定大小的小对象,如 task_struct、inode、dentry 等。频繁向 buddy system 申请 / 释放这些小对象会很慢,也容易产生碎片。

Slab allocator 为每类常用对象维护 cache。一个 cache 由多个 slab 组成,每个 slab 包含若干连续页,并被切成多个等大小对象。

  • Full:slab 中所有对象都已分配
  • Partial:部分对象已分配,部分空闲
  • Empty:全部对象空闲

分配对象时,优先从 partial slab 中取空闲对象,若没有就使用 empty slab,若还没有就新建 slab。

Slab Allocator 的优点

  • 分配 / 释放固定大小对象很快
  • 减少小对象碎片
  • 对象字段可能复用,减少初始化成本
  • 与 buddy system 分工明确:buddy 管页块,slab 管内核对象
SLAB / SLOB / SLUB
  • SLAB:早期 Linux slab allocator
  • SLOB:面向小内存系统的 simple list of blocks
  • SLUB:现代 Linux 常用的高性能 slab allocator,简化队列结构,把更多元数据放入 page structure

9 Linux Virtual Memory

引入

早期或简单系统可能使用 flat memory:所有程序、OS、外设共享同一个地址空间。

CPUs with flat memory
  • 8086-80286 时代的早期 x86
  • ARM Cortex-M
  • 很多 8-bit / 16-bit IoT 芯片

Flat memory 没有内存保护,用户程序可能破坏内核内存,进程之间难以隔离,外设与 RAM 混在同一物理地址空间中,管理复杂。旧 x86 中 RAM 还会被 DOS area、extended memory、硬件映射区域切开,访问方式更复杂。

虚拟内存则为每个进程建立独立映射:

  • 同一个 virtual address 在不同进程中可以指向不同 physical frame
  • 用户态看不到内核内存,权限位阻止非法读 / 写 / 执行
  • 物理页可以被移动、换出、共享或映射到外设

9. 1 Linux Address Spaces

Linux 中软件通常使用 virtual address,包括用户态和内核态,地址空间按架构分成用户区和内核区。

32-bit Linux

经典 32-bit Linux 通常采用 3G/1G 分割:

  • 0x00000000 ~ 0xBFFFFFFF:user space,约 3GB
  • 0xC0000000 ~ 0xFFFFFFFF:kernel space,约 1GB
  • CONFIG_PAGE_OFFSET 默认常为 0xC0000000

每个进程有自己的 user-space 映射,context switch 时,内核通过类似 switch_mm() 的路径切换当前进程的地址空间。

64-bit / ARM64

64-bit 系统的虚拟地址空间远大于物理内存,kernel / user split 依架构而定,ARM64 常见配置有:

  • 48-bit VA:四级页表,\(9 + 9 + 9 + 9 + 12\)

  • 39-bit VA:三级页表,\(9 + 9 + 9 + 12\)

4KB page configuration
  • 3 levels of page tables (pgtable-nopud.h)
  • Linear mapping using 4KB, 2MB or 1GB blocks
  • AArch32 (compat) supported

9. 2 Linux Virtual Addresses

Linux 设备驱动和内核内存管理中常区分三类虚拟地址:

类型 含义 映射特点
User virtual address 用户进程使用的虚拟地址 每个进程私有,通常虚拟连续、物理不连续
Kernel logical address 线性映射物理内存的内核地址 virtual 与 physical 有固定偏移,转换快
Kernel virtual address vmalloc() 等非连续映射区域 虚拟连续,但物理可不连续

Kernel logical address

在经典 32-bit 低内存系统中,PAGE_OFFSET 以上的一段 kernel logical address 线性映射低端物理内存,例如:

\[ Virt: 0xC0000000 \rightarrow Phys: 0x00000000 \]

因此可以用类似 pa(x) / va(x) 的宏在二者之间转换。

Large physical RAM

32-bit kernel address space 只有约 1GB,且顶端还要保留一部分给 vmalloc() 等非连续映射。因此当物理内存超过约 1GB 时,不可能把所有 RAM 都线性映射进 kernel logical address space。

结果是只有底部一部分物理 RAM 有直接 kernel logical address,其他高端内存需要额外映射机制访问。64-bit 系统一般没有这个问题。

Kernel Virtual Addresses

Kernel Virtual Addresses (Large Mem)

9. 3 Others

虚拟连续区域的映射不必对应物理上连续的内存,这让内存分配变得更简单,几乎所有用户态代码都不需要物理连续的内存。

每个进程都拥有独立的页表映射,不同进程中的相同虚拟地址可能指向不同的物理地址。

共享内存可以通过把同一个 physical frame 映射进不同进程实现。不同进程中的 virtual address 不一定相同。

示意图

共享内存中的指针问题

若共享区域内部保存的是绝对指针,那么不同进程必须把该区域映射到相同 virtual address,否则指针值在另一个进程中没有意义。

mmap() 允许用户请求一个目标 virtual address,但内核不一定能满足;若地址冲突或不合法,映射会失败或被安排到其他位置。

9. 4 Memory Management Unit

页是虚拟 / 逻辑层面的单元,帧是物理内存层面的单元,二者通过页表建立映射关系。

  • MMU 位于 CPU core 和内存系统之间,负责把 virtual address 翻译成 physical address
  • TLB 是 MMU 内部或旁边的高速缓存,保存最近使用的地址翻译和权限位
  • 若 TLB miss,硬件或软件需要进行 page table walk
  • 若页表项不存在、权限不满足或页面被换出,则触发 page fault
常见 page size
  • ARM:4KB
  • ARM64:4KB 或 64KB
  • x86:4KB
  • MIPS:配置更灵活

页大小通常在 kernel build time 或体系结构配置中决定。

Linux 中的关键结构
  • mm_struct:描述进程地址空间
  • mm_struct.pgd:指向顶级页表
  • vm_area_struct:描述一段连续虚拟区域及其权限 / 映射来源
  • switch_mm():context switch 时切换地址空间
  • do_page_fault():体系结构相关的 page fault 入口
  • __handle_mm_fault():执行主要 fault 处理和 page table walk

Lazy allocation in Linux

用户进程申请内存时,内核可以只建立 VMA 记录,而不立刻分配物理页,也不立即更新 TLB。

当进程第一次访问该区域时,MMU 触发 page fault;内核发现地址合法后,再分配物理页、清零、更新页表,然后返回用户态继续执行。

对实时性敏感的程序,可以在启动阶段主动触碰相关内存,或使用 mlock() / mlockall() 降低运行中首次访问带来的 page fault 风险。

当内存紧张时,Linux 可以把某些页面写入 swap,释放对应 physical frame。之后进程再次访问该页时,MMU 会因为 PTE 不 present 而触发 page fault,内核再把页面从 swap 读回。

换入后不必回到原 frame

页面从 swap 读回时,不需要回到原来的物理 frame。只要更新 page table,让原来的 virtual address 指向新的 frame,用户程序就感知不到变化。

这也是用户态普通内存通常不能直接用于 DMA 的原因之一:设备使用 physical address,而 OS 可能移动或换出该页。

What does the memory layout of a user-space program look like?