Linux 内核内存管理深度实战:从伙伴系统到 Slab 分配器的完全工程指南

Linux 内核内存管理是操作系统最底层的基石之一,从物理页面的分配与回收,到虚拟地址空间的映射与管理,再到内核态高效的 Slab/Slob/Slub 分配器设计,每一层都蕴含着精妙的工程智慧。本文将带你深入 Linux 内存管理的核心子系统,从原理到生产实践,构建完整的知识体系。

一、物理内存管理:伙伴系统(Buddy System)

1.1 物理页面(Page Frame)与内存节点

Linux 将物理内存组织为 NUMA 节点 → Zone → Page Frame 的三层结构:

struct pglist_data (每个NUMA节点)
├── struct zone ZONE_DMA / ZONE_NORMAL / ZONE_HIGHMEM
│   ├── free_area[MAX_ORDER]   // 伙伴系统的空闲块链表
│   ├── pageset per_cpu        // per-CPU 热页缓存
│   └── watermark[WMARK_MIN/LOW/HIGH]  // 水位线
└── node_mem_map               // 全局 page 数组

每个物理页面对应一个 struct page 结构体(约 64 字节),它在 mem_map 数组中按页帧号(PFN)索引,管理着页面引用计数、映射指针、lru 链表指针等核心元数据。

1.2 伙伴系统核心算法

伙伴系统解决的是 外部碎片 问题,将空闲页面按 2^n 大小分组管理:

// 分配 2^order 个空闲页面
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);

// 核心分配流程(简化版)
alloc_pages()
  └── __alloc_pages_nodemask()
      ├── alloc_pages_slowpath()    // 慢路径:回收/压缩
      │   └── __alloc_pages_direct_reclaim()
      │       └── try_to_free_pages()  // 扫描 LRU 链表
      └── get_page_from_freelist()  // 快速路径:从 freelist 取
          └── __rmqueue()
              └── rmqueue_bulk()     // 从 free_area 链表移除块

伙伴系统的名称来源于:当一个空闲块被等分为两半时,这两半被称为"伙伴"(buddy)。释放时,系统会检查其伙伴是否空闲,若空闲则合并(coalesce)成更大的块,递归向上合并直到无法合并或达到最大阶。

1.3 页面分配器(Page Allocator)与 GFP 标志

GFP(Get Free Pages)标志控制页面分配的行为:

类别标志含义
区域修饰符__GFP_DMADMA 区域内存(物理地址<16MB>
区域修饰符__GFP_HIGHMEM高内存区域(32位系统 >896MB)
区域修饰符__GFP_MOVABLE可移动页面
行为修饰符__GFP_WAIT允许等待(可睡眠)
行为修饰符__GFP_HIGH高优先级,可访问紧急预留
行为修饰符__GFP_IO允许启动 I/O 回收
行为修饰符__GFP_FS允许调用文件系统回收
行为修饰符__GFP_ZERO分配后清零页面
类型GFP_KERNEL内核标准分配(可睡眠)
类型GFP_ATOMIC原子分配(不可睡眠)
类型GFP_NOWAIT绝不等待,失败直接返回

1.4 水位线机制与内存回收触发

伙伴系统通过三条水位线控制内存紧张度:

水位线层级:
  FREE_PAGES_HIGH  ─┐  // 充裕水位,kswapd 停止回收
                   │
  FREE_PAGES_LOW   ─┤  // 警戒水位,唤醒 kswapd 开始回收
                   │
  FREE_PAGES_MIN   ─┘  // 紧急水位,同步回收 + OOM 风险

触发链:
  page allocation < LOW>

二、Slab 分配器:内核对象的高效缓存

2.1 Slab 分配器架构演进

Slab 分配器解决的是内部碎片和对象分配初始化开销两大问题。Linux 内核目前支持三种 Slab 分配器实现:

  • Slab:最早的实现,Linus Torvalds 设计
  • Slob:针对嵌入式设备的极简实现
  • Slub:默认分配器,调试支持强,性能最优

Slab 的核心思想是对象缓存:将频繁分配/释放的对象保持初始化状态,避免重复执行构造函数。典型的 Slab 缓存大小从 32 字节到 8KB 不等。

2.2 Slab 缓存结构

struct kmem_cache
├── slab_cache 缓存本身(元数据)
├── slab_partial  // 部分空闲 slab 链表
├── slab_full    // 满 slab 链表
├── slab_free    // 完全空闲 slab 链表(Slub)
├── object_size  // 单个对象大小
├── size         // 包含元数据的对象大小
├── offset       // 下一个空闲对象的指针偏移
├── oo           // 最佳 max/min order / objects-per-slab
└── name         // 缓存名称(/proc/slabinfo 中可见)

struct slab(Slub 结构)
├── freelist     // 空闲对象链表
├── inuse        // 已使用对象计数
├── objects      // 总对象数
└── node[0]      // 所属 node 的 slab 列表节点

2.3 Slab 分配与释放流程

// 创建 Slab 缓存
struct kmem_cache *kmem_cache_create(const char *name, size_t size,
                                     unsigned long flags, void (*ctor)(void *));

// 从缓存分配对象
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);

