1. 内存管理基础

内存管理是操作系统与内核驱动开发的基石。在深入内存泄漏排查之前,必须清晰理解内核如何组织、分配和回收内存。本章将系统梳理物理内存与虚拟内存的核心概念、页框与页表的映射机制,以及两大核心分配器——伙伴系统与slab分配器的设计原理。

1.1 物理内存与虚拟内存

物理内存是硬件实际提供的RAM芯片,而虚拟内存是操作系统为每个进程提供的抽象地址空间。内核通过内存管理单元(MMU)将虚拟地址转换为物理地址,实现进程隔离、地址空间扩展和内存保护。

核心要点: 驱动开发中,你操作的是虚拟地址(如 kmalloc 返回的指针),但最终数据必须落在物理页框上。理解这一层映射是调试内存问题的前提。
概念 定义 驱动开发中的意义
物理内存 硬件RAM,以字节为单位,地址连续 DMA操作、页框分配、硬件寄存器访问
虚拟内存 进程/内核看到的线性地址空间 kmalloc/vmalloc返回的地址,通过页表映射
MMU 硬件单元,执行地址转换 缺页异常、TLB刷新影响驱动性能
地址空间 用户空间(0~3G) + 内核空间(3G~4G) 驱动运行在内核空间,需注意地址合法性
虚拟地址 → 物理地址转换流程 虚拟地址空间 进程A: 0x0000~0xBFFFFFFF 内核: 0xC0000000~0xFFFFFFFF MMU 页表 页目录 → 页表项 虚拟页号 → 物理页框号 转换 物理地址空间 页框0, 页框1, ... 实际RAM地址 💡 驱动开发者视角 驱动中使用的指针(如 devm_kzalloc 返回值)是虚拟地址。当进行 DMA 或访问硬件寄存器时, 需要调用 dma_map_single() 等接口获取物理地址,或使用 ioremap() 映射物理寄存器。 进程访问虚拟地址 MMU查页表 访问物理页框

1.2 页框与页表

物理内存被划分为固定大小的页框(Page Frame),通常为4KB。虚拟内存同样被划分为相同大小的页(Page)。页表就是虚拟页到物理页框的映射表。

  • 页框(Page Frame):物理内存的最小分配单元,由 struct page 结构体描述,内核维护全局的 mem_map 数组。
  • 页表(Page Table):多级结构(如x86的4级页表),每级表项指向下一级或物理页框。
  • TLB(Translation Lookaside Buffer):MMU内部的页表缓存,驱动中频繁切换地址空间时需注意TLB刷新开销。
调试提示: 使用 cat /proc/$(pid)/maps 查看进程虚拟内存布局;使用 cat /proc/meminfo 查看系统物理内存使用情况。驱动中可通过 __pa() 和 __va() 在虚拟地址与物理地址之间转换。
多级页表结构(x86-64 4级页表) 虚拟地址 (64位) [未使用位] | PML4索引(9位) | 页目录指针索引(9位) | 页目录索引(9位) | 页表索引(9位) | 页内偏移(12位) PML4 (1个表, 512项) 页目录指针 (最多512个表) 页目录 (最多512个表) 页表 (最多512个表) 物理页框 (4KB) struct page 描述 🔍 关键点 每级页表项占用8字节,一个4KB页框可存放512项。4级页表可管理 512⁴ × 4KB = 256TB 地址空间。

1.3 伙伴系统(Buddy System)

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

Order 页框数量 大小 典型用途
0 1 4KB 普通页分配
1 2 8KB 小规模DMA缓冲区
2 4 16KB 网络协议栈
3 8 32KB 文件系统缓存
4 16 64KB 中等DMA缓冲区
9 512 2MB 大页分配(HugeTLB)
10 1024 4MB 内核映像、大块连续内存
⚠️ 驱动开发陷阱: 频繁调用 __get_free_pages(GFP_KERNEL, order) 分配高阶order(order≥3)容易导致内存碎片化,长期运行后可能分配失败。优先使用 slab 分配器或 kmalloc 处理小对象。
伙伴系统分配与合并示例(order=0~3) 初始:order=3 空闲块 (8页) 8页连续空闲块 分配 order=0 (1页):拆分 order=3 → order=2 + order=2 已分配 order=2 空闲块 (4页) 再分配 order=0:拆分 order=2 → order=1 + order=1 已分配 已分配 order=1 空闲块 (2页) 释放两个 order=0:合并为 order=1,再与相邻 order=1 合并为 order=2 合并后:order=2 空闲块 (4页) + order=2 空闲块 (4页) → 恢复 order=3 空闲块 (8页)

1.4 Slab分配器

Slab分配器建立在伙伴系统之上,专门用于管理内核中频繁分配/释放的小对象(如 task_struct、inode、dentry)。它通过缓存预分配的对象来减少伙伴系统的碎片化和分配开销。

  • Slab缓存(kmem_cache):每个缓存管理一种特定大小的对象,如 kmalloc-32、kmalloc-64。
  • Slab节点:每个缓存包含多个slab,每个slab由一个或多个连续页框组成,内部划分为对象槽。
  • 三种状态:slab可以是 满(Full)、部分满(Partial)、空(Empty),分配优先从Partial中取。
  • kmalloc:基于slab分配器的通用接口,驱动中最常用的内存分配函数。
驱动最佳实践: 如果驱动需要频繁分配同一结构体(如网络描述符、USB请求块),应使用 kmem_cache_create() 创建专用缓存,而非直接使用 kmalloc。这能显著减少内存碎片并提升性能。
Slab分配器架构 kmem_cache 对象大小: 64字节 Slab链表 Full (满) Partial (部分满) Empty (空) Partial Slab 内部结构(1个页框 = 4KB,可容纳64个对象) 已用 已用 空闲 空闲 已用 ...... 空闲 空闲 ...... 已用 空闲 已用 空闲 空闲 空闲 分配流程 kmalloc(64, GFP_KERNEL) 从Partial取空闲对象 返回对象指针

1.5 内存分配路径总结

驱动开发中,理解内存分配的全路径有助于定位性能瓶颈和内存泄漏源头。以下表格总结了不同分配接口的底层路径:

驱动接口 底层分配器 分配单位 典型场景
kmalloc(size, flags) Slab分配器 对象(8B~8KB) 通用小内存分配
kzalloc(size, flags) Slab分配器 对象(清零) 需要初始化为0的结构体
kmem_cache_alloc(cache, flags) Slab分配器 固定大小对象 高频分配相同结构体
__get_free_pages(order, flags) 伙伴系统 2^order个页框 大块连续物理内存
vmalloc(size) 伙伴系统 + 页表映射 页框(非连续虚拟地址) 大块虚拟连续内存
dma_alloc_coherent(dev, size, ...) 伙伴系统 + 一致性映射 连续物理页框 DMA缓冲区
学习路径提示: 本章建立的内存管理基础概念(虚拟地址映射、页框、伙伴系统、slab)是后续理解 kmemleak 工作原理的前提。kmemleak 正是通过扫描伙伴系统和 slab 缓存中的对象引用来检测孤立内存块。下一章将深入 kmemleak 的启用与配置。