2. Linux 内存管理简介:虚拟内存、物理内存、页表、伙伴系统、slab分配器

在深入内存调试与 kmemleak 泄漏检测之前,必须理解 Linux 内核如何管理内存。本章将拆解从虚拟地址到物理页框的完整路径,并介绍内核分配内存的两大核心机制:伙伴系统与 slab 分配器。这些知识是理解内存泄漏根源的基础。

2.1 虚拟内存与物理内存

Linux 为每个进程提供独立的 虚拟地址空间(Virtual Address Space),使进程误以为自己独占整个内存。虚拟地址通过硬件 MMU(内存管理单元)和内核页表转换为真实的 物理地址(Physical Address)。

概念 说明 典型大小(64位)
虚拟内存 进程看到的线性地址空间,通过页表映射到物理内存或磁盘交换区 128 TB(用户空间) + 128 TB(内核空间)
物理内存 硬件 RAM 的实际字节单元,内核以页框(Page Frame)为单位管理 取决于硬件安装容量
页框 物理内存的最小分配单元,通常为 4 KB 4 KB(标准页) / 2 MB(大页)
调试要点: kmemleak 扫描的是内核对象的虚拟地址,但最终泄漏的是物理页框。理解虚拟到物理的映射,有助于定位泄漏对象所在的物理区域。
虚拟地址 → 物理地址转换流程 进程虚拟地址空间 0x0000… ~ 0x7FFF… MMU 多级页表 PGD → PUD → PMD → PTE 查表 物理页框 PFN → 页框号 关键路径说明: 1. CPU 发出虚拟地址(VA) 2. MMU 遍历多级页表,找到页表项(PTE) 3. PTE 包含物理页框号(PFN)和权限位 4. 组合 PFN + 页内偏移得到物理地址(PA)

2.2 页表与地址转换

Linux 在 x86-64 架构下使用 4 级页表(PGD → PUD → PMD → PTE)。每个进程拥有自己的页表,内核通过 struct mm_struct 管理。页表项(PTE)不仅包含物理页框号,还包含 Present、Dirty、Accessed 等标志位。

// 页表项结构示意(arch/x86/include/asm/pgtable_types.h)
typedef struct { unsigned long pte; } pteval_t;

// 关键标志位
#define _PAGE_PRESENT   (1 << 0)   // 页在物理内存中
#define _PAGE_RW        (1 << 1)   // 可写
#define _PAGE_ACCESSED  (1 << 5)   // 已访问
#define _PAGE_DIRTY     (1 << 6)   // 已脏
调试关联: 当 kmemleak 报告一个泄漏对象时,可以通过 /proc/self/pagemap 或 pagemap 接口反向查找该虚拟地址对应的物理页框,确认泄漏页是否被意外保留。

2.3 伙伴系统(Buddy System)

伙伴系统是内核管理物理页框的核心算法,用于分配连续的物理内存块。它将空闲页框按 2 的幂次阶(order) 组织成链表,分配时从高阶切分,释放时合并相邻的“伙伴”块。

阶(Order) 连续页框数 大小(4KB 页) 典型用途
0 1 4 KB slab 分配器、小对象
1 2 8 KB 小缓冲区
3 8 32 KB 文件系统元数据
10 1024 4 MB 大块 DMA 缓冲区
伙伴系统分配与合并示意 初始空闲:order=4(16 页,64 KB) 分配 order=2(4 页) 剩余空闲:order=2(4 页)+ order=3(8 页) 释放 order=2 合并为 order=3 最终空闲:order=4(16 页) 伙伴合并条件:两个相邻块大小相同、物理地址连续、且来自同一个高阶块。 内存泄漏时,伙伴系统无法合并碎片,导致大块分配失败。

2.4 slab 分配器

伙伴系统以页框为单位分配,对于内核中大量的小对象(如 task_struct、inode、dentry)效率太低。slab 分配器在伙伴系统之上,为每种对象类型维护一个 缓存(cache),实现对象复用和高速分配。

  • slab 缓存:每个缓存管理一种固定大小的对象,如 kmalloc-32、kmalloc-128。
  • slab 节点:每个缓存包含多个 slab,每个 slab 由一个或多个连续页框组成。
  • 对象着色(coloring):通过偏移调整对象在 slab 内的起始位置,优化 CPU 缓存行对齐。
// 查看 slab 缓存信息(/proc/slabinfo)
# cat /proc/slabinfo | head -5
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> ...
kmalloc-256         1024   2048    256    16     1
task_struct          512    512   3456     4     4
inode_cache         2048   4096    640    12     2
泄漏高发区: slab 缓存是 kmemleak 的重点监控对象。如果驱动或内核模块分配了 kmalloc 对象但未释放,该对象所在的 slab 缓存会持续增长,最终导致内存耗尽。通过 /proc/slabinfo 观察异常增长的缓存,可以快速定位泄漏源。
slab 分配器三层架构 slab 缓存(cache):kmalloc-128 / task_struct_cache / inode_cache slab 节点 0(部分满) slab 节点 1(满) slab 节点 2(空闲) 伙伴系统页框(order=0) 伙伴系统页框(order=0) 伙伴系统页框(order=0) 每个 slab 节点包含 1 个或多个连续页框,对象在 slab 内连续排列,通过空闲链表管理。

2.5 内存管理路径总结

从进程申请内存到最终物理页框的完整路径如下:

  1. 用户态:malloc() → brk() / mmap() → 内核虚拟内存区域(VMA)
  2. 内核态:kmalloc() / vmalloc() → slab 分配器 / 伙伴系统
  3. 缺页异常:访问未映射页 → 内核分配物理页框 → 填写页表项
  4. 释放:kfree() → slab 回收 → 伙伴系统合并 → 物理页框归还
实战关联: kmemleak 在步骤 2 和步骤 4 之间进行扫描。如果 kmalloc 分配的对象指针丢失(没有 kfree 且没有其他引用),kmemleak 会将其标记为“泄漏”。理解 slab 和伙伴系统的层级关系,有助于判断泄漏对象属于哪个缓存、占用多少物理页框。