32 - 存储管理

内存是计算机中最宝贵的资源之一。操作系统内存管理的核心任务是:保护(不让进程互相干扰)、共享(让进程高效共享代码/数据)、虚拟化(让每个进程感觉拥有整个地址空间)。Linux 通过分页、段机制和一系列精巧的分配器实现了这些目标。


32.1 内存管理目标

三大核心目标

graph TD
 A["内存管理三大目标"] --> P["保护<br/>进程 A 不能读写<br/>进程 B 的内存"]
 A --> S["共享<br/>多个进程共享<br/>同一份库代码"]
 A --> V["虚拟化<br/>每个进程拥有<br/>独立的虚拟地址空间"]
 
 P --> MMU["MMU / 页表<br/>每次访问都检查权限"]
 S --> COW["写时复制 COW<br/>只读共享,写入时复制"]
 V --> VAS["虚拟地址空间<br/>逻辑地址 ≠ 物理地址"]
 
 style A fill:#e1f5fe,stroke:#333,stroke-width:2px
# 验证进程间的内存隔离
# 终端 1: 启动一个进程并查看其内存映射
sleep 1000 &
PID1=$!
cat /proc/$PID1/maps | head -5
 
# 终端 2: 尝试读写该进程的内存(正常用户无法跨进程访问)
# cat /proc/$PID1/mem 会失败(除非 root 且有 ptrace 权限)
 
# 查看内核保护的物理内存布局
sudo cat /proc/iomem | head -20
# 输出:物理内存地址范围和用途
# 00000000-00000fff : Reserved
# 00100000-7fffffff : System RAM
# ...

32.2 地址绑定与地址空间

从源代码到物理内存

graph LR
 S["源代码<br/>main.c"] -->|编译器| O["目标文件<br/>main.o<br/>(相对地址)"]
 O -->|链接器| E["可执行文件<br/>a.out<br/>(逻辑地址)"]
 E -->|加载器| M["内存中的进程<br/>(逻辑地址 → 物理地址)"]
 M -->|MMU| P["物理内存"]
 
 style S fill:#fff9c4,stroke:#333
 style E fill:#c8e6c9,stroke:#333
 style M fill:#e1f5fe,stroke:#333
 style P fill:#f3e5f5,stroke:#333

地址绑定的三个阶段

绑定时间何时确定特点
编译时编译器已知进程的绝对地址不灵活,嵌入式系统
加载时程序加载到内存时可重定位,但不能运行时移动
执行时指令执行时由 MMU 动态翻译现代 OS 使用此方案(支持换入换出)

逻辑地址 vs 物理地址

# 逻辑地址(虚拟地址):进程看到的地址,从 0 开始
cat /proc/$$/maps | head -5
# 555555554000-555555555000 r--p 00000000 08:01 1234567 /usr/bin/bash
# ^^^^^^^^^^^^ 这是虚拟地址!不是物理内存地址
 
# 物理地址:内存芯片上的实际地址
# 用户态程序无法直接看到物理地址(这是安全的基石)
 
# 查看物理内存布局(需要 root)
sudo cat /proc/iomem | head -10
 
# 查看页表:从虚拟地址到物理地址的映射
sudo cat /proc/$$/pagemap 2>/dev/null | xxd | head -5
# pagemap 是一个二进制文件,每个页对应 8 字节

32.3 连续内存分配与碎片

固定分区 vs 可变分区

graph TD
 subgraph "固定分区"
 FP0["分区 0: 8MB"]
 FP1["分区 1: 8MB"]
 FP2["分区 2: 8MB"]
 FP3["分区 3: 8MB"]
 end
 
 subgraph "可变分区"
 VP0["进程 A: 5MB"]
 VP1["空闲: 3MB"]
 VP2["进程 B: 10MB"]
 VP3["空闲: 6MB"]
 VP4["进程 C: 8MB"]
 end

两种碎片

