引言
Linux内存管理是操作系统内核最复杂的子系统之一,它负责管理物理内存、虚拟地址空间、页面缓存以及内存分配与回收。理解Linux内存管理的底层机制,对于系统调优、性能排查和内核开发至关重要。本文将从虚拟地址空间出发,深入剖析Linux内存管理的完整技术链条。
一、虚拟地址空间架构
Linux为每个进程提供独立的虚拟地址空间,在x86_64架构下,用户空间占据0x0000000000000000到0x00007FFFFFFFFFFF(128TB),内核空间占据0xFFFF80000000000以上。
// 典型进程内存布局(高地址→低地址)
┌──────────────────────┐ 0xFFFFFFFFFFFFFFFF
│ 内核空间 │ (128TB, 所有进程共享)
├──────────────────────┤ 0xFFFF800000000000
│ 未使用区域 │
├──────────────────────┤ 0x00007FFFFFFFFFFF
│ 栈 (Stack) │ ↓ 向下增长
├──────────────────────┤
│ mmap 区域 │ 动态库、共享内存映射
├──────────────────────┤
│ 堆 (Heap) │ ↑ 向上增长 (brk/mmap)
├──────────────────────┤
│ BSS段 │ 未初始化全局变量
├──────────────────────┤
│ Data段 │ 已初始化全局变量
├──────────────────────┤
│ Text段 │ 程序代码
└──────────────────────┘ 0x0000000000400000 (典型ELF入口)
用户空间通过mm_struct结构体进行管理:
struct mm_struct {
pgd_t *pgd; // 页全局目录(PGD)基址
unsigned long start_code, end_code; // 代码段范围
unsigned long start_data, end_data; // 数据段范围
unsigned long start_brk, brk; // 堆边界
unsigned long start_stack; // 栈起始地址
unsigned long mmap_base; // mmap区域基址
int map_count; // VMA数量
// ...
};
虚拟内存区域通过VMA(vm_area_struct)进行细粒度管理,每个VMA代表一段具有相同权限的连续虚拟地址范围:
// 查看进程内存映射
$ cat /proc/$$/maps
55c3d8a6d000-55c3d8a6e000 r--p 00000000 08:02 1310778 /usr/bin/bash
55c3d8a6e000-55c3d8a7a000 r-xp 00001000 08:02 1310778 /usr/bin/bash
55c3d8a7a000-55c3d8a7d000 r--p 0000d000 08:02 1310778 /usr/bin/bash
55c3d8a7d000-55c3d8a7e000 rw-p 00010000 08:02 1310778 /usr/bin/bash
55c3d8a7e000-55c3d8a87000 rw-p 00000000 00:00 0 [heap]
7f1d8c000000-7f1d8c028000 rw-p 00000000 00:00 0
7f1d8c028000-7f1d90000000 ---p 00000000 00:00 0
7f1d92200000-7f1d923e7000 r-xp 00000000 08:02 1048602 /usr/lib/libc.so.6
二、多级页表机制
x86_64架构采用4级页表结构,将64位虚拟地址拆分为5个字段:
// 48位有效虚拟地址的页表索引结构
┌─────────┬─────────┬─────────┬─────────┬──────────┐
│ PML4 │ PDPT │ PD │ PT │ Offset │
│ 9 bits │ 9 bits │ 9 bits │ 9 bits │ 12 bits │
│(bit47:39)│(bit38:30)│(bit29:21)│(bit20:12)│(bit11:0) │
└─────────┴─────────┴─────────┴─────────┴──────────┘
PML4: Page Map Level 4 (顶级页表)
PDPT: Page Directory Pointer Table (页目录指针表)
PD: Page Directory (页目录)
PT: Page Table (页表)
Offset: 页内偏移 (4KB页)
2017年后Intel引入了5级页表(LA57),支持57位虚拟地址空间(128PB):
// 5级页表新增PML5层级,索引bit56:49
┌─────────┬─────────┬─────────┬─────────┬─────────┬──────────┐
│ PML5 │ PML4 │ PDPT │ PD │ PT │ Offset │
└─────────┴─────────┴─────────┴─────────┴─────────┴──────────┘
TLB(Translation Lookaside Buffer)缓存最近使用的页表项访问:
// 查看TLB信息
$ lscpu | grep TLB
L1d TLB: 64 entries (4KB pages), 4-way
L1i TLB: 128 entries (4KB pages), 8-way
L2 TLB: 1536 entries (4KB pages), 6-way
// TLB刷新相关指令
// invlpg addr - 刷新单个TLB项
// mov cr3, reg - 刷新所有非全局TLB项
Linux内核使用struct page描述每个物理页框:
struct page {
unsigned long flags; // 状态标志 (PG_locked, PG_dirty等)
atomic_t _refcount; // 引用计数
atomic_t _mapcount; // 映射计数(被多少PTE指向)
struct {
union {
struct list_head lru; // LRU链表节点
struct page *next; // 复合页链表
};
};
unsigned long private; // 私有数据指针
struct address_space *mapping;// 关联地址空间
pgoff_t index; // 在映射中的偏移
};
三、物理内存分配器
Linux物理内存管理采用三层架构:
3.1 Buddy System(伙伴系统)
伙伴系统以2的幂次为单位管理物理页(4KB, 8KB, 16KB...2MB),核心思想是分裂与合并:
// 伙伴系统核心逻辑(简化版)
// 分配时:向上查找合适阶→分裂成两个伙伴→返回一半
// 释放时:检查伙伴是否空闲→合并→向上传导
// 阶(order)与页数对应关系
order 0: 1 page (4KB)
order 1: 2 pages (8KB)
order 2: 4 pages (16KB)
order 9: 512 pages (2MB) // 最大
// 伙伴关系示例(order=2, 16页框=64KB块)
Block A: pages 0-15 Block B: pages 16-31
若 A的地址 XOR (4 << PAGE_SHIFT) == B的地址 → 互为伙伴
// 查看区域空闲情况
$ cat /proc/buddyinfo
Node 0, zone DMA 1 0 1 1 2 1 1 0 1 1 3
Node 0, zone DMA32 315 287 226 133 72 36 19 10 4 1 2
Node 0, zone Normal 8014 6144 4096 2048 1024 512 256 128 64 32 16
↑order0 ↑order1 ↑ ... ↑order10(2MB)
3.2 SLAB/SLUB分配器
SLAB分配器在伙伴系统之上构建对象缓存,避免频繁分配/释放小对象的开销:
// SLUB(Linux默认分配器)核心数据结构
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // CPU本地快速路径
struct kmem_cache_node *node; // NUMA节点级缓存
unsigned int size; // 对象大小(含对齐)
unsigned int object_size; // 用户请求的实际大小
unsigned long flags; // SLAB_ACCOUNT等
const char *name; // 缓存名称(如"dentry_cache")
};
struct kmem_cache_cpu {
void **freelist; // 空闲对象链表头
unsigned long tid; // 全局事务ID(用于并发控制)
struct page *page; // 当前正在使用的页
struct page *partial; // CPU级部分空闲slab链表
};
// 典型的kmem_cache
dentry_cache (目录缓存)
inode_cache (索引节点)
vm_area_struct (VMA结构体)
task_struct (进程描述符)
// 查看SLAB分配器使用
$ cat /proc/slabinfo | head
slabinfo - version: 2.1
# name ...
dentry 28674 33216 192 21 4 0 0
inode_cache 9874 10400 632 5 8 0 0
vm_area_struct 11234 12012 208 19 4 0 0
task_struct 1024 1080 7488 1 8 2 0
3.3 Per-CPU页框缓存
每个CPU维护一批热页和冷页的私有缓存,减少跨CPU的缓存行 bouncing:
// Per-CPU pageset
struct per_cpu_pageset {
struct per_cpu_pages {
int count; // 当前页框数
int high; // 高水位线(超过则归还zone)
int batch; // 批量分配/回收数量
struct list_head lists[MIGRATE_PCPTYPES]; // 按迁移类型分链
} pcp[2]; // [0]=热页 pcp, [1]=冷页 pcp
};
四、页面回收机制
当物理内存不足时,Linux通过LRU(Least Recently Used)算法回收页面:
4.1 LRU链表设计
Linux内核采用双链表设计分别追踪活跃和不活跃页面:
// 区域LRU链表(zone→pgdat→lruvec)
enum LRU_LISTS {
LRU_INACTIVE_ANON = 0, // 不活跃匿名页
LRU_ACTIVE_ANON, // 活跃匿名页
LRU_INACTIVE_FILE, // 不活跃文件页
LRU_ACTIVE_FILE, // 活跃文件页
LRU_UNEVICTABLE, // 不可回收页(mlock等)
NR_LRU_LISTS
};
// 页面在活跃/不活跃链表间的演化:
// 首次访问 → INACTIVE链表
// 第二次访问 → 提升到ACTIVE链表(promote)
// 长时间未访问 → 降级到INACTIVE链表(demote)
// 仍在INACTIVE且内存紧张 → 回收
// 相关参数
vm.swappiness = 60 // 回收倾向(0=尽量不swap,100=积极swap)
vm.min_free_kbytes = 67584 // 保留的最低空闲内存(kswapd唤醒阈值)
4.2 kswapd与直接回收
页面回收有两种路径:
// kswapd(后台异步回收)
触发条件:free < WMARK_LOW(低水位线)
工作:扫描LRU尾部,回收页面直到free > WMARK_HIGH
每次回收可唤醒后睡眠,避免CPU占用
// direct reclaim(同步直接回收)
触发条件:进程分配时free < WMARK_MIN,且kswapd来不及回收
影响:进程被阻塞,性能急剧下降(可能达秒级)
避免方法:合理设置min_free_kbytes,或预先分配
// 水位线定义(zone层)
┌─────────────────────────┐ WMARK_HIGH ← kswapd目标
│ │
│ 可用内存充足 │ WMARK_LOW ← kswapd唤醒阈值
│ │
│ 内存开始紧张 │ WMARK_MIN ← 直接回收触发点
│ │
└─────────────────────────┘ 0
4.3 页面写回与交换
// 文件页回收路径(双LRU回收)
shrink_page_list() {
for page in lru_list:
if PageAnon(page): // 匿名页
if has_swap: // 有swap分区
写入swap(swap_cache)
从页表移除(保留swap_entry)
else: // 文件页
if PageDirty: // 脏页
异步写回(writeback)
写回后尝试回收
else:
直接回收(来自PageCache)
}
五、OOM Killer机制
当内存极度紧张且回收也无法满足时,内核触发OOM Killer选择进程终止:
// OOM评分计算(oom_badness)
// 评分越高,得分高者被杀掉
points = total_vm_pages // 基础分:进程使用的内存总量
points *= 1 << (oom_score_adj + 1000) / 1000 // 调整系数
// 特殊规则:
// - init/systemd (pid=1) 评分除以4
// - 内核线程忽略
// - CAP_SYS_ADMIN进程减分(受保护)
// OOM评分调整
$ echo -500 > /proc/$$/oom_score_adj // 降低被杀概率
$ echo 1000 > /proc/$$/oom_score_adj // 最容易被杀(测试用)
// 查看进程OOM评分
$ cat /proc/$$/oom_score // 当前评分(只读)
$ cat /proc/$$/oom_score_adj // 调整值(可写)
较新的内核引入了oomd(systemd配套)和BPF机制,在用户空间更早介入,避免完全依赖内核OOM:
// 使用cgroup memory.limit限制进程组
$ echo "2G" > /sys/fs/cgroup/memory/myapp/memory.limit_in_bytes
$ echo "1.5G" > /sys/fs/cgroup/memory/myapp/memory.soft_limit_in_bytes
// cgroup v2 内存控制
$ cat /sys/fs/cgroup/myapp/memory.current
$ cat /sys/fs/cgroup/myapp/memory.high // 软限制(触发回收)
$ cat /sys/fs/cgroup/myapp/memory.max // 硬限制(触发OOM)
$ cat /sys/fs/cgroup/myapp/memory.stat // 详细统计
六、大页(HugePages)支持
标准4KB粒度导致TLB Miss率随内存增大而上升,大页机制降低页表深度和TLB压力:
6.1 2MB/1GB大页对比
// 页表层级与TLB覆盖
页大小 TLB条目 TLB覆盖(典型) 页表层级
Normal 4KB 64 256KB 4级
HugePage 2MB 32 64MB 3级(跳过PT)
HugePage 1GB 4 4GB 2级(跳过PT和PD)
// 查看大页配置
$ cat /proc/meminfo | grep Huge
HugePages_Total: 0
HugePages_Free: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
// 预留静态大页(启动参数或sysctl)
$ echo 256 > /proc/sys/vm/nr_hugepages
// 或启动参数: default_hugepagesz=2M hugepagesz=2M hugepages=256
6.2 透明大页(THP)
Linux引入透明大页,由khugepaged线程主动合并连续的普通页:
// THP工作模式
$ cat /sys/kernel/mm/transparent_hugepage/enabled
always [madvise] never
always: 对所有区域尝试合并(可能增加延迟)
madvise: 仅对MADV_HUGEPAGE标注的映射进行
never: 禁用THP (数据库场景常用)
// 对关键区域显式控制
#include <sys/mman.h>
mmap(NULL, size, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0); // 显式大页
madvise(ptr, len, MADV_HUGEPAGE); // 建议THP扫描
madvise(ptr, len, MADV_NOHUGEPAGE); // 建议THP不处理
// THP的坑:延迟与内存碎片
// 数据库场景(Redis/MySQL)通常禁用THP,避免:
// 1. 大页分配时compact导致停顿
// 2. khugepaged CPU开销
// 3. 非必要的大页合并浪费内存
七、NUMA架构下的内存管理
多处理器系统中,内存访问延迟与物理位置相关:
// NUMA拓扑查看
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 size: 65408MB free: 32768MB
node 1 cpus: 8 9 10 11 12 13 14 15 size: 65536MB free: 49152MB
node distances:
node 0 1
0: 10 20
1: 20 10
// 跨节点访问价格为本地节点2倍
// NUMA感知的分配策略
// 默认:本地节点分配(First Touch Policy)
// 绑定:numactl --membind=0 ./app
// 交错:numactl --interleave=all ./app
// 优先:numactl --preferred=0 ./app
// 查看进程NUMA统计
$ cat /proc/$$/numa_maps
7f3e80000000 default file=/usr/lib/libc.so.6
7f3e80000000 bind:1 anon=128 dirty=128 N1=128 kernelpagesize_kB=4
// N0, N1...表示各节点上的页面分布
Linux内核通过自动均衡(Auto NUMA Balancing)动态迁移页面:
// 工作流程
// 1. task_numa_fault() 记录跨节点访问的页面
// 2. 累积到一定阈值后触发迁移
// 3. migrate_misplaced_page() 将页面迁移到访问者节点
$ echo 1 > /proc/sys/kernel/numa_balancing // 启用
$ echo 0 > /proc/sys/kernel/numa_balancing // 禁用(固定绑定场景)
八、内存碎片整理
长时间运行后,物理内存被分割成小块,影响大页分配:
// 外部碎片问题
// 场景:即使总空闲内存足够,也可能没有连续2MB的物理块
// 解决:内存规整(page compaction)
// 碎片索引(0-10,10表示完全碎片化)
$ cat /sys/kernel/debug/extfrag/extfrag_index
// 触发碎片整理
$ echo 1 > /proc/sys/vm/compact_memory
// compaction模式
$ cat /sys/kernel/mm/compaction/proactiveness
// 0=只在需要时触发, 100=积极预整理
// 迁移类型分组(buddy system内部)
enum migratetype {
MIGRATE_UNMOVABLE, // 不可移动(内核数据结构)
MIGRATE_MOVABLE, // 可移动(用户空间匿名页)
MIGRATE_RECLAIMABLE,// 可回收(文件缓存)
MIGRATE_PCPTYPES, // Per-CPU缓存
MIGRATE_ISOLATE, // 隔离(hotplug等)
MIGRATE_TYPES
};
// 目的:让相同迁移类型的页聚集,减少碎片
九、内存管理调试与观测
9.1 关键统计信息
$ cat /proc/meminfo
MemTotal: 16384000 kB
MemFree: 2048000 kB
MemAvailable: 8192000 kB // 包含可回收缓存的可用内存
Buffers: 256000 kB // 裸块设备缓冲
Cached: 4096000 kB // 文件页缓存
SwapTotal: 8388608 kB
SwapFree: 8388608 kB
Slab: 512000 kB // SLAB分配器使用
SReclaimable: 384000 kB // 可回收SLAB(dentry等)
SUnreclaim: 128000 kB // 不可回收SLAB
PageTables: 12000 kB // 页表占用
NFS_Unstable: 0 kB
Writeback: 256 kB
AnonHugePages: 0 kB // 透明大页数
// MemAvailable估算公式(kernel 3.14+)
MemAvailable = MemFree + Cached + SReclaimable + ...
- Watermark[low] - lowmem_reserve
9.2 性能诊断工具
// vmstat - 内存/页错误/IO概览
$ vmstat 1 5
procpmemory_____________swap_____io___system_______cpu____
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 2048000 256000 4096000 0 0 32 8 120 450 5 2 92 1 0
// si/so: swap in/out(应接近0)
// bi/bi: block in/out
// pmap - 进程内存映射详情
$ pmap -x $$
Address Kbytes RSS Dirty Mode
000055c3d8a6d000 12 12 0 r-- bash
000055c3d8a6e000 92 92 0 r-x bash
000055c3d8a7a000 28 28 0 r-- bash
000055c3d8a7d000 4 4 4 rw- bash
000055c3d8a7e000 36 36 36 rw- [heap]
// RSS=实际驻留内存, Dirty=脏页
// sar - 历史趋势
$ sar -r 1 5
10:00:01 kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
10:00:02 2048000 14336000 87.50 256000 4096000 9876543 60.25 8192000 2048000 512
// /proc/pid/smaps - 精细映射信息
$ cat /proc/$$/smaps_rollup
00400000-7fff0000 ---p 00000000 00:00 0 [stack]
Size: 838860 kB
Rss: 12345 kB
Pss: 12345 kB // Proportional Set Size(共享页按比例分摊)
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 512 kB
Private_Dirty: 11833 kB
Referenced: 12345 kB
Anonymous: 11833 kB
9.3 eBPF视角的内存分析
// 使用bcc工具观测页分配延迟
$ /usr/share/bcc/tools/funclatency mm_page_alloc
nsecs : count distribution
0 -> 1 : 0 | |
2 -> 3 : 0 | |
4 -> 7 : 0 | |
8 -> 15 : 12 |*** |
16 -> 31 : 45 |************* |
32 -> 63 : 123 |************************************* |
64 -> 127 : 89 |************************ |
128 -> 255 : 34 |********** |
256 -> 511 : 8 |** |
// 如果大量分配超过1μs,可能发生了direct reclaim
// 跟踪TLB miss
$ perf stat -e dTLB-load-misses,dTLB-loads ./workload
// 计算:dTLB miss ratio = misses / loads
// 高命中率(>10%)时考虑大页
十、调优实践总结
// 服务器场景通用调优参数(/etc/sysctl.conf)
vm.swappiness = 10 // 仅在必要时swap
vm.dirty_ratio = 40 // 脏页最大比例(触发同步写回)
vm.dirty_background_ratio = 10 // 后台异步写回起始比例
vm.dirty_expire_centisecs = 3000 // 脏页过期时间(30s)
vm.dirty_writeback_centisecs = 500// kwbflush周期(5s)
vm.min_free_kbytes = 209712 // 保证200MB最低保留(16GB内存机器)
vm.vfs_cache_pressure = 50 // 目录/inode缓存回收倾向(默认100)
vm.overcommit_memory = 0 // 允许合理overcommit(1=永远允许,2=严格)
vm.overcommit_ratio = 50 // 可超分物理+swap的%
// 数据库专用优化
vm.swappiness = 1
# echo never > /sys/kernel/mm/transparent_hugepage/enabled
nr_hugepages = 1024 // 为大内存数据库预留2GB大页
vm.zone_reclaim_mode = 0 // 禁用NUMA本地回收(避免本地节点抖动)
// 高并发网络服务
vm.max_map_count = 262144 // 允许大量线程的mmap
net.core.rmem_max = 16777216 // socket缓冲区
net.ipv4.tcp_rmem = 4096 131072 16777216
总结
Linux内存管理是一个精密而复杂的系统工程。从虚拟地址到物理页框的映射,到SLAB分配器的高效管理,再到LRU驱动的页面回收,每个子系统都经过数十年的演进优化。理解这些底层机制,是进行系统级性能调优和故障排查的基础。
关键要点:
- 多级页表是虚拟内存的基石,TLB性能直接影响内存密集型应用
- 伙伴系统 + SLUB分配器构成物理内存管理的双层架构
- LRU双链表设计在内存压力和应用性能间取得平衡
- 大页(THP/HugePages)是减少TLB Miss的有效手段,但需场景适配
- NUMA感知策略在多路服务器上至关重要
- eBPF工具链让内存行为的实时观测成为可能
随着持久内存(PMEM)、CXL互联内存等新技术的发展,Linux内存管理仍在持续演进,掌握底层原理将帮助开发者更好地驾驭未来架构。

发表评论 取消回复