虚拟内存基础概念:物理地址与虚拟地址、MMU工作原理、页表与页表项
好,我们正式开始聊虚拟内存。说实话,这个概念刚接触时很容易绕晕。我当年第一次看《深入理解Linux内核》时,看到「虚拟地址」和「物理地址」这两个词,脑子里全是问号——地址就地址,还分什么虚拟和物理?
后来在项目中踩过坑,才真正理解这套设计的精妙之处。今天我就把这块掰开了讲清楚。
物理地址 vs 虚拟地址
先说说最基础的概念。
物理地址,就是硬件层面实实在在的地址。CPU通过地址总线直接访问内存芯片上的某个存储单元。比如你的内存条是4GB,那物理地址范围就是0x00000000到0xFFFFFFFF。说白了,物理地址就是内存条上的门牌号。
虚拟地址,是每个进程看到的地址空间。在32位系统上,每个进程都觉得自己独占4GB内存(0x00000000到0xFFFFFFFF)。但实际上,这只是一个「幻觉」。
你想想看,如果同时跑10个进程,每个都觉得自己有4GB内存,那总共需要40GB。你的物理内存才4GB,怎么可能?
嗯,这就是虚拟内存要解决的问题——让多个进程安全地共享有限的物理内存。
核心要点:虚拟地址是进程看到的,物理地址是硬件实际使用的。中间有个翻译过程,这个翻译官就是MMU。
我在项目中遇到过一个问题:一个嵌入式设备,内存只有256MB,但有个应用非要申请512MB的缓冲区。当时同事说「这不可能」。我说「不一定,用虚拟内存映射,只要不全部同时访问,就能搞定」。后来我们用mmap做了文件映射,确实跑起来了。
MMU工作原理
MMU,全称Memory Management Unit,内存管理单元。它是CPU内部的一个硬件模块,专门负责地址翻译。
它的工作流程,我习惯用三步来理解:
- CPU发出虚拟地址:比如进程执行指令
mov eax, [0x8048000],CPU就把0x8048000这个虚拟地址发给MMU。 - MMU查页表:MMU拿着这个虚拟地址,去查当前进程的页表,找到对应的物理地址。
- 访问物理内存:MMU把翻译后的物理地址发给内存控制器,真正去读写数据。
整个过程对进程来说是透明的。进程根本不知道自己的地址被「翻译」过。它只管用虚拟地址,剩下的脏活累活全交给MMU。
为什么会这样设计?说白了,就是为了隔离和安全。进程A无法访问进程B的物理内存,因为它们的页表是独立的。MMU在翻译时,如果发现虚拟地址没有对应的物理页,直接触发缺页异常,操作系统就能介入处理。
个人经验:调试MMU相关的问题时,我最常用的工具是看页表转储。在Linux下,/proc/self/pagemap 这个文件能告诉你每个虚拟页对应的物理页框号。我曾经靠这个定位过一个DMA缓冲区地址不对的bug。
页表与页表项
页表,就是MMU用来翻译地址的「字典」。每个进程都有自己的页表。
页表的基本单位是页表项(PTE,Page Table Entry)。每个PTE对应一个虚拟页到物理页的映射关系。
一个典型的页表项包含以下字段:
| 字段 | 位数 | 说明 |
|---|---|---|
| 物理页框号 | 20位(32位系统) | 映射到的物理页基地址 |
| Present位 | 1位 | 该页是否在物理内存中 |
| Read/Write位 | 1位 | 是否可写 |
| User/Supervisor位 | 1位 | 用户态能否访问 |
| Accessed位 | 1位 | 该页是否被访问过 |
| Dirty位 | 1位 | 该页是否被写过 |
你看,一个PTE才32位(4字节),却能管理4KB的物理页。这就是映射的威力——用很小的元数据管理大块内存。
我记得在x86架构下,页表是两级结构(32位时代)。虚拟地址被拆成三部分:
- 页目录索引(10位):在页目录表中找到对应的页目录项
- 页表索引(10位):在页表中找到对应的页表项
- 页内偏移(12位):在物理页内的具体位置
翻译过程就像查字典:先查目录,再查具体页表,最后找到物理地址。
// 虚拟地址 0x8048000 的分解示例(4KB页大小)
// 0x8048000 = 0000 0000 0000 1000 0000 0100 1000 0000 0000 0000
// 页目录索引: 0x01 (1)
// 页表索引: 0x01 (1)
// 页内偏移: 0x000 (0)
注意:多级页表虽然节省了内存(不需要为整个4GB空间都分配页表),但增加了翻译延迟。每次地址翻译都要查多次内存。为了解决这个问题,CPU引入了TLB(Translation Lookaside Buffer)——一个页表缓存。我调过TLB miss率过高的问题,那真是性能杀手。
在微内核设计中,页表的管理方式又有所不同。微内核通常把页表管理放在用户态的服务进程中,内核只提供最基础的映射操作。这样做的好处是灵活,坏处是上下文切换时页表切换开销更大。
我曾经在一个微内核项目里,为了优化进程切换时的TLB刷新,折腾了整整两周。最后发现,用ASID(Address Space Identifier)可以避免每次都刷TLB,性能提升很明显。
嗯,关于页表,还有一点要提:大页。现代CPU支持2MB甚至1GB的大页。大页的好处是减少页表级数,降低TLB miss。我在数据库场景下用过1GB大页,性能提升立竿见影。
好了,虚拟内存的基础概念就聊到这里。物理地址是硬件真实地址,虚拟地址是进程看到的假象,MMU负责翻译,页表是翻译的依据。这套机制,是操作系统的基石之一。