961 字
3 分钟
为什么 Linux 需要虚拟内存
「你的服务器内存是 16GB,但跑的服务占用了 20GB,为什么没有崩溃?」
答案就是 虚拟内存(Virtual Memory)。
一、物理内存的困境
1.1 直接访问物理内存的问题
早期系统直接访问物理内存:
flowchart LR
APP1[应用 1] --> MEM[物理内存]
APP2[应用 2] --> MEM
APP3[应用 3] --> MEM
APP1 -->|"物理地址 0x1000"| M1[数据]
APP2 -->|"物理地址 0x1000"| M1
问题:
- 地址冲突:两个程序都用 0x1000,但指向不同数据
- 内存碎片:程序释放后留下不连续空洞
- 安全问题:程序可以直接访问其他程序的数据
- 缺乏保护:一个程序崩溃可能影响整个系统
1.2 内存碎片化
flowchart TB
subgraph 物理内存
A1[进程 A<br/>2GB] -->|"释放"| A2[空 2GB]
B1[进程 B<br/>1GB] -->|"释放"| B2[空 1GB]
C1[进程 C<br/>3GB] -->|"运行中"| C2[进程 C<br/>3GB]
end
Note over A1,C2: 物理地址不连续
物理地址碎片化导致:
- 无法为需要连续 4GB 的进程分配内存
- 即使总空闲内存 > 4GB
二、虚拟内存的核心思想
2.1 虚拟地址空间
每个进程拥有独立的虚拟地址空间:
flowchart LR
subgraph 进程 A 虚拟空间
VA_A1[0x0000 - 0xFFFF]
end
subgraph 进程 B 虚拟空间
VA_B1[0x0000 - 0xFFFF]
end
VA_A1 -->|"MMU 转换"| PA1[物理地址]
VA_B1 -->|"MMU 转换"| PA2[物理地址]
PA1 --> MEM[物理内存]
PA2 --> MEM
style MMU fill:#f9f
关键洞察:程序使用的地址是虚拟地址,实际访问的是物理地址。
2.2 虚拟内存解决的问题
| 问题 | 虚拟内存解决方案 |
|---|---|
| 地址冲突 | 每个进程独立的虚拟地址空间 |
| 内存碎片 | 虚拟地址连续,物理地址可不连续 |
| 安全问题 | 进程间虚拟地址隔离 |
| 内存不足 | 磁盘作为扩展(swap) |
| 程序重定位 | 程序可加载到任意虚拟地址 |
三、虚拟地址到物理地址的转换
3.1 MMU 工作原理
Memory Management Unit(内存管理单元)负责地址转换:
flowchart LR
CPU["CPU<br/>发出虚拟地址"] --> MMU["MMU"]
MMU -->|"查找页表"| PTE["页表项<br/>物理页帧号 + 权限"]
PTE -->|"成功"| PHYS["物理地址"]
PTE -->|"失败(缺页)"| PF["缺页异常"]
style MMU fill:#f9f
地址转换公式:
虚拟地址 (VA) = 页号 (VPN) + 页内偏移 (Offset)物理地址 (PA) = 物理页号 (PFN) + 页内偏移 (Offset)3.2 分页机制
Linux 默认页大小 4KB:
虚拟地址:0x0040 1234 │ │ │ └─── Offset (12 位,4KB 页内偏移) └──────── VPN (页号)
虚拟页号 (VPN) ──→ 查页表 ──→ 物理页号 (PFN)物理地址 = PFN + Offset3.3 页表结构
x86-64 架构的页表(4 级):
flowchart TB
VA["虚拟地址 (64 位)"]
VA --> PML4["PML4<br/>(9 位)"]
PML4 --> PDPT["PDPT<br/>(9 位)"]
PDPT --> PD["PD<br/>(9 位)"]
PD --> PT["PT<br/>(9 位)"]
PT --> PFN["物理页帧号<br/>(PFN)"]
PFN --> PA["物理地址"]
VA --> OFF["Offset (12 位)"]
OFF --> PA
style PML4 fill:#f96
style PDPT fill:#9f9
style PD fill:#9f9
style PT fill:#9f9
每级页表项(PTE)包含:
- 物理页帧号(PFN)
- 访问权限(R/W/X)
- 存在位(Present)
- 脏位(Dirty)
四、为什么需要虚拟内存
4.1 内存保护
flowchart LR
subgraph 进程 A
VA_A[虚拟地址] --> MMU_A[MMU]
end
subgraph 进程 B
VA_B[虚拟地址] --> MMU_B[MMU]
end
MMU_A -->|"检查权限"| OK1[允许访问]
MMU_B -->|"检查权限"| OK2[允许访问]
MMU_A -.->|"越界访问"| FAULT[缺页异常<br/>SIGSEGV]
权限控制:
- 用户态代码不能访问内核地址
- 代码段只读,数据段可写
- 栈区域不可执行(NX bit)
4.2 内存交换(Swap)
物理内存不足时,将不活跃页面换出到磁盘:
# 查看 swap 使用$ free -h total used free shared buff/cache availableMem: 125Gi 45Gi 32Gi 2Gi 28Gi 78GiSwap: 20Gi 12Gi 8Giflowchart TB
subgraph 物理内存
ACTIVE["活跃页面<br/>进程 A, B, C"]
INACTIVE["不活跃页面<br/>可换出"]
end
INACTIVE -->|"swap out"| DISK["磁盘 Swap 区"]
DISK -->|"swap in"| INACTIVE
Note over ACTIVE,DISK: Swap 机制让内存"看起来"更大
4.3 内存分配效率
// 程序申请的虚拟内存void *ptr = malloc(1GB); // 立即返回,不占用物理内存
// 实际使用时(缺页异常分配)for (i = 0; i < 1GB / 4096; i++) { ptr[i * 4096] = 1; // 触发缺页,分配物理页}按需分配(Demand Paging):
- 分配时只占用虚拟地址
- 首次访问时才分配物理页
- 避免预先占用大块物理内存
五、虚拟内存的高级特性
5.1 内存映射(mmap)
将文件映射到虚拟地址空间:
// mmap 示例int fd = open("data.txt", O_RDONLY);void *addr = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0);
// 读取文件像读内存一样char c = *(char *)addr;flowchart TB
FILE["文件"] <-->|"页缓存"| VM["虚拟地址空间"]
subgraph VM
MAPPED["映射区域"]
OTHER["其他区域"]
end
5.2 Copy-on-Write(写时复制)
fork() 后父子进程共享物理页,只读:
pid_t pid = fork();
if (pid == 0) { // 子进程修改数据 data = 100; // 触发 COW,分配新物理页}sequenceDiagram
participant P as 父进程
participant C as 子进程
participant M as 物理内存
Note over P,C: fork() 后
P->>M: 读取共享页 (R)
C->>M: 读取共享页 (R)
Note over P,C: 子进程写入时
C->>M: 写入共享页
M->>M: COW: 复制新页
M->>C: 返回新页
5.3 大页面(HugePages)
减少页表层级,降低 TLB miss:
# 查看大页面配置$ cat /proc/meminfo | grep -i hugeAnonHugePages: 0 kBShmemHugePages: 0 kBHugePages_Total: 10HugePages_Free: 10Hugepagesize: 2048 kB| 页大小 | 页表深度 | TLB 条目覆盖 |
|---|---|---|
| 4KB | 4 级 | 4KB |
| 2MB | 3 级 | 2MB |
| 1GB | 2 级 | 1GB |
六、缺页异常处理
6.1 缺页异常流程
flowchart TB
CPU["CPU 访问虚拟地址"] --> MMU["MMU 查页表"]
MMU -->|"Present=0"| PF["缺页异常"]
PF --> KERNEL["内核处理缺页"]
KERNEL -->|"页面不在内存"| DISK["从磁盘加载"]
KERNEL -->|"页面未分配"| ALLOC["分配物理页"]
DISK --> UPDATE["更新页表"]
ALLOC --> UPDATE
UPDATE --> RESUME["恢复执行"]
style PF fill:#f66
style KERNEL fill:#ff9
6.2 页面置换算法
当物理内存不足时,需要置换页面:
| 算法 | 说明 | 特点 |
|---|---|---|
| LRU | 最近最少使用 | 开销大,近似实现 |
| CLOCK | 二次机会 | 折中方案 |
| 工作集 | 基于活跃页面 | 更准确 |
# 查看页面置换统计$ vmstat 1procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 1 0 0 32768 4096 28672 0 0 0 0 0 0 5 2 93 0 0七、虚拟内存的代价
7.1 性能开销
虚拟内存的好处: 简化的编程模型 内存保护 超出物理内存运行
虚拟内存的代价: 地址转换开销(MMU + TLB) 缺页异常处理 页面置换(swap)代价极高7.2 TLB 优化
TLB(Translation Lookaside Buffer)缓存最近使用的地址转换:
# 查看 TLB 信息$ getconf PAGE_SIZE4096
# TLB miss 开销 ~30-100 周期# TLB hit 开销 ~1 周期八、总结
虚拟内存是操作系统最伟大的设计之一:
| 特性 | 解决的问题 |
|---|---|
| 地址空间隔离 | 安全、稳定性 |
| 按需分配 | 内存效率 |
| 内存保护 | 进程间隔离 |
| Swap 扩展 | 超物理内存运行 |
| mmap | 高效文件访问 |
| COW | 高效进程创建 |
理解虚拟内存,对于:
- 性能调优(swap 使用分析)
- 内存泄漏排查
- 程序行为理解
都至关重要。
参考资料
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时
相关文章 智能推荐
1
为什么 Linux 需要 Swapping
技术科普 深入解析 Linux Swapping 机制,为什么需要将内存交换到磁盘,以及 swappiness 的作用。
2
为什么 Linux 默认页大小是 4KB
技术科普 深入解析 Linux 默认选择 4KB 页大小的历史原因和技术权衡。
3
为什么 Linux 和 macOS 不需要碎片整理
技术科普 深入解析为什么 Linux 和 macOS 的文件系统不需要碎片整理,与 Windows NTFS 的对比。
4
为什么 HugePages 可以提升数据库性能
技术科普 深入解析 HugePages 如何提升数据库性能,TLB 缓存的工作原理。
5
虚拟内存
操作系统 深入探讨虚拟内存的工作机制——虚拟地址空间布局、MMU 地址转换流程、多级页表结构与缺页异常处理。