// 释放对象回缓存
void kmem_cache_free(struct kmem_cache *cachep, void *objp);

// 通用分配(小对象走 Slab,大对象走 Page Allocator)
void *kmalloc(size_t size, gfp_t flags);
void kfree(const void *objp);
void *vmalloc(size_t size);   // 虚拟地址连续、物理不连续
void vfree(const void *addr);

2.4 Slab 与伙伴系统的接口

Slab 分配器在内部消费伙伴系统的页面:每个 Slab 通常占用连续的 2^order 个物理页面。当 Slab 分配对象需要新的物理内存时,调用 alloc_pages();当 Slab 中所有对象都被释放时,将页面归还给伙伴系统。

三、虚拟内存管理:地址空间与页表

3.1 进程地址空间(mm_struct)

每个进程拥有独立的虚拟地址空间(64 位系统下为 128TB 用户态 + 128TB 内核态):

struct mm_struct
├── mmap           // VMA 红黑链表(vm_area_struct)
├── mm_rb          // VMA 红黑树根
├── pgd            // 顶级页表指针(PGD)
├── start_code/end_code    // 代码段范围
├── start_data/end_data    // 数据段范围
├── start_brk/brk          // 堆区域
├── start_stack            // 栈顶
├── arg_start/arg_end      // 命令行参数
├── env_start/env_end      // 环境变量
├── map_count              // VMA 数量
├── total_vm               // 总映射页数
├── locked_vm / pinned_vm  // 锁定/钉住的页数
└── rss_stat               // 驻留页面统计

3.2 虚拟内存区域(VMA)结构

struct vm_area_struct
├── vm_start          // VMA 起始地址
├── vm_end            // VMA 结束地址(不包含)
├── vm_flags          // VM_READ/WRITE/EXEC/SHARED 等
├── vm_page_prot      // 页面保护位
├── vm_next/vm_prev   // VMA 链表
├── vm_rb             // VMA 红黑树节点
├── vm_file           // 映射的文件(文件映射时非空)
├── vm_pgoff          // 文件偏移(以页为单位)
└── vm_ops            // 缺页处理等操作函数集

3.3 多级页表实现

Linux 5.x 使用五级页表(57 位虚拟地址,256TB):

// x86_64 五级页表翻译流程
CR3 ──────┐
          ├── PGD (Page Global Directory)   9 bits
          ├── P4D (Page 4th Directory)      9 bits
          ├── PUD (Page Upper Directory)    9 bits
          ├── PMD (Page Middle Directory)   9 bits
          ├── PTE (Page Table Entry)        9 bits
          └── Offset                        12 bits

// TLB 翻译缓存效果:
// 1 次 TLB 命中 = 1 次内存访问
// 1 次 TLB miss(5级)= 6 次内存访问(页表遍历)+ 1 次目标访问

3.4 缺页异常处理流程

do_page_fault(struct pt_regs *regs, unsigned long error_code)
├── 查找 VMA:find_vma(mm, address)
│   ├── VMA 不存在 → SIGSEGV(段错误)
│   ├── VMA 存在但权限不足 → SIGSEGV
│   └── VMA 存在且权限合法 → 继续处理
├── handle_mm_fault()
│   ├── VM fault(匿名映射)
│   │   └── do_anonymous_page():分配物理页面并建立映射
│   ├── File-backed fault(文件映射)
│   │   └── do_fault()
│   │       ├── do_read_fault():文件预读 + 页面缓存查找
│   │       ├── do_cow_fault():写时复制
│   │       └── do_shared_fault():共享写映射
│   └── p4d_alloc/pud_alloc/pmd_alloc:页表项不存在时动态创建
└── 返回用户态,重新执行触发缺页的指令

四、页面回收与交换:KSwapD 与 LRU 算法

4.1 双重 LRU 链表机制

Linux 使用 Active/Inactive 双重 LRU 链表来更精确地区分页面热度:

每个 Zone 的 LRU 链表(LRU_BASE):
├── LRU_ACTIVE_ANON    // 活跃匿名页(最近访问过)
├── LRU_INACTIVE_ANON  // 非活跃匿名页(候选回收)
├── LRU_ACTIVE_FILE    // 活跃文件页(最近访问过)
├── LRU_INACTIVE_FILE  // 非活跃文件页(候选回收)
├── LRU_UNEVICTABLE    // 不可回收页(mlocked/device映射)
└── 辅助:zone->lruvec(per-zone的 LRU 管理上下文)

