内存管理单元的来源
在早期的计算机系统中(如 DOS 时代),程序直接操作物理内存地址,加载程序的位置以及程序使用的内存地址都由程序员手动指定。随着计算机技术的发展,CPU 越来越快,内存也越来越大,多道程序的需求应运而生,其中内存管理变成了需要解决的头等难题:
- 需要保证程序加载时的内存地址不会出现冲突
- 需要解决不同程序间内存隔离的问题
- 需要解决日益增长的内存容量的问题
于是,虚拟地址技术出现了。它为每个程序构建了一个独立且相同的线性地址内存视图。但要将虚拟地址作用于硬件上的电路,还需要将它翻译成物理地址。出于性能考虑,这些工作目前普遍由硬件**内存管理单元(MMU)**实现。

MMU 基本概念与作用
MMU(Memory Management Unit)是 CPU 中负责虚拟地址到物理地址转换的硬件单元。它的核心职责包括:
| 功能 | 说明 |
|---|---|
| 地址转换 | 将 CPU 发出的虚拟地址翻译为物理内存地址 |
| 访问权限检查 | 根据页表中的权限位判断当前操作是否合法(读/写/执行) |
| 内存隔离 | 为不同进程维护独立的地址空间,防止互相干扰 |
| 缺页异常触发 | 当访问的虚拟页未映射到物理页时,触发缺页异常交由操作系统处理 |
| Cache 控制 | 通过页表属性位控制页面缓存策略(Write-Back / Write-Through) |
在现代操作系统中,每个进程都有自己独立的虚拟地址空间。以 64 位 Linux 系统为例,用户空间通常占 128 TB(0x0000000000000000 ~ 0x00007fffffffffff),内核空间也占 128 TB。这些虚拟地址到物理内存的映射关系由页表描述,而 MMU 就是查页表、完成地址转换的硬件执行者。
虚拟地址到物理地址的转换过程
MMU 即内存管理单元,它通过软件在内存中提供的虚拟→物理地址转换表进行地址转换。目前它一般配合分页内存模型一起使用,因此这个地址转换表也可称为页表,它描述了虚拟页到物理页的映射关系。页表的结构如下图所示:

分页机制的基本概念
分页(Paging)是将虚拟地址空间和物理地址空间都划分为固定大小的块,称为页(Page)和页帧(Frame / Page Frame)。两者大小相同,常见的页面大小为 4 KB。
转换过程可以分为以下几个步骤:
1. CPU 生成虚拟地址(Virtual Address)2. MMU 将虚拟地址拆分为:虚拟页号(VPN)+ 页内偏移(Offset)3. MMU 用 VPN 查询页表,得到物理页帧号(PFN)4. 物理地址 = PFN × 页面大小 + Offset5. 如果页表项标记为"不存在",触发缺页异常(Page Fault)以 32 位系统、4 KB 页面为例:
31 12 11 0+--------------------+----------------------+| 虚拟页号 (VPN) | 页内偏移 (Offset) || 20 bits | 12 bits |+--------------------+----------------------+- 虚拟页号:高 20 位,用于在页表中索引,最多表示 2^20 = 1,048,576 个页表项
- 页内偏移:低 12 位,因为 4 KB = 2^12,所以 12 位偏移可以覆盖一页内的每个字节
最终物理地址的拼接方式:
物理地址 = 物理页帧号 << 12 | 页内偏移完整的地址转换流程
页表结构
单级页表
最简单的页表结构就是单级页表(Flat Page Table),即用一个连续的数组存储所有虚拟页到物理页的映射。
以 32 位地址空间、4 KB 页面为例:
- 虚拟页数量 = 2^32 / 4 KB = 2^20 = 1,048,576 个页表项
- 每个页表项通常占 4 字节
- 页表总大小 = 1,048,576 × 4 = 4 MB
对于 32 位系统,每个进程 4 MB 的页表尚可接受。但如果切换到 64 位系统:
- 虚拟页数量 = 2^64 / 4 KB = 2^52 ≈ 4.5 × 10^15 个页表项
- 页表总大小 = 2^52 × 8 = 32 PB
这显然是不现实的。因此,现代处理器都使用多级页表来解决这个问题。
二级页表
二级页表(Two-Level Paging)是 x86 32 位系统使用的经典方案。它将 20 位的虚拟页号进一步拆分为两部分:
31 22 21 12 11 0+--------+--------+----------------------+| 页目录 | 页表项 | 页内偏移 (Offset) || 10 bit | 10 bit | 12 bits |+--------+--------+----------------------+- 页目录索引(10 位):索引页目录(Page Directory),共 2^10 = 1024 个目录项
- 页表索引(10 位):索引页表(Page Table),每个页表有 2^10 = 1024 个页表项
- 页内偏移(12 位):页内字节偏移
转换过程:
1. CR3 寄存器指向当前进程的页目录物理地址2. 用[31:22]索引页目录,找到对应的页表物理地址3. 用[21:12]索引页表,找到对应的物理页帧号4. 物理地址 = PFN << 12 | Offset二级页表的优势在于按需分配:如果某个虚拟地址范围没有被使用,对应的二级页表根本不需要创建。这意味着一个只用了少量内存的进程,实际页表占用量可能只有几 KB 而非 4 MB。
三级页表
某些 32 位架构(如带有 PAE 扩展的 x86)使用三级页表。PAE(Physical Address Extension)将物理地址总线从 32 位扩展到 36 位,支持最多 64 GB 物理内存。
在 PAE 模式下:
31 30 29 21 20 12 11 0+------+------+----------+----------------+| PDPT | 页目录 | 页表项 | Offset || 2 bit| 9 bit | 9 bit | 12 bits |+------+------+----------+----------------+四级页表(x86-64 / Linux 默认)
64 位系统普遍使用四级页表。x86-64 架构下,虽然虚拟地址理论上是 64 位,但实际上只使用了 48 位(canonical address),地址空间为 256 TB。
Linux 在 x86-64 上使用四级页表结构(4-Level Paging,也叫四级页表):
47 39 38 30 29 21 20 12 11 0+------+------+------+------+----------------+| PML4 | PDPT | PD | PT | Offset || 9bit | 9bit | 9bit | 9bit | 12 bits |+------+------+------+------+----------------+各级页表的含义:
| 层级 | 名称 | 全称 | 条目数 | 索引位数 |
|---|---|---|---|---|
| 第4级 | PML4(Page Map Level 4) | 顶级页表 | 512 | 9 位 |
| 第3级 | PDPT(Page Directory Pointer Table) | 页目录指针表 | 512 | 9 位 |
| 第2级 | PD(Page Directory) | 页目录 | 512 | 9 位 |
| 第1级 | PT(Page Table) | 页表 | 512 | 9 位 |
每次地址转换需要访问 4 次内存(逐级查表),这也是为什么 TLB 如此重要。
注意:较新的 Intel 处理器(Ice Lake 及之后)引入了五级页表(LA57),将虚拟地址空间扩展到 128 PB,多了一级 PML5(Page Map Level 5)。目前 Linux 从 4.14 开始支持五级页表。
多级页表内存开销对比
以 64 位 Linux 系统为例,假设进程只使用了 1 GB 的虚拟内存:
| 方案 | 页表大小 |
|---|---|
| 单级页表 | 2^52 × 8 ≈ 32 PB(理论值,不可能实现) |
| 四级页表 | 1 个 PML4 + 1 个 PDPT + 1 个 PD + 512 个 PT = 约 2 MB |
多级页表把内存开销从天文数字降到了可接受的范围。
TLB(Translation Lookaside Buffer)
为什么需要 TLB
从上面的分析可知,四级页表每次地址转换需要 4 次内存访问。假设内存访问延迟为 100 ns,那么一次地址转换就需要 400 ns,加上最终的数据访问共 500 ns。而 CPU 的时钟周期可能只有 0.3 ns(3 GHz),这意味着一次内存访问就要等待上千个时钟周期。
为了解决这个问题,MMU 内部集成了一个高速缓存——TLB(Translation Lookaside Buffer),用于缓存最近使用的虚拟页到物理页的映射关系。
TLB 工作原理
TLB 本质上是一个全相联或组相联的 Cache,其条目格式如下:
+-------------------+-------------------+----------+| 虚拟页号 (VPN) | 物理页帧号 (PFN) | 标志位 |+-------------------+-------------------+----------+标志位包括:有效位(Valid)、读写权限(R/W)、用户/超级用户(U/S)、脏位(Dirty)等。
工作流程:
1. CPU 发出虚拟地址2. MMU 提取 VPN3. 在 TLB 中并行匹配所有条目4. 如果命中(Hit): - 直接获取 PFN,拼接物理地址 - 访问延迟 ≈ 1-2 个时钟周期5. 如果未命中(Miss): - 遍历多级页表(Hardware Page Table Walk) - 将新映射写入 TLB(可能淘汰旧条目) - 访问延迟 ≈ 数十个时钟周期TLB 的分层结构
现代 CPU 通常将 TLB 分为两层:
| 层级 | 名称 | 大小(典型值) | 覆盖范围 | 延迟 |
|---|---|---|---|---|
| L1 TLB | 一级 TLB | 64-128 条目 | 指令 / 数据分离 | 1-2 周期 |
| L2 TLB | 二级 TLB | 512-1536 条目 | 指令数据共享 | 5-10 周期 |
例如 Intel Core i7 处理器的 TLB 参数:
- L1 ITLB:64 条目(4 KB 页),32 条目(大页)
- L1 DTLB:64 条目(4 KB 页),32 条目(大页)
- L2 STLB(共享):1536 条目(4 KB 页)
TLB 刷新与上下文切换
当发生进程切换时,新进程的虚拟地址空间与旧进程完全不同,因此 TLB 中的旧映射不再有效。处理方式有三种:
- 全部刷新:最简单的方式,切换时清空所有 TLB 条目。代价是切换后会有大量 TLB Miss。
- ASID 标记(Address Space ID):为每个进程分配一个 ASID,TLB 条目中附带 ASID 标记。查找时只匹配当前 ASID 的条目,避免刷新。ARM 和 MIPS 架构使用这种方式。
- PCID 机制(Process-Context Identifier):x86 从 Nehalem 架构开始支持类似 ASID 的机制,称为 PCID。Linux 从 2.6.36 开始支持 PCID。
页面大小与 TLB 覆盖范围
TLB 覆盖范围的计算
TLB 的覆盖范围是指 TLB 中所有条目能够映射的物理内存总量。计算公式:
TLB 覆盖范围 = TLB 条目数 × 页面大小以 L2 STLB 1536 条目、4 KB 页面为例:
覆盖范围 = 1536 × 4 KB = 6144 KB ≈ 6 MB这意味着如果一个进程的工作集(Working Set)超过 6 MB,就会频繁发生 TLB Miss。对于现代数据库、科学计算等需要访问大量内存的应用来说,这个覆盖范围远远不够。
不同页面大小的影响
| 页面大小 | TLB 覆盖范围(1536 条目) | 适用场景 |
|---|---|---|
| 4 KB | 6 MB | 通用应用 |
| 2 MB | 3 GB | 数据库、虚拟化 |
| 1 GB | 1.5 TB | 大规模内存数据库 |
显然,更大的页面可以让 TLB 覆盖更多内存,减少 TLB Miss。
大页的代价
大页并非没有代价:
- 内存浪费:大页的分配粒度更大,即使只使用其中一小部分,也要占用整页。例如 2 MB 大页哪怕只用了 1 KB,也要分配 2 MB。
- 碎片化:随着系统运行,物理内存会碎片化,很难找到连续的 2 MB 或 1 GB 物理页帧。
- 管理复杂度:操作系统需要额外的数据结构来管理不同大小的页面。
Linux 中的内存管理
核心源码结构
Linux 内核的内存管理代码主要位于 mm/ 目录下,以下是关键文件及其职责:
| 源文件 | 职责 |
|---|---|
| mm/page-table.c | 页表操作辅助函数 |
| mm/memory.c | 缺页处理、页表遍历、COW 等 |
| mm/mmap.c | 虚拟内存区域(VMA)管理 |
| mm/hugetlb.c | 大页(HugeTLB)管理 |
| mm/page_alloc.c | 物理页帧分配器(Buddy System) |
| arch/x86/mm/init.c | x86 架构的内存初始化 |
| arch/x86/mm/pageattr.c | 页表属性修改(加密、保护等) |
关键数据结构
pgd_t、p4d_t、pud_t、pmd_t、pte_t 分别对应五级页表的每一级:
// 各级页表项类型定义(以 x86-64 为例)typedef unsigned long pgd_t; // Page Global Directorytypedef unsigned long p4d_t; // Page 4-level Directory (五级页表)typedef unsigned long pud_t; // Page Upper Directorytypedef unsigned long pmd_t; // Page Middle Directorytypedef unsigned long pte_t; // Page Table Entrystruct vm_area_struct 描述进程的一段连续虚拟内存区域:
struct vm_area_struct { unsigned long vm_start; // 虚拟区域起始地址 unsigned long vm_end; // 虚拟区域结束地址 struct mm_struct *vm_mm; // 所属的内存描述符 struct file *vm_file; // 关联的文件(mmap) unsigned long vm_flags; // 属性标志(读/写/执行/共享等) // ...};struct mm_struct 描述一个进程的整个虚拟地址空间:
struct mm_struct { pgd_t *pgd; // 指向顶级页目录 unsigned long task_size; // 进程地址空间大小 unsigned long total_vm; // 总虚拟内存页数 unsigned long locked_vm; // 被锁定(不可换出)的页数 struct list_head mmlist; // 所有 mm_struct 的链表 // ...};页表遍历代码示例
Linux 内核提供了 follow_page 系列函数来遍历页表,以 follow_pte 为例,它展示了四级页表的逐级查找过程:
// 简化版 follow_pte 逻辑int follow_pte(struct mm_struct *mm, unsigned long addr, pte_t **ptepp){ pgd_t *pgd; p4d_t *p4d; pud_t *pud; pmd_t *pmd; pte_t *pte;
// 第1级:查 PGD(Page Global Directory) pgd = pgd_offset(mm, addr); if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd))) return -EINVAL;
// 第2级:查 P4D(Page 4-level Directory) p4d = p4d_offset(pgd, addr); if (p4d_none(*p4d) || unlikely(p4d_bad(*p4d))) return -EINVAL;
// 第3级:查 PUD(Page Upper Directory) pud = pud_offset(p4d, addr); if (pud_none(*pud) || unlikely(pud_bad(*pud))) return -EINVAL;
// 第4级:查 PMD(Page Middle Directory) pmd = pmd_offset(pud, addr); if (pmd_none(*pmd) || unlikely(pmd_bad(*pmd))) return -EINVAL;
// 第5级:查 PTE(Page Table Entry) pte = pte_offset_map(pmd, addr); if (!pte || !pte_present(*pte)) return -EINVAL;
*ptepp = pte; return 0;}透明大页(THP)实现
Linux 的**透明大页(Transparent Huge Pages, THP)**机制允许内核自动将连续的 4 KB 小页合并为 2 MB 大页,对应用程序完全透明。核心实现在 mm/huge_memory.c 中:
// 简化版 khugepaged 扫描逻辑// khugepaged 是一个内核线程,定期扫描进程的虚拟内存,// 尝试将连续的 4KB 小页合并为 2MB 大页static void khugepaged_scan_mm_slot(void){ // 遍历进程的 VMA // 检查是否可以合并连续的小页 // 如果满足条件,分配 2MB 大页并复制数据 // 更新页表映射}可以通过以下方式控制系统的大页行为:
# 查看当前 THP 状态cat /sys/kernel/mm/transparent_hugepage/enabled
# 启用 THP(三种模式)echo always > /sys/kernel/mm/transparent_hugepage/enabled # 总是启用echo madvise > /sys/kernel/mm/transparent_hugepage/enabled # 仅对 MADV_HUGEPAGE 的 VMA 启用echo never > /sys/kernel/mm/transparent_hugepage/enabled # 禁用
# 查看预分配的大页数量cat /proc/sys/vm/nr_hugepages
# 预分配 100 个 2MB 大页echo 100 > /proc/sys/vm/nr_hugepages实际影响:为什么大页能提升数据库性能
TLB Miss 的性能惩罚
数据库系统(如 MySQL、PostgreSQL、Redis)通常需要访问大量内存中的数据页。以 MySQL InnoDB 为例:
- InnoDB 的 Buffer Pool 可能配置为几十 GB 甚至上百 GB
- 假设 Buffer Pool 为 32 GB,使用 4 KB 页面
- 需要映射的页数 = 32 GB / 4 KB = 8,388,608 个页
- 而 L2 STLB 只有 1536 条目,覆盖率仅为 1536 / 8,388,608 ≈ 0.018%
这意味着几乎每次内存访问都会发生 TLB Miss。每次 TLB Miss 的代价约为 20-100 个 CPU 周期(需要遍历四级页表),加上可能的 Cache Miss,性能损失非常显著。
大页的优化效果
使用 2 MB 大页后:
- 需要映射的页数 = 32 GB / 2 MB = 16,384 个页
- L2 STLB 覆盖率 = 1536 / 16,384 ≈ 9.4%
- TLB 覆盖范围 = 1536 × 2 MB = 3 GB
TLB 覆盖率提升了约 500 倍,大幅度减少了 TLB Miss。
实际测试数据显示,在大内存工作负载下,启用大页可以带来 5%-30% 的性能提升。
MySQL 中的大页配置
MySQL 支持两种大页模式:
1. Transparent Huge Pages(THP)
# 在系统层面启用 THPecho always > /sys/kernel/mm/transparent_hugepage/enabled
# MySQL 无需额外配置,自动利用 THP注意:很多生产环境建议关闭 THP,因为 THP 的后台合并操作可能导致延迟抖动(latency spike)。Oracle、MongoDB 等官方文档也建议关闭 THP。
2. 静态大页(HugeTLB)
# 1. 计算需要的大页数量# 假设 Buffer Pool = 8 GB,大页大小 = 2 MB# 需要的大页数 = 8 GB / 2 MB = 4096echo 4096 > /proc/sys/vm/nr_hugepages
# 2. 配置 MySQL 使用大页# 在 my.cnf 中添加:# [mysqld]# large-pages
# 3. 确保 MySQL 用户有权限使用大页# 查看 memlock 限制ulimit -l
# 在 /etc/security/limits.conf 中设置:# mysql soft memlock unlimited# mysql hard memlock unlimitedPostgreSQL 中的大页配置
PostgreSQL 从 9.4 版本开始支持大页:
# 1. 计算所需的共享内存和大页数# PostgreSQL 使用共享内存进行进程间通信# 假设 shared_buffers = 4 GB# 需要的大页数 = 4 GB / 2 MB + 适当的余量 = 2050
echo 2050 > /proc/sys/vm/nr_hugepages
# 2. 在 postgresql.conf 中启用# huge_pages = try # 尝试使用大页,失败则回退# huge_pages = on # 强制使用大页,无可用大页则启动失败# huge_pages = off # 不使用大页Redis 中的大页配置
Redis 从 5.0 版本开始支持用户态大页(通过 madvise 系统调用):
# 在 redis.conf 中配置activedefrag yes不过 Redis 官方建议在生产环境中关闭 THP,因为 Redis 的 fork + COW(Copy-On-Write)持久化机制与 THP 可能冲突:fork 后的子进程需要复制父进程的页表,如果使用了 THP,那么复制 2 MB 大页的页表条目比 4 KB 小页要慢得多。
# Redis 官方建议echo never > /sys/kernel/mm/transparent_hugepage/enabled性能对比总结
| 场景 | 4 KB 页面 | 2 MB 大页 | 提升幅度 |
|---|---|---|---|
| MySQL OLTP (8GB) | 基准 | +8%~15% | QPS 提升 |
| PostgreSQL OLAP | 基准 | +10%~25% | 查询延迟降低 |
| Redis 全量扫描 | 基准 | +5%~10% | 吞吐提升 |
| 内存带宽密集型任务 | 频繁 TLB Miss | TLB Miss 大幅减少 | 显著提升 |
小结
MMU 是现代计算机系统中不可或缺的硬件组件,它通过页表实现虚拟地址到物理地址的转换,为操作系统提供了内存隔离、按需分配等关键能力。以下是本文的核心要点:
- MMU 的核心作用:地址转换、权限检查、内存隔离,是操作系统虚拟内存的硬件基础
- 多级页表:解决了大地址空间下页表过大的问题,Linux x86-64 使用四级(或五级)页表
- TLB 加速:通过缓存页表映射关系,将地址转换延迟从数十个时钟周期降到 1-2 个时钟周期
- 大页优化:通过增大页面大小(2 MB / 1 GB),显著扩大 TLB 覆盖范围,减少 TLB Miss,对数据库等大内存应用至关重要
- 实际影响:正确配置大页可以为数据库带来 5%-30% 的性能提升,但需要注意 THP 与 fork + COW 的冲突问题
参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






