2、操作系统内存管理基础:虚拟内存与物理内存、分页与分段机制、页表与TLB、缺页中断与页面置换算法
好,咱们正式开始聊内存管理。说实话,这是整个内存优化的地基。你想想看,如果连操作系统怎么管内存都不清楚,后面那些优化手段基本就是空中楼阁。
我个人习惯把内存管理比作一个「大仓库」。物理内存就是仓库的实际货架,而虚拟内存呢,是给每个程序发的一张「仓库地图」。程序以为自己独占整个仓库,其实它看到的只是地图上的位置——这就是虚拟内存的核心思想。
2.1 虚拟内存与物理内存
先问个问题:为什么要有虚拟内存?
直接原因很简单——物理内存不够用。我早年做嵌入式开发时,遇到过一台设备只有 256MB 内存,却要同时跑三个应用。每个应用都觉得自己有 1GB 空间,这怎么做到的?
答案就是虚拟内存。
虚拟内存给每个进程提供了一个独立的、连续的地址空间。进程访问的是虚拟地址,操作系统和硬件再把这个地址翻译成真实的物理地址。说白了,就是「骗」进程说你有大房子,实际上你住的是隔断间。
举个例子:
进程A认为地址 0x1000 是它的变量
实际上 0x1000 被映射到了物理内存的 0x7000
进程B也认为地址 0x1000 是它的变量
实际上 0x1000 被映射到了物理内存的 0x8000
两个进程用同一个虚拟地址,互不干扰。这就是隔离性。
2.2 分页与分段机制
虚拟地址怎么映射到物理地址?主要有两种方式:分段和分页。
分段,就是把程序分成代码段、数据段、堆栈段等。每个段有独立的基地址和长度限制。我记得早期 x86 架构就是分段为主,写个程序还得操心段寄存器,挺麻烦的。
分页,才是现代操作系统的标配。它把虚拟内存和物理内存都切成固定大小的块——叫「页」。通常是 4KB 一页。
我做个对比你就明白了:
| 特性 | 分段 | 分页 |
|---|---|---|
| 单位大小 | 可变(段大小由程序决定) | 固定(通常 4KB) |
| 碎片问题 | 外部碎片严重 | 内部碎片可控 |
| 管理复杂度 | 较低 | 较高 |
| 现代系统 | 基本淘汰 | 主流方案 |
为什么分页成了主流?说白了,分段那个可变大小太坑了。程序运行久了,内存里全是小洞,新的大段塞不进去——这就是外部碎片。分页固定大小,碎片最多浪费一页,好处理得多。
2.3 页表与 TLB
分页机制的核心就是页表。页表记录了每个虚拟页号对应的物理页框号。
每次 CPU 访问内存,都要先查页表。但问题来了——页表存在内存里,查一次页表就要访问一次内存。这意味着每次内存访问,实际上变成了两次内存访问。性能直接腰斩。
怎么办?加缓存。
这就是 TLB(Translation Lookaside Buffer)——页表的高速缓存。它把最近用过的页表项存到 CPU 芯片里,访问速度比内存快两个数量级。
CPU访问虚拟地址 → 先查TLB
├─ TLB命中 → 直接得到物理地址 → 访问内存
└─ TLB未命中 → 查内存中的页表 → 更新TLB → 访问内存
我在项目中遇到过一个问题:某个数据处理程序,性能突然下降 30%。查了半天,发现是数据访问模式太分散,TLB 命中率从 99% 掉到了 70%。后来把数据重新排列,让访问更集中,性能就恢复了。
2.4 缺页中断
进程访问一个虚拟地址,页表里找不到对应的物理页——这就触发了缺页中断。
缺页中断的处理流程大概是这样的:
- CPU 触发缺页异常,操作系统接管
- 检查虚拟地址是否合法(是不是非法访问?)
- 如果合法,从磁盘上把数据加载到物理内存
- 更新页表,建立映射关系
- 重新执行触发缺页的那条指令
嗯,这里要注意:缺页中断是「同步」的。进程在等磁盘 I/O 的时候,会被挂起。如果频繁缺页,性能会惨不忍睹。
我曾经调过一个 Java 应用,堆内存设得太大,物理内存装不下,结果频繁缺页。磁盘 I/O 成了瓶颈,CPU 大部分时间在等磁盘。这就是典型的「颠簸」(Thrashing)。
2.5 页面置换算法
物理内存满了,又要加载新页——必须把某个旧页踢出去。踢谁?这就是页面置换算法的事。
常见的算法有这些:
| 算法 | 思路 | 优缺点 |
|---|---|---|
| FIFO | 先进先出 | 实现简单,但可能踢掉常用页 |
| LRU | 最近最少使用 | 效果好,但实现成本高 |
| Clock | 近似 LRU,用标志位 | 折中方案,Linux 在用 |
| LFU | 最不经常使用 | 适合长期模式,不适应突发 |
我个人最推荐理解 LRU。虽然实际系统很少用纯 LRU(太费资源),但它的思想是所有置换算法的基准。
LRU 的核心假设:如果一页最近被访问过,那么它很可能很快还会被访问。反过来,很久没被访问的页,以后被访问的概率也低。
Linux 实际用的是 Clock 算法的变种。它给每个页一个「访问位」,定期扫描。如果访问位是 1,就清零并跳过;如果是 0,就置换出去。说白了,就是给每个页一次「免死金牌」。
好了,这一章的内容就到这。虚拟内存、分页、页表、TLB、缺页中断、置换算法——这些概念环环相扣。你理解透了,后面讲内存分配、内存池、大页优化,才能真的听懂。
下一章,我们聊聊进程的内存布局和堆栈管理。到时候我会分享一个我踩过的坑——栈溢出导致的神秘崩溃。