3. 内存管理基础:虚拟内存、分页机制、页表结构、TLB(快表)的作用

内存管理,说白了就是操作系统怎么把有限的物理内存,安排得明明白白。你写程序的时候,根本不用管物理内存还剩多少、碎片多不多,对吧?这就是虚拟内存的功劳。我做了这么多年内核安全,见过太多因为内存管理出岔子导致的漏洞——嗯,这块儿要是搞不明白,后面讲漏洞利用你会听得一头雾水。

3.1 虚拟内存:给每个进程一个“假”的地址空间

虚拟内存的核心思想很简单:让每个进程都觉得自己独占整个地址空间。32位系统下,每个进程看到的是4GB的连续空间;64位下就更大了,理论上16EB。但实际上,物理内存可能只有8GB、16GB。

为什么会这样?说白了就是“骗”进程。进程访问地址0x8048000,实际上物理内存里可能对应的是0x3F000000。这个映射关系,由操作系统和硬件共同维护。

关键点:虚拟地址 ≠ 物理地址。这个“不等于”是所有内存安全机制的基础。没有虚拟内存,进程A就能直接读到进程B的密码——那还谈什么安全?

我个人习惯把虚拟内存比作“酒店房卡”。你拿着房卡(虚拟地址)去开门,前台(MMU)帮你找到真正的房间号(物理地址)。你根本不需要知道房间在几楼几号,前台知道就行。

3.2 分页机制:把内存切成小方块

虚拟内存怎么映射到物理内存?靠分页。操作系统把虚拟地址空间切成固定大小的“页”(Page),物理内存也切成同样大小的“页框”(Page Frame)。默认大小通常是4KB。

你想想看,如果不分页,整个地址空间作为一个整体来映射,那得多浪费?进程可能只用了几MB,却要占一整块连续物理内存。分页之后,虚拟页可以散落在物理内存的任意位置,甚至被换到磁盘上。

概念 说明 大小(典型)
虚拟页(Page) 虚拟地址空间中的一块 4KB
物理页框(Page Frame) 物理内存中的一块 4KB
页表项(PTE) 记录虚拟页到物理页框的映射 4字节或8字节

避坑指南:我曾经调试过一个内核模块,发现内存访问偶尔会崩溃。查了两天才发现,是用了非标准页大小(64KB),但页表配置没跟上。嗯,这里要注意——改页大小一定要同步更新页表结构。

3.3 页表结构:多级映射的艺术

页表就是一张映射表,记录每个虚拟页对应哪个物理页框。但问题来了:32位系统下,虚拟地址空间有2^20个页(4GB / 4KB),每个页表项4字节,光一级页表就要4MB。每个进程一个页表,100个进程就是400MB——这谁受得了?

所以现代CPU都用多级页表。以x86的32位经典两级页表为例:

  • 页目录(Page Directory):1024个条目,每个指向一个页表
  • 页表(Page Table):1024个条目,每个指向一个物理页框

虚拟地址被拆成三部分:页目录索引(10位)、页表索引(10位)、页内偏移(12位)。

// 虚拟地址分解示例(32位,两级页表)
// 地址:0x8048000
// 二进制:0000 1000 0000 0100 1000 0000 0000 0000
// 页目录索引:0000 1000 00 = 0x20 (32)
// 页表索引:00 0100 1000 = 0x48 (72)
// 页内偏移:0000 0000 0000 = 0x000 (0)

64位系统就更复杂了,通常是4级甚至5级页表。为什么?因为地址空间太大了,一级页表根本装不下。多级页表的好处是:如果某个页目录项是空的,对应的整个页表都不用分配——省内存。

安全警告:页表本身是内核的关键数据结构。如果攻击者能篡改页表项,就能实现任意物理内存读写——这就是“页表攻击”的原理。我见过一个案例,攻击者通过内核漏洞修改了页表项,把只读页面改成了可写,然后注入了恶意代码。

3.4 TLB(快表):加速地址转换的缓存

每次访问内存都要查页表,而页表本身也在内存里。这意味着一次内存访问,实际上要访问两次内存(先查页表,再读数据)——性能直接腰斩。

TLB(Translation Lookaside Buffer)就是来解决这个问题的。它是CPU内部的一个硬件缓存,专门缓存最近用过的虚拟地址到物理地址的映射。

我个人习惯把TLB比作“前台的小本本”。前台(MMU)第一次查房间号(物理地址)时,会记在小本本上。下次同一个客人来,直接翻小本本,不用再查大表了。

特性 TLB 页表(内存中)
访问速度 1-2个CPU周期 几十到上百个周期
容量 几十到几百个条目 数百万个条目
位置 CPU内部 内存中
管理方式 硬件自动管理 操作系统管理

TLB命中率通常能达到99%以上。但一旦没命中(TLB Miss),就得去内存查页表,性能会下降不少。所以内核开发者会特别注意“TLB友好”的代码——比如尽量让数据在页内对齐,减少跨页访问。

实战经验:我在优化一个网络驱动时,发现吞吐量上不去。用性能工具一查,TLB Miss率高达15%。后来把数据缓冲区按4KB对齐,并改用大页(2MB),TLB Miss率降到了1%以下。嗯,这里要注意——大页虽然能减少TLB Miss,但会浪费内存,得权衡着用。

3.5 知识体系总览

下面这张图把本章的核心逻辑串起来了。你从左上角开始看,顺着箭头走一遍,就能理解整个内存管理的数据流。

内存管理核心流程 虚拟地址 进程看到的地址空间 TLB(快表) 硬件缓存,加速映射 命中 未命中 页表(多级结构) 虚拟页 → 物理页框 物理地址 真正的内存位置 分页机制 虚拟地址 → 页目录索引 + 页表索引 + 页内偏移 页大小:4KB(默认) | 大页:2MB / 1GB ⚠ 安全关注点 页表篡改 → 任意物理内存读写 | TLB刷新不当 → 信息泄露

这张图里,你从左上角的“虚拟地址”出发。先查TLB——如果命中,直接拿到物理地址。如果没命中,就去内存查页表,拿到映射关系后,再访问物理内存。整个过程由MMU(内存管理单元)硬件自动完成,操作系统只负责维护页表。

核心总结:虚拟内存让每个进程有独立的地址空间;分页机制把内存切成小块,灵活映射;页表记录映射关系,多级结构节省内存;TLB加速地址转换,是性能的关键。这四个概念环环相扣,缺一不可。

好了,这一章就到这里。下一章我们会深入页表的具体实现,看看Linux内核里到底是怎么管理这些映射的——到时候我会拿一个实际的内核模块代码来拆解。


公众号:蓝海资料掘金营,微信deep3321