页面热度变化:
  访问 → mark_page_accessed() → 从 Inactive 移到 Active
  时间衰减 → shrink_lruvec() → 从 Active 移到 Inactive
  回收 → shrink_page_list() → 写出磁盘 / 丢弃 → 释放

4.2 页面老化算法(Refault Distance Tracking)

Linux 内核 5.6+ 引入了 Workingset 跟踪机制,通过记录页面从 Inactive 被回收后在不久后又被访问的情况(refault),动态调整匿名页和文件页之间的回收比例,防止一次性扫描流将工作集页面挤出内存。

4.3 Swap 机制

Swap 将匿名页面写入磁盘空间(分区/文件),释放出的物理页面可重新分配:

Swap 核心数据结构:
struct swap_info_struct
├── flags              // SWP_USED / SWP_WRITEOK
├── swap_file          // 指向文件或分区
├── bdev               // 块设备上下文
├── swap_map           // 到 page slot 的映射数组
├── prio               // 优先级(优先使用高优先级 swap)
├── pages              // 总可用 slot 数
├── max                // 最大 slot 数
└── inuse_pages        // 已使用 slot 数

Swap 读写路径:
  swap_out(): 脏匿名页 → pageout() → writepage() → 写入磁盘 → 释放 page
  swap_in():  缺页异常 → do_swap_page() → swapin_readahead() → 从磁盘读回

五、内存碎片整理与反碎片化

5.1 碎片类型与成因

伙伴系统的分配策略虽然解决了部分外部碎片问题,但长时间运行后仍会产生不可移动页面造成的空洞。Linux 通过 页面迁移类型分组(pageblock migration type)来对抗碎片:

迁移类型可分配性典型用户
MIGRATE_UNMOVABLE不可移动内核数据结构、页表、内核栈
MIGRATE_MOVABLE可移动用户空间匿名页、文件缓存
MIGRATE_RECLAIMABLE不可移动但可回收inode cache、dentry cache
MIGRATE_PCPper-CPU 热页缓存临时高频分配
MIGRATE_ISOLATE完全隔离CMA 区域

5.2 内存压缩(Memory Compaction)

当碎片导致无法满足连续的大块分配请求时,kcompactd 内核线程被唤醒,将可移动页面迁移到一起,创造出更大的连续物理块:

compaction 流程:
  kcompactd_run() → kcompactd()
  └── compact_zone_order()
      ├── isolate_migratepages()  // 扫描并隔离可移动页面
      ├── isolate_freepages()     // 扫描并隔离空闲目标区域
      ├── migrate_pages()         // 执行页面迁移(建立反向映射更新)
      └── putback_movable_pages() // 释放失败时回滚

// 触发时机:
alloc_pages() 失败 → direct compaction → failed && order >= pageblock_order → wake kcompactd

六、CMA:连续内存分配器

Contiguous Memory Allocator (CMA) 为需要连续大块物理内存的设备驱动(如 GPU、视频编解码器)提供支持:

CMA 工作流程:
1. 启动时保留一块物理连续区域(mem=@ 或设备树配置)
2. 该区域标记为 MIGRATE_CMA,空闲时可按 MIGRATE_MOVABLE 被使用
3. 设备驱动请求时(dma_alloc_coalesce),将已分配的 movable 页面迁移走
4. 释放页面时归还给伙伴系统而非 CMA 区域

// 核心 API:
struct page *dma_alloc_coherent(struct device *dev, size_t size,
                                dma_addr_t *dma_handle, gfp_t flag);
void dma_free_coherent(struct device *dev, size_t size,
                       void *cpu_addr, dma_addr_t dma_handle);

七、大规模内存调优实践

7.1 NUMA 感知分配策略

在多 NUMA 节点服务器上,内存分配的性能取决于分配发生在本地节点还是远程节点(远程访问延迟通常是本地的 2-3 倍):

// NUMA 策略配置(/sys/devices/system/node/)
// per-node 页面分配策略
/sys/devices/system/node/node*/meminfo
/sys/devices/system/node/node*/numastat

// 查看进程 NUMA 内存分布
numastat -p $PID
numactl --hardware
numactl --interleave=all ./app    // 跨节点交错分配
numactl --cpunodebind=0 --membind=0 ./app  // 绑定本地节点

// 内核 NUMA balancing(Linux 3.8+)
/proc/sys/kernel/numa_balancing = 1
// 自动扫描进程内存,将页面迁移到访问 CPU 所在节点

7.2 HugePage 与透明大页

大页(2MB/1GB)通过减少 TLB miss 和页表遍历开销来提升性能:

