虚拟内存管理

虚拟内存是现代操作系统的核心特性之一,通过硬件 MMU 和内核的协作,为每个进程提供独立的地址空间。

虚拟内存架构

┌─────────────────────────────────────────────────┐
│           进程虚拟地址空间                        │
├─────────────────────────────────────────────────┤
│  0xFFFFFFFFFFFFFFFF                             │
│  ┌────────────────┐                             │
│  │  Kernel Space  │  内核空间(共享)            │
│  └────────────────┘                             │
│  0xFFFF800000000000                             │
│  ┌────────────────┐                             │
│  │   Stack        │  ↓ 栈(向下增长)            │
│  ├────────────────┤                             │
│  │   Shared Libs  │  共享库                      │
│  ├────────────────┤                             │
│  │   Heap         │  ↑ 堆(向上增长)            │
│  ├────────────────┤                             │
│  │   BSS          │  未初始化数据                │
│  ├────────────────┤                             │
│  │   Data         │  已初始化数据                │
│  ├────────────────┤                             │
│  │   Text         │  代码段                      │
│  └────────────────┘                             │
│  0x0000000000000000                             │
└─────────────────────────────────────────────────┘
         ↓ MMU 地址转换
┌─────────────────────────────────────────────────┐
│           物理内存                               │
└─────────────────────────────────────────────────┘

分页机制

四级页表(x86-64)

虚拟地址(64位)
┌──────┬──────┬──────┬──────┬──────┬────────────┐
│ 未用 │ PGD  │ PUD  │ PMD  │ PT   │   Offset   │
│ 16位 │ 9位  │ 9位  │ 9位  │ 9位  │   12位     │
└──────┴──────┴──────┴──────┴──────┴────────────┘
         │      │      │      │           │
         │      │      │      │           └─ 页内偏移(4KB)
         │      │      │      └─ Page Table 索引
         │      │      └─ Page Middle Directory 索引
         │      └─ Page Upper Directory 索引
         └─ Page Global Directory 索引

转换流程:
1. CR3 寄存器 → PGD 基址
2. PGD[index] → PUD 基址
3. PUD[index] → PMD 基址
4. PMD[index] → PT 基址
5. PT[index] → 物理页框号
6. 物理页框号 + 偏移 → 物理地址

页表项结构

// arch/x86/include/asm/pgtable_types.h
typedef struct {
    unsigned long pte;
} pte_t;

// 页表项标志位(64位)
#define _PAGE_PRESENT   (1UL << 0)   // P: 页面在内存中
#define _PAGE_RW        (1UL << 1)   // R/W: 可写
#define _PAGE_USER      (1UL << 2)   // U/S: 用户可访问
#define _PAGE_PWT       (1UL << 3)   // PWT: 写通
#define _PAGE_PCD       (1UL << 4)   // PCD: 禁用缓存
#define _PAGE_ACCESSED  (1UL << 5)   // A: 已访问
#define _PAGE_DIRTY     (1UL << 6)   // D: 已修改
#define _PAGE_PSE       (1UL << 7)   // PS: 大页(2MB/1GB)
#define _PAGE_GLOBAL    (1UL << 8)   // G: 全局页
#define _PAGE_NX        (1UL << 63)  // NX: 不可执行

// 页表项布局(简化)
┌─────┬──────────────────────────┬────────────┐
│ NX  │   物理页框号(PFN)       │   标志位   │
│ 1位 │      51-12位             │   12位     │
└─────┴──────────────────────────┴────────────┘

TLB(Translation Lookaside Buffer)

TLB 快表缓存:
┌──────────────┬──────────────┬──────┐
│  虚拟页号    │  物理页框号  │ 标志 │
├──────────────┼──────────────┼──────┤
│  0x1000      │  0x5000      │ RW   │
│  0x2000      │  0x6000      │ R    │
│  0x3000      │  0x7000      │ RWX  │
└──────────────┴──────────────┴──────┘

TLB 命中:直接获取物理地址(1 个时钟周期)
TLB 未命中:遍历页表(~100 个时钟周期)

查看 TLB 统计

# 使用 perf 查看 TLB 未命中
perf stat -e dTLB-loads,dTLB-load-misses,iTLB-loads,iTLB-load-misses ./program

# 查看大页使用(减少 TLB 未命中)
cat /proc/meminfo | grep Huge

内存区域管理

vm_area_struct

// include/linux/mm_types.h
struct vm_area_struct {
    unsigned long vm_start;         // 起始地址
    unsigned long vm_end;           // 结束地址
    
    struct vm_area_struct *vm_next; // 链表指针
    struct vm_area_struct *vm_prev;
    
    struct rb_node vm_rb;           // 红黑树节点
    
    struct mm_struct *vm_mm;        // 所属 mm_struct
    pgprot_t vm_page_prot;          // 页保护标志
    unsigned long vm_flags;         // VMA 标志
    
    struct anon_vma *anon_vma;      // 匿名页
    
    const struct vm_operations_struct *vm_ops; // 操作函数
    
    unsigned long vm_pgoff;         // 文件偏移
    struct file *vm_file;           // 映射的文件
    void *vm_private_data;          // 私有数据
};

// VMA 标志
#define VM_READ         0x00000001  // 可读
#define VM_WRITE        0x00000002  // 可写
#define VM_EXEC         0x00000004  // 可执行
#define VM_SHARED       0x00000008  // 共享
#define VM_MAYREAD      0x00000010  // 可能可读
#define VM_MAYWRITE     0x00000020  // 可能可写
#define VM_MAYEXEC      0x00000040  // 可能可执行
#define VM_MAYSHARE     0x00000080  // 可能共享
#define VM_GROWSDOWN    0x00000100  // 向下增长(栈)
#define VM_GROWSUP      0x00000200  // 向上增长
#define VM_DONTCOPY     0x00020000  // fork 时不复制
#define VM_DONTEXPAND   0x00040000  // 不能扩展
#define VM_LOCKED       0x00002000  // 锁定在内存
#define VM_IO           0x00004000  // I/O 映射

mm_struct

// include/linux/mm_types.h
struct mm_struct {
    struct vm_area_struct *mmap;    // VMA 链表头
    struct rb_root mm_rb;           // VMA 红黑树
    
    pgd_t *pgd;                     // 页全局目录
    
    atomic_t mm_users;              // 用户数
    atomic_t mm_count;              // 引用计数
    
    unsigned long start_code;       // 代码段起始
    unsigned long end_code;         // 代码段结束
    unsigned long start_data;       // 数据段起始
    unsigned long end_data;         // 数据段结束
    unsigned long start_brk;        // 堆起始
    unsigned long brk;              // 堆当前位置
    unsigned long start_stack;      // 栈起始
    
    unsigned long total_vm;         // 总虚拟内存页数
    unsigned long locked_vm;        // 锁定页数
    unsigned long pinned_vm;        // 固定页数
    unsigned long data_vm;          // 数据段页数
    unsigned long exec_vm;          // 可执行页数
    unsigned long stack_vm;         // 栈页数
};

查看进程内存映射

# 查看进程的内存映射
cat /proc/<PID>/maps

# 输出示例:
# 地址范围           权限  偏移    设备   inode    路径
# 00400000-00452000 r-xp 00000000 08:01 173521   /usr/bin/dbus-daemon
# 00651000-00652000 r--p 00051000 08:01 173521   /usr/bin/dbus-daemon
# 00652000-00655000 rw-p 00052000 08:01 173521   /usr/bin/dbus-daemon
# 00e03000-00e24000 rw-p 00000000 00:00 0        [heap]
# 7f7d4e000000-7f7d4e020000 rw-p 00000000 00:00 0
# 7ffde5f0a000-7ffde5f2c000 rw-p 00000000 00:00 0 [stack]

# 更详细的内存信息
cat /proc/<PID>/smaps

# 内存统计
cat /proc/<PID>/status | grep -E 'Vm|Rss'

分析脚本

#!/bin/bash
# analyze-process-memory.sh

PID=$1

if [ -z "$PID" ]; then
    echo "用法: $0 <PID>"
    exit 1
fi

echo "=== 进程 $PID 内存分析 ==="
echo ""

# 基本信息
echo "--- 基本内存信息 ---"
cat /proc/$PID/status | grep -E 'Name|Vm|Rss|Shared|Private'
echo ""

# 内存区域统计
echo "--- 内存区域统计 ---"
awk '
BEGIN { 
    print "类型\t\t大小(KB)"
}
/^[0-9a-f]/ {
    size = strtonum("0x" $2) - strtonum("0x" $1)
    size_kb = size / 1024
    
    if ($6 ~ /\[heap\]/) type = "Heap"
    else if ($6 ~ /\[stack\]/) type = "Stack"
    else if ($6 ~ /\.so/) type = "Shared Lib"
    else if ($2 == "r-xp" && $6 != "") type = "Code"
    else if ($2 ~ /rw/) type = "Data"
    else type = "Other"
    
    total[type] += size_kb
}
END {
    for (t in total) {
        printf "%s\t\t%.2f\n", t, total[t]
    }
}
' /proc/$PID/maps

缺页异常

缺页处理流程

// arch/x86/mm/fault.c
dotraplinkage void
do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
    unsigned long address = read_cr2(); // 获取触发地址
    
    // 查找 VMA
    struct vm_area_struct *vma = find_vma(mm, address);
    
    if (!vma || vma->vm_start > address) {
        // 非法访问
        goto bad_area;
    }
    
    // 检查权限
    if (error_code & PF_WRITE) {
        if (!(vma->vm_flags & VM_WRITE))
            goto bad_area;
    }
    
    // 处理缺页
    return handle_mm_fault(vma, address, flags);
    
bad_area:
    // 发送 SIGSEGV 信号
    force_sig_fault(SIGSEGV, si_code, address);
}

缺页类型

  1. Minor Page Fault:页在内存中,但页表未建立映射
  2. Major Page Fault:页不在内存中,需要从磁盘读取
  3. Invalid Page Fault:访问非法地址,触发 SIGSEGV

查看缺页统计

# 系统级缺页统计
vmstat 1

# 进程级缺页统计
ps -o min_flt,maj_flt,cmd -p <PID>

# /proc 文件系统
cat /proc/<PID>/stat | awk '{print "Minor faults: " $10 "\nMajor faults: " $12}'

内存分配

用户空间内存分配

#include <stdlib.h>
#include <sys/mman.h>

// malloc/free(通过 brk 或 mmap)
void *ptr = malloc(1024);
free(ptr);

// brk/sbrk(调整堆大小)
void *brk(void *addr);
void *sbrk(intptr_t increment);

// mmap(内存映射)
void *addr = mmap(NULL, 4096, 
                  PROT_READ | PROT_WRITE,
                  MAP_PRIVATE | MAP_ANONYMOUS,
                  -1, 0);
munmap(addr, 4096);

// mmap 文件
int fd = open("file.dat", O_RDWR);
void *mapped = mmap(NULL, size, PROT_READ | PROT_WRITE,
                   MAP_SHARED, fd, 0);
// 修改 mapped 内容会同步到文件
munmap(mapped, size);
close(fd);

内存锁定

#include <sys/mman.h>

// 锁定内存(防止换出)
mlock(addr, len);       // 锁定指定区域
mlockall(MCL_CURRENT);  // 锁定当前所有页
mlockall(MCL_FUTURE);   // 锁定未来分配的页

// 解锁
munlock(addr, len);
munlockall();

// 查看锁定内存限制
ulimit -l

// 设置限制(KB)
ulimit -l 65536

Copy-on-Write (COW)

fork() 时的 COW

// fork 时不立即复制内存
pid_t pid = fork();

if (pid == 0) {
    // 子进程
    // 父子进程共享页表,页面标记为只读
    // 写入时触发缺页,内核复制页面
    data[0] = 1;  // 触发 COW
}

COW 机制

fork() 调用
    ↓
复制页表(不复制物理页)
    ↓
所有页标记为只读
    ↓
写操作 → 缺页异常
    ↓
内核复制物理页
    ↓
更新页表为可写
    ↓
继续执行

查看 COW 效果

// cow_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

void show_memory() {
    char cmd[128];
    snprintf(cmd, sizeof(cmd), "cat /proc/%d/status | grep VmRSS", getpid());
    system(cmd);
}

int main() {
    // 分配 100MB
    size_t size = 100 * 1024 * 1024;
    char *data = malloc(size);
    memset(data, 0, size);
    
    printf("Parent before fork:\n");
    show_memory();
    
    pid_t pid = fork();
    
    if (pid == 0) {
        printf("\nChild after fork (before write):\n");
        show_memory();
        
        // 触发 COW
        memset(data, 1, size);
        
        printf("\nChild after write:\n");
        show_memory();
        exit(0);
    } else {
        printf("\nParent after fork:\n");
        show_memory();
        
        wait(NULL);
        
        printf("\nParent after child exits:\n");
        show_memory();
    }
    
    return 0;
}

性能优化

大页(Huge Pages)

# 查看大页配置
cat /proc/meminfo | grep Huge

# 配置大页(2MB)
echo 1024 > /proc/sys/vm/nr_hugepages  # 分配 1024 个 2MB 页(2GB)

# 透明大页(THP)
cat /sys/kernel/mm/transparent_hugepage/enabled
echo always > /sys/kernel/mm/transparent_hugepage/enabled

# 使用大页
#include <sys/mman.h>
void *addr = mmap(NULL, 2*1024*1024,
                  PROT_READ | PROT_WRITE,
                  MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
                  -1, 0);

NUMA 优化

# 查看 NUMA 拓扑
numactl --hardware

# 查看进程的 NUMA 统计
numastat -p <PID>

# 绑定到特定 NUMA 节点
numactl --cpunodebind=0 --membind=0 ./program

# 设置内存分配策略
numactl --interleave=all ./program  # 交错分配

下一步:学习 物理内存管理(Buddy 和 Slab) 章节。