虚拟内存¶
是什么 / 解决什么问题¶
Linux 为每个进程提供独立的虚拟地址空间。虚拟内存让每个进程"以为"自己拥有全部地址空间,实际物理内存由内核按需分配和管理。理解虚拟内存是理解 mmap、malloc、OOM 等行为的基础。
地址空间布局(64 位)¶
高地址
┌──────────────────┐ 0x7FFF_FFFF_FFFF (用户态上限,128TB)
│ Stack ↓ │ 线程栈(向下增长)
│ │
├──────────────────┤
│ │
│ mmap 区域 ↓ │ 动态库、mmap 文件、匿名映射
│ │
├──────────────────┤
│ │
│ Heap ↑ │ brk/sbrk 管理(向上增长)
│ │
├──────────────────┤
│ BSS │ 未初始化全局变量(零初始化)
├──────────────────┤
│ Data │ 已初始化全局变量
├──────────────────┤
│ Text (代码) │ 只读 + 可执行
├──────────────────┤
低地址 0x0000_0000_0000(NULL 区域不可访问)
# 查看具体布局
cat /proc/<pid>/maps
# 输出格式:起始-结束 权限 偏移 设备 inode 路径
# 7f8a3c000000-7f8a3c021000 rw-p 00000000 00:00 0
# 7f8a3c021000-7f8a40000000 ---p 00000000 00:00 0
行为与语义¶
缺页中断 (Page Fault)¶
虚拟地址 → 查页表 → 物理页存在?
│
┌────────┴────────┐
是 否
│ │
正常访问 缺页中断 (Page Fault)
│
┌─────────┼─────────┐
次缺页 主缺页 非法访问
(Minor) (Major) → SIGSEGV
│ │
页在内存 需要磁盘I/O
(如 COW) (如 mmap 文件)
│ │
分配/映射 读磁盘→分配页
│ │
~1μs ~几ms
内存 overcommit¶
Linux 默认允许 overcommit:
malloc/mmap 时不检查是否有足够物理内存
→ 申请成功不意味着有物理内存
→ 首次写入时才分配物理页
→ 如果此时物理内存不足 → OOM Killer
# overcommit 策略
cat /proc/sys/vm/overcommit_memory
# 0: 启发式(默认,允许合理范围的超额)
# 1: 总是允许(不检查)
# 2: 严格不超额(commit ≤ swap + RAM * ratio)
cat /proc/sys/vm/overcommit_ratio # 默认 50(RAM 的 50%)
页面大小¶
long page_size = sysconf(_SC_PAGESIZE); // 通常 4096 (4KB)
// 大页 (Huge Pages)
// 标准大页: 2MB (x86_64)
// 超大页: 1GB
// 优势: 减少页表条目和 TLB miss
// 劣势: 内存碎片、分配粒度大
VMA (Virtual Memory Area)¶
使用模式¶
mprotect — 修改内存权限¶
#include <sys/mman.h>
// 分配可读写内存
void *mem = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// 改为只读
mprotect(mem, 4096, PROT_READ);
// 写入会 SIGSEGV
// 改为可执行(JIT 编译器使用)
mprotect(mem, 4096, PROT_READ | PROT_EXEC);
// 完全不可访问(guard page)
mprotect(mem, 4096, PROT_NONE);
mlock — 锁定物理内存¶
// 防止页面被换出到 swap
mlock(addr, length);
// 锁定进程全部内存
mlockall(MCL_CURRENT | MCL_FUTURE);
// 解锁
munlock(addr, length);
// 用途: 实时系统(避免 swap 导致延迟抖动)
// 安全(密钥不被换出到磁盘)
mincore — 查询页面是否在内存¶
unsigned char vec[page_count];
mincore(addr, length, vec);
// vec[i] & 1: 第 i 页在物理内存中
// vec[i] == 0: 第 i 页不在内存(访问会触发缺页)
性能考量¶
TLB (Translation Lookaside Buffer)¶
虚拟地址→物理地址的转换需要查页表(最多 4 级)
TLB 缓存最近的转换结果
TLB miss 代价:
- 需要遍历页表(硬件 page walk)
- 几十到几百纳秒
减少 TLB miss:
1. 使用大页(2MB 页覆盖 512x 更多内存)
2. 保持数据局部性(紧凑的数据结构)
3. 减少 mmap/munmap 频率
大页使用¶
// 方法1: 透明大页 (THP)
madvise(addr, length, MADV_HUGEPAGE);
// 方法2: 显式 hugetlbfs
void *p = mmap(NULL, 2*1024*1024, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);
// 方法3: mmap flag
void *p = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGE_2MB, -1, 0);
减少 page fault¶
// 预先 fault 所有页面(避免运行时 page fault 延迟)
madvise(addr, length, MADV_POPULATE_READ); // 5.14+
madvise(addr, length, MADV_POPULATE_WRITE); // 5.14+
// 老方法:手动遍历每页
for (size_t i = 0; i < length; i += page_size) {
volatile char x = ((char *)addr)[i]; // 触发 page fault
}
常见陷阱与 FAQ¶
1. 虚拟内存 ≠ 物理内存¶
# 进程的虚拟内存和物理内存使用
cat /proc/<pid>/status
# VmSize: 虚拟内存总量(包含未分配物理页的部分)
# VmRSS: 实际物理内存(Resident Set Size)
# VmSwap: 交换到磁盘的部分
# 常见误解:VmSize 很大 ≠ 内存泄漏
# 64位系统虚拟空间几乎无限,关键看 RSS
2. 栈溢出¶
默认栈大小 8MB,超出触发 SIGSEGV
guard page 机制:栈下方有一个不可访问页
如果一次性跳过 guard page(如大数组在栈上分配)→ 可能不触发保护
ulimit -s unlimited # 设置无限栈大小(不推荐)
3. 地址空间碎片¶
频繁 mmap/munmap 可能导致虚拟地址空间碎片,使得大块连续映射无法分配。
观测与调试¶
# 详细内存映射
pmap -x <pid>
# Address Kbytes RSS Dirty Mode Mapping
# 总计显示每个 VMA 的物理内存占用
# 查看 page fault 统计
ps -o pid,min_flt,maj_flt -p <pid>
# min_flt: 次缺页(page cache 命中或 COW)
# maj_flt: 主缺页(磁盘 I/O)
# perf 统计
perf stat -e page-faults,dTLB-load-misses,dTLB-store-misses ./app
# 查看内存碎片
cat /proc/buddyinfo
# 查看系统内存总览
cat /proc/meminfo
延伸阅读¶
man 2 mmap/man 2 mprotect/man 2 mlockman 5 proc— /proc/[pid]/maps, status, smaps/proc/sys/vm/*— 虚拟内存相关参数