HugePage(静态大页):
  预留方式:vm.nr_hugepages = 2048
  挂载:mount -t hugetlbfs ...
  使用:mmap(MAP_HUGETLB)

透明大页(Transparent HugePages,THP):
  /sys/kernel/mm/transparent_hugepage/enabled = madvise/always
  khugepaged 内核线程:扫描匿名区域并合并为 2MB 大页
  数据库场景建议关闭(redis 推荐 never)

巨型页(1GB 页):
  需在内核启动参数配置:hugepagesz=1G hugepages=4
  适用:DPDK 驱动的网卡用户态驱动、大型数据库缓冲池

7.3 内存 OOM 杀手调优

当系统内存耗尽且无法通过回收释放时,OOM killer 选择一个进程杀死以换取系统生存:

// OOM 评分机制
// 每个进程的 OOM 评分计算在 oom_badness() 中:
// score = total_vm * 1000 / totalram_pages * oom_score_adj_range (0~1000)
// 评分最高的进程被选中杀死

// 调整进程被 OOM 选中的概率
echo -1000 > /proc/$PID/oom_score_adj  // 永不选中(关键服务)
echo -1000 > /proc/$PID/oom_adj        // 旧接口
echo 100 > /proc/$PID/oom_score_adj    // 更容易被选中

// 控制 OOM 行为
/proc/sys/vm/overcommit_memory = 0     // 启发式过量分配
/proc/sys/vm/overcommit_memory = 1     // 总是允许(风险高)
/proc/sys/vm/overcommit_memory = 2     // 严格不允许过量分配
/proc/sys/vm/panic_on_oom = 2          // OOM 时触发 kernel panic

八、调试与性能监控工具

生产环境中排查内存问题需要系统化的工具链:

工具/接口用途
/proc/meminfo内核内存使用概况
/proc/buddyinfo伙伴系统各阶空闲块数量
/proc/slabinfoSlab 缓存详细使用情况
/proc/$PID/smaps进程详细 VMA 内存映射
/proc/pagetypeinfo按迁移类型分布的内存使用情况
free -h快速查看用户态可见内存
vmstat -s内存统计信息
drop_caches清理页面缓存(3=全部,1=dentry+inode)
perf stat -e dtlb_load_missesTLB miss 监控
bpftrace -e 'kprobe:oom_kill_process { ... }'OOM 触发追踪
slabtopSlab 实时使用监控(类似 top)
numastatNUMA 节点访问分布

8.1 内存泄漏检测工具链

// slabinfo 追踪:增长不降的缓存即疑似泄漏
watch -n 1 'cat /proc/slabinfo | sort -k3 -n -r | head -20'

// ftrace + kprobe 追踪 kmalloc/kfree 不匹配
echo 'p:kmalloc_Entry kmalloc size=$arg2' > /sys/kernel/debug/tracing/kprobe_events
echo 'r:kmalloc_Exit kmalloc retval=$retval' > /sys/kernel/debug/tracing/kprobe_events

// eBPF 实时内存追踪(bcc-tools)
# /usr/share/bcc/tools/memleak -p $PID
# /usr/share/bcc/tools/kmemleak
# /usr/share/bcc/tools/shmsnoop -p $PID

九、最新发展趋势

Linux 内存管理子系统的持续演进:

  • MGLRU(Multi-Gen LRU):Linux 6.1+ 引入,通过多代跟踪(最多 4 代)替代 Active/Inactive 二元链表,大幅提升扫描效率和 refault 保护
  • Damon(Data Access Monitor):基于区域的精确内存访问监控,为 MGLRU 提供运行时热度数据
  • FOLIO(复合页):Linux 5.16+ 引入,统一 head page 和 tail page 管理,减少 slab/kmem 开销
  • Memory Cgroup v2:容器内存限制的统一控制器,支持 recursive protection(pod→container 级联限制)
  • CXL 2.0/3.0 内存扩展:CXL.mem 协议让 CPU 使用非本地 DRAM/PMEM 的共享内存池,NUMA 距离更长但容量更大
  • PKey(Protection Keys):硬件支持的页面权限控制,允许用户态进程无 sys call 切换内存访问权限

十、总结

Linux 内核内存管理是一个精密协作的子系统:伙伴系统管理物理页面,Slab 分配器加速频繁对象分配,页表和多级寻址实现虚拟地址空间隔离,LRU 算法和 kswapd 维护页面热度,CMA 满足特殊硬件需求,NUMA 和大页优化满足企业级服务器性能需求。

掌握这些机制不仅是理解 Linux 内核的关键,更是性能调优、驱动开发、容器平台优化、生产排障的共同基础。在 AI 计算、大数据处理、高频交易等低延迟场景中,对内存管理精度的把控往往能带来数量级的性能差异。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部