碎片类型定义示例
外部碎片空闲空间总和足够但分散成小块,无法满足一个大请求两个 3MB 空闲块,但需要 5MB 连续空间
内部碎片分配的空间比请求的大(多余部分被浪费)请求 47KB,分得 64KB 的块(13KB 浪费)
# 查看内存碎片情况(通过 /proc/buddyinfo)
cat /proc/buddyinfo
# Node 0, zone DMA 1 1 0 1 2 1 1 0 1 1 3 
# Node 0, zone DMA32 567 442 234 123 67 34 12 3 1 1 0
# Node 0, zone Normal 12345 9876 5432 2345 1234 567 234 123 45 12 3
# 
# 每列代表 2^order 个连续页面块的数量
# order 0: 2^0 = 1 个页面 (4KB)
# order 1: 2^1 = 2 个页面 (8KB) 连续块
# ...
# order 10: 2^10 = 1024 个页面 (4MB)
# 
# 如果高阶 order 的值很小 → 内存碎片化严重

32.4 分页(Paging)

分页的基本原理

graph TD
 subgraph "虚拟地址空间"
 VA0["页 0"]
 VA1["页 1"]
 VA2["页 2"]
 VA3["页 3"]
 end
 
 subgraph "页表"
 PT["页表"]
 end
 
 subgraph "物理内存"
 PF0["帧 100"]
 PF1["帧 50"]
 PF2["帧 200"]
 PF3["帧 3"]
 end
 
 VA0 -->|"页 0 → 帧 100"| PF0
 VA1 -->|"页 1 → 帧 50"| PF1
 VA2 -->|"页 2 → 帧 200"| PF2
 VA3 -->|"页 3 → 帧 3"| PF3

页表条目(PTE)的结构

PTE (Page Table Entry) — x86-64:
┌─────────────────────────────────────────────────────────────┐
│ 物理页框号 (PFN) │ AVL │ G │ PAT │ D │ A │ PCD │ PWT │ U/S │ R/W │ P │
│ 位 63-M │ │ │ │ │ │ │ │ │ │ │
└─────────────────────────────────────────────────────────────┘
P (Present): 1 = 页在内存中
R/W (Read/Write): 0 = 只读, 1 = 可读写
U/S (User/Supervisor): 0 = 仅内核, 1 = 用户可访问
A (Accessed): 1 = 曾被访问过
D (Dirty): 1 = 曾被写入过(用于判断是否需要写回磁盘)

MMU 与 TLB

sequenceDiagram
 participant CPU
 participant TLB as TLB (快表)
 participant MMU
 participant PT as 页表 (内存中)
 participant MEM as 物理内存

 CPU->>MMU: 虚拟地址 VA
 
 alt TLB 命中
 MMU->>TLB: 查询 TLB
 TLB-->>MMU: 物理地址 PA
 MMU->>MEM: 访问 PA
 Note over MMU: 约 0.5ns (一次缓存访问)
 else TLB 未命中
 MMU->>PT: 走页表 (可能需要多次内存访问)
 PT-->>MMU: 物理地址 PA
 MMU->>TLB: 更新 TLB
 MMU->>MEM: 访问 PA
 Note over MMU: 约 50-100ns (多次内存访问)
 end
# 查看 TLB 相关信息
# TLB 是 CPU 硬件结构,无法从 /proc 直接查看
# 但可以通过性能计数器间接观察
 
# 查看 TLB shootdown 的影响(多核系统中刷新 TLB 的 IPI)
sudo perf stat -e 'dtlb_load_misses.miss_causes_a_walk,dtlb_store_misses.miss_causes_a_walk' -- sleep 3
 
# 查看页大小
getconf PAGE_SIZE
# 4096 (4KB,标准页大小)
 
# 查看是否启用了大页
grep Huge /proc/meminfo
# HugePages_Total: 0
# Hugepagesize: 2048 kB (2MB 大页)

32.5 多级页表

为什么需要多级

32 位系统,4KB 页,每个 PTE 4 字节:

  • 总页数:4GB / 4KB = 1,048,576 页
  • 页表总大小:1M × 4B = 4MB(每个进程!)
  • 100 个进程 → 400MB 仅用于页表

多级页表允许不存在的映射部分不分配页表:

