内存地址空间(逻辑 vs 物理)
每个程序都以为自己独占全部内存——这是操作系统的"障眼法",通过逻辑地址(虚拟地址)和物理地址的分离,让每个程序拥有独立的地址空间
每个程序都”以为”自己独占内存
打开三个记事本窗口,每个都在编辑不同的文件。在程序看来,每个记事本的地址都是从 0x00000000 到 0xFFFFFFFF(32 位)——它们都用了相同的地址,但互不干扰。
这是怎么做到的?
🏫 类比:酒店的房卡 每个住客拿到的房卡上都写着”301”——但酒店有 10 层楼,每层都有 301 房间吗?不是的。前台(OS)把每个客人分配到了不同的实际房间,但给每个客人的房卡上都写”301”。
- 逻辑地址 = 房卡上写的”301”(每个客人看到的一样)
- 物理地址 = 真正的房间号 1208(只有前台知道)
逻辑地址 vs 物理地址
程序看到的世界(逻辑地址空间) 真实的世界(物理地址空间)
┌────────────────────┐ ┌────────────────────┐
│ 0xFFFFFFFF │ │ 物理内存 │
│ (高地址) │ │ ┌──────────────────┐│
│ │ │ │ 其他进程/内核 ││
│ │ │ ├──────────────────┤│
│ 程序以为自己独占 │ 映射 │ │ 进程 A 的代码 ││
│ 整个 4GB 空间 │ ────────→ │ ├──────────────────┤│
│ │ │ │ 进程 B 的数据 ││
│ ┌──────────────────┐│ MMU 翻译 │ ├──────────────────┤│
│ │ 代码段 ││ │ │ 空闲内存 ││
│ │ 数据段 ││ │ ├──────────────────┤│
│ │ 堆 ↓ ││ │ │ 进程 B 的代码 ││
│ │ ... ││ │ └──────────────────┘│
│ │ ↑ 栈 ││ └────────────────────┘
│ └──────────────────┘│
│ 0x00000000(低地址) │
└────────────────────┘
| 概念 | 含义 | 谁看见的 |
|---|---|---|
| 逻辑地址(虚拟地址) | 程序指令中使用的地址 | CPU(执行指令时) |
| 线性地址 | x86 中经过段式转换后的地址 | MMU |
| 物理地址 | 内存芯片上的真实地址 | 内存控制器 |
| 虚拟地址空间 | 进程看到的假想内存空间 | 进程 |
地址翻译——MMU 的魔法
CPU 执行 mov rax, [0x12345678] 时,地址 0x12345678 不是直接发给内存的——它先经过 MMU(Memory Management Unit) 翻译:
CPU MMU 物理内存
┌────┐ ┌──────────┐ ┌──────────┐
│ │ │ │ │ │
│ 执 │ 虚拟地址 │ 查页表 │ 物理地址 │ 读/写数据 │
│ 行 │────→│ 0x1234 │───→│ 0x9ABC │─────→ │
│ 指 │ │ ↓↓ │ │ │ │
│ 令 │ │ MMU │ │ │ │
└────┘ └──────────┘ └──────────┘
地址翻译的层次
CPU 发出虚拟地址 VA
│
▼
① 段式转换(x86 特有的历史包袱)
VA → 线性地址(Linear Address)
│
▼
② 页式转换(所有现代 OS 都用的方式)
线性地址 → 拆分为 页号 + 偏移量
查页表 → 找到对应的物理页框号
│
▼
③ 物理地址 = 物理页框号 × 页大小 + 偏移量
💡 现代 x86-64 Linux 直接绕过了段式转换(“平坦模型”),只使用页式转换。
为什么需要虚拟地址空间?
理由 1:隔离——程序之间不捣乱
没有虚拟内存: 有虚拟内存:
程序 A 写地址 0x1000 程序 A 写虚拟地址 0x1000
↓ ↓
直接写到物理地址 0x1000 MMU 映射到物理页框 X
↓ ↓
如果程序 B 也在用 0x1000→崩 程序 B 的 0x1000 映射到 Y
互不影响 ✅
理由 2:简化编程——不用管物理内存布局
程序编译时,代码、数据、堆、栈各放什么地址——编译器可以假设从 0 开始。不用关心物理内存的碎片和空闲区域。
理由 3:共享与保护
多个进程可以共享同一份物理内存(如动态库的代码),但每个进程的页表映射到同一物理页——只读执行,不能修改。
进程 A 的虚拟地址 物理内存 进程 B 的虚拟地址
0x7f00 (只读) ┌──────────┐ 0x7f00 (只读)
───────────────→ │ libc.so │ ←──────────────
│ 代码段 │
└──────────┘
理由 4:使用超过物理内存的大小
虚拟地址空间可以比物理内存大——通过虚拟内存,程序可以”以为”自己有 4TB 空间,但实际物理内存只有 8GB。
进程的地址空间布局
典型的 64 位 Linux 进程地址空间:
0x0000000000000000
┌──────────────────────┐
│ 代码段(Text) │ ← 只读,程序的机器指令
├──────────────────────┤
│ 数据段(Data) │ ← 初始化了的全局变量
├──────────────────────┤
│ BSS 段 │ ← 未初始化的全局变量(不占文件空间)
├──────────────────────┤
│ 堆(Heap) │ ← malloc / new
│ ↓ │
│ │
│ ↑ │
│ 栈(Stack) │ ← 局部变量、函数调用
├──────────────────────┤
│ 动态链接库 │ ← libc.so 等
├──────────────────────┤
│ 内核空间(只对内核可见)│ ← 系统调用入口、内核代码
0x7FFFFFFFFFFFFFFF (用户空间顶部)
💡 在 64 位 Linux 上,用户空间一般是 47 位(约 128TB),内核空间也是 47 位。这个分割点在地址空间的中间,而不是 32 位的 3:1 分割。
实际运用:malloc 背后的地址空间变化
// 一个简单的 C 程序
#include <stdio.h>
#include <stdlib.h>
int global_var = 42; // 数据段
int uninit_var; // BSS 段
int main() {
int local = 10; // 栈
int* heap = malloc(100); // 堆
printf("代码段 (main): %p\n", main);
printf("数据段 (global): %p\n", &global_var);
printf("BSS (uninit): %p\n", &uninit_var);
printf("堆 (malloc): %p\n", heap);
printf("栈 (local): %p\n", &local);
free(heap);
return 0;
}
输出示例(每次运行地址可能不同,但相对位置固定):
代码段 (main): 0x4004b6 ← 低地址
数据段 (global): 0x400900 ← 代码段上方
BSS (uninit): 0x400910 ← 数据段上方
堆 (malloc): 0x602010 ← BSS 上方
栈 (local): 0x7fff1234 ← 高地址
关键观察:代码段在最低地址,栈在最高地址,堆在中间向上增长——和你刚学的进程地址空间布局完全吻合。每个程序都从 0x400000(Linux 默认加载地址)开始,认为自己独占整个地址空间。
查看进程地址空间
# 查看进程的内存映射
$ cat /proc/$$/maps
00400000-00452000 r-xp 00000000 08:01 12345 /bin/bash
00651000-00652000 r--p 00051000 08:01 12345 /bin/bash
00652000-00653000 rw-p 00052000 08:01 12345 /bin/bash
01c83000-01ca4000 rw-p 00000000 00:00 0 [heap]
7f5c5d8e0000-7f5c5dac0000 r-xp 00000000 08:01 67890 /lib/x86_64-linux-gnu/libc.so.6
...
7ffc2b5b1000-7ffc2b5d2000 rw-p 00000000 00:00 0 [stack]
7ffc2b5de000-7ffc2b5e1000 r--p 00000000 00:00 0 [vvar]
7ffc2b5e1000-7ffc2b5e3000 r-xp 00000000 00:00 0 [vdso]
每行列出了:
- 虚拟地址范围
- 权限(r=读, w=写, x=执行, p/s=私有/共享)
- 偏移、设备号、inode
- 映射的文件名
小结
| 概念 | 要点 |
|---|---|
| 逻辑地址 | 程序看到的地址,每个进程独立 |
| 物理地址 | 内存芯片上的真实地址 |
| MMU | 地址翻译的硬件单元 |
| 页表 | 虚拟页 → 物理页的映射关系 |
| 隔离性 | 每个进程有独立地址空间 |
| 共享 | 通过页表映射到同一物理页实现 |
为什么先学这个? 理解了地址空间的分裂,才能理解分页和虚拟内存。下一节看看分页与页表——地址翻译的具体实现。