mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
961 字
3 分钟
为什么 Linux 需要虚拟内存
2023-07-22

「你的服务器内存是 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 + Offset

3.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 available
Mem: 125Gi 45Gi 32Gi 2Gi 28Gi 78Gi
Swap: 20Gi 12Gi 8Gi
flowchart 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 huge
AnonHugePages: 0 kB
ShmemHugePages: 0 kB
HugePages_Total: 10
HugePages_Free: 10
Hugepagesize: 2048 kB
页大小页表深度TLB 条目覆盖
4KB4 级4KB
2MB3 级2MB
1GB2 级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 1
procs -----------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_SIZE
4096
# TLB miss 开销 ~30-100 周期
# TLB hit 开销 ~1 周期

八、总结#

虚拟内存是操作系统最伟大的设计之一:

特性解决的问题
地址空间隔离安全、稳定性
按需分配内存效率
内存保护进程间隔离
Swap 扩展超物理内存运行
mmap高效文件访问
COW高效进程创建

理解虚拟内存,对于:

  • 性能调优(swap 使用分析)
  • 内存泄漏排查
  • 程序行为理解

都至关重要。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

为什么 Linux 需要虚拟内存
https://blog.souloss.com/posts/why-the-design/why-linux-needs-virtual-memory/
作者
Souloss
发布于
2023-07-22
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时