graph TD
 PGDT["PGD (页全局目录)<br/>1 个条目 = 512GB"] --> PUD0["PUD 条目 0"]
 PGDT --> PUDn["PUD 条目 511"]
 PUD0["PUD (页上级目录)<br/>1 个条目 = 1GB"] --> PMD0_0["PMD 条目 0<br/>(如不需要,不分配)"]
 PUD0 --> PMD0_m["PMD 条目 511"]
 PMD0_0["PMD (页中间目录)<br/>1 个条目 = 2MB"] --> PTE0_0_0["PTE 条目 0"]
 PMD0_0 --> PTE0_0_p["PTE 条目 511"]
 PTE0_0_0["PTE (页表)<br/>1 个条目 = 4KB"] --> PAGE["物理页 4KB"]
 
 style PGDT fill:#ffcdd2,stroke:#333
 style PUD0 fill:#c8e6c9,stroke:#333
 style PMD0_0 fill:#b3e5fc,stroke:#333
 style PTE0_0_0 fill:#fff9c4,stroke:#333

x86-64 的五级页表(Linux 命名)

级别名称偏移位数寻址范围
1PGD (Page Global Directory)bits 47:39512GB
2P4D (Page 4th-level Directory)bits 38:301GB
3PUD (Page Upper Directory)bits 29:212MB
4PMD (Page Middle Directory)bits 20:124KB
5PTE (Page Table Entry)bits 11:0页内偏移
# 查看内核页表占用的内存(需要 root)
grep PageTables /proc/meminfo
# PageTables: 123456 kB — 所有进程页表占用的总内存
 
# 查看每个进程的页表大小
sudo cat /proc/$$/status | grep -E "VmPTE|VmPMD"
# VmPTE: 页表条目占用的虚拟内存

32.6 分段(Segmentation)

段的概念

段是一段有逻辑意义的连续内存区域

代码段 (.text) — 可执行、只读、固定大小
数据段 (.data) — 可读写、固定大小
堆 (Heap) — 可读写、动态增长
栈 (Stack) — 可读写、动态增长
内存映射段 (mmap) — 共享库、文件映射等

分段与分页的结合

Intel x86 历史上使用”段:偏移”的地址模型(分段+分页),但实际上 Linux 将段平坦化了:

# 查看段寄存器(Linux 对所有用户进程使用相同的平坦段)
# cs/ss/ds/es 等段寄存器都指向相同的平坦内存区域
 
# Linux 的段布局:base=0, limit=4GB(32位)或全 64 位范围
# 实际的内存保护仅由分页机制(页表的 R/W、U/S 位)完成

32.7 Linux 内存分配器

Buddy System(伙伴系统)

Buddy System 是 Linux 物理内存分配的核心算法:

graph TD
 subgraph "Buddy System 分配流程"
 O10["Order 10 (4MB)<br/>■ 1 个块"] -->|"1 个请求 4KB<br/>(order 0)"| O9["Order 9 (2MB)<br/>■■ 2 个块"]
 O9 --> O8["Order 8 (1MB)<br/>■■■ 4 个块"]
 O8 --> O5["...重复分裂..."]
 O5 --> O0["Order 0 (4KB)<br/>■■■■■... 1024 个块"]
 O0 --> ALLOC["分配 4KB"]
 end
 
 subgraph "Buddy System 释放流程"
 FREE["释放 4KB"] --> MERGE["与伙伴合并<br/>如果伙伴空闲"]
 MERGE --> MERGE2["继续向上合并<br/>直到伙伴不空闲"]
 end
# 查看 Buddy System 状态(各区各阶的空闲块数)
cat /proc/buddyinfo
# Node 0, zone Normal 1234 567 234 89 34 12 5 2 1 0 0
# ^^^^ order 0 (4KB)
# ^^^ order 1 (8KB)
# ... ^ order 10 (4MB)
 
# 查看各区的详细信息
cat /proc/pagetypeinfo | head -30
# 显示每种页面迁移类型的可用页数

Slab / Slub Allocator

Slab 分配器是 Buddy 之上的缓存层,用于内核对象(task_struct、inode 等)的高效分配:

