跳转至

虚拟内存

是什么 / 解决什么问题

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)

每个连续的虚拟地址范围是一个 VMA:
  - 权限(r/w/x)
  - 映射类型(匿名/文件)
  - 共享模式(private/shared)

合并规则:相邻 VMA 如果属性相同会被合并
# 查看 VMA 数量
cat /proc/<pid>/status | grep VmPTE
wc -l /proc/<pid>/maps

使用模式

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);
# 查看 THP 使用情况
cat /proc/meminfo | grep -i huge
# AnonHugePages: 使用中的匿名大页
# HugePages_Total: 预留的大页数

减少 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 mlock
  • man 5 proc — /proc/[pid]/maps, status, smaps
  • /proc/sys/vm/* — 虚拟内存相关参数