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——如果命中,直接拿到物理地址。如果没命中,就去内存查页表,拿到映射关系后,再访问物理内存。整个过程由MMU(内存管理单元)硬件自动完成,操作系统只负责维护页表。
核心总结:虚拟内存让每个进程有独立的地址空间;分页机制把内存切成小块,灵活映射;页表记录映射关系,多级结构节省内存;TLB加速地址转换,是性能的关键。这四个概念环环相扣,缺一不可。
好了,这一章就到这里。下一章我们会深入页表的具体实现,看看Linux内核里到底是怎么管理这些映射的——到时候我会拿一个实际的内核模块代码来拆解。
公众号:蓝海资料掘金营,微信deep3321