# 查看 slab 缓存信息
cat /proc/slabinfo | head -20
# slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> ...
# task_struct 1234 1280 2752 11 ...
# inode_cache 5678 6000 1088 30 ...
# dentry 8765 9000 256 32 ...
 
# 查看 slab 总体内存占用
grep Slab /proc/meminfo
# Slab: 123456 kB
 
# 用 slabtop 实时查看(需要 root)
sudo slabtop -o 2>/dev/null | head -20

kmalloc / vmalloc

分配器特点映射方式使用场景
kmalloc()分配物理连续内存直接映射区(线性映射)小对象、DMA 缓冲区
vmalloc()分配虚拟连续内存(物理可以不连续)单独映射(需要建立页表)大缓冲区、内核模块
# 查看 vmalloc 区域使用情况
cat /proc/vmallocinfo | head -20
# 0xffffc90000000000-0xffffc90000005000 20480 module_1+0x0/0x...

32.8 /proc 内存接口总览

# 1. 全局内存统计(最常用)
cat /proc/meminfo
# MemTotal: 总物理内存
# MemFree: 完全空闲内存
# MemAvailable: 可用内存(包括可回收的缓存)
# Buffers: 块设备缓冲区
# Cached: 页面缓存
# SwapTotal: 交换空间总量
# SwapFree: 空闲交换空间
# Dirty: 等待写回磁盘的脏页
# Writeback: 正在写回磁盘的页
# AnonPages: 匿名页(堆、栈等)
# Mapped: 被映射的文件页
# Shmem: tmpfs 使用的内存
# KReclaimable: 可回收的内核内存(如 slab 缓存)
# Slab: slab 分配器使用的内存
# PageTables: 页表占用的内存
# KernelStack: 内核栈占用的内存
 
# 2. 查看特定进程的内存
cat /proc/$$/status | grep -E "VmSize|VmRSS|VmData|VmStk|VmExe|VmLib|VmPTE|VmSwap"
# VmSize: 虚拟内存总大小
# VmRSS: 常驻物理内存(Resident Set Size)
# VmData: 数据段(堆 + 静态数据)
# VmStk: 栈大小
# VmExe: 代码段(text)
# VmLib: 映射的共享库
# VmSwap: 被换出的内存
 
# 3. 内存映射详情
cat /proc/$$/smaps | head -50
# 每个映射区域的详细信息,包括 RSS、PSS、匿名/文件映射
 
# 4. 查看 NUMA 内存分配
cat /proc/$$/numa_maps 2>/dev/null | head

32.9 地址空间随机化(ASLR)

ASLR 的工作原理

ASLR(Address Space Layout Randomization)为进程的各个内存区域添加随机偏移,使攻击者难以预测代码/数据的地址:

# 查看当前 ASLR 设置
cat /proc/sys/kernel/randomize_va_space
# 0 = 关闭
# 1 = 随机化共享库、栈、mmap、VDSO
# 2 = 完全随机化(包括 brk/堆,默认值)
 
# 验证 ASLR 效果:两次运行同一个程序,地址不同
cat /proc/self/maps | head -5
cat /proc/self/maps | head -5
# 每次输出中的地址范围都不同
 
# 临时禁用 ASLR
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
 
# 以禁用 ASLR 的方式运行单个程序
setarch $(uname -m) -R bash -c 'cat /proc/self/maps | head -3'

32.10 内存管理命令速查

命令作用示例
free -h查看系统内存使用概况free -h
vmstat 1实时查看内存/交换/CPUvmstat 1 5
top / htop按内存排序查看进程htop, 按 M 键
pmap -x PID查看进程的内存映射详情pmap -x $$
smem -k按 PSS/USS 查看内存占用sudo smem -k -s pss
slabtop查看内核 slab 缓存sudo slabtop -o
numastat查看 NUMA 内存统计numastat -m
cat /proc/buddyinfo查看 Buddy 分配器状态cat /proc/buddyinfo
cat /proc/slabinfo查看 Slab 缓存详情cat /proc/slabinfo

相关链接