虚拟内存管理
虚拟内存是现代操作系统的核心特性之一,通过硬件 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);
}
缺页类型:
- Minor Page Fault:页在内存中,但页表未建立映射
- Major Page Fault:页不在内存中,需要从磁盘读取
- 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) 章节。