2. Linux 内存管理架构:内存节点、区域、伙伴系统与 slab 分配器

Linux 内核的内存管理是一个分层架构,从物理内存的拓扑结构到内核对象的细粒度分配,共分为四个核心层级:内存节点 (Node)、内存区域 (Zone)、伙伴系统 (Buddy System) 和 slab 分配器。理解这一架构是驱动开发中进行内存映射与性能调优的基础。

核心观点: 驱动开发者必须理解内存从“物理页”到“内核对象”的完整流转路径。mmap 操作直接与伙伴系统和内存区域交互,而 slab 分配器则影响驱动内部数据结构的分配效率。
Linux 内存管理四层架构图 Linux 内存管理四层架构 内存节点 (Node) NUMA 架构下的物理 CPU 与内存簇 内存区域 (Zone) DMA | DMA32 | Normal | HighMem | Movable 伙伴系统 (Buddy System) 以 2^order 页块为单位管理物理页 slab 分配器 kmem_cache / kmalloc 细粒度对象分配 驱动 mmap 直接操作伙伴系统分配的页;驱动内部数据结构常用 slab 分配。 Zone 决定了页的 DMA 属性与可访问性。

2.1 内存节点 (Node)

在 NUMA (Non-Uniform Memory Access) 架构下,系统被划分为多个内存节点。每个节点与一个或多个 CPU 核心紧密关联,CPU 访问本地节点的内存速度远快于访问远程节点。内核使用 struct pglist_data 结构体描述每个节点。

  • 节点结构: pg_data_t 包含节点内的内存区域列表、伙伴系统信息、页面回收参数等。
  • UMA 系统: 在 UMA 架构中,整个系统只有一个内存节点 (Node 0)。
  • 驱动影响: 驱动在分配大块内存时,应尽量选择与执行 CPU 相同的节点,避免跨节点访问延迟。
驱动提示: 使用 devm_kmalloc_node() 或 alloc_pages_node() 可以指定内存节点,优化 NUMA 性能。

2.2 内存区域 (Zone)

每个内存节点被划分为多个内存区域 (Zone),用于管理具有不同硬件特性的物理内存页。内核使用 struct zone 结构体描述每个区域。

区域名称 宏定义 物理地址范围 (典型 x86_64) 用途与限制
DMA ZONE_DMA 0 - 16 MB 用于 ISA 设备 DMA,地址总线宽度有限。
DMA32 ZONE_DMA32 0 - 4 GB 用于 32 位设备 DMA,x86_64 系统必须。
Normal ZONE_NORMAL 4 GB - 896 MB (旧) / 直接映射区 内核线性映射区,大部分内核操作在此进行。
HighMem ZONE_HIGHMEM 高于 896 MB (32位) 32 位系统访问超过 1GB 内存,64 位系统通常不使用。
Movable ZONE_MOVABLE 可变 用于可迁移页面,支持内存热插拔和碎片整理。
故障排查要点: 驱动调用 dma_alloc_coherent() 时,如果设备不支持 64 位寻址,必须确保从 ZONE_DMA 或 ZONE_DMA32 分配内存。分配失败通常表现为 dma_alloc_coherent 返回 NULL。
内存区域与物理地址映射关系图 x86_64 物理内存区域布局示例 ZONE_DMA (0 - 16 MB) ZONE_DMA32 (16 MB - 4 GB) ZONE_NORMAL (4 GB - 物理内存上限) ZONE_MOVABLE (可选,用于可迁移页) 驱动 mmap 映射物理内存时,需确保目标物理地址落在正确的 Zone 内。 例如:DMA 缓冲区必须位于 ZONE_DMA 或 ZONE_DMA32。

2.3 伙伴系统 (Buddy System)

伙伴系统是内核管理物理页面的核心算法,用于解决外部碎片问题。它将所有空闲页按 2 的幂次方 (order) 组织成链表。

  • 分配单位: 以 2^order 个连续物理页为单位,order 范围 0 ~ MAX_ORDER (通常为 11,即 4MB)。
  • 核心 API:
    • alloc_pages(gfp_mask, order) — 分配 2^order 个连续物理页。
    • __get_free_pages(gfp_mask, order) — 返回逻辑地址。
    • free_pages(addr, order) — 释放页面。
  • 碎片问题: 虽然伙伴系统减少了外部碎片,但长期运行后仍可能出现内存碎片化,导致高阶分配失败。
性能调优指标: 通过 /proc/buddyinfo 可以查看每个 Zone 的空闲页块分布。如果高阶 (order > 3) 块长期为 0,说明内存碎片严重,驱动应避免大块连续分配。
// 示例:查看 /proc/buddyinfo
// Node 0, zone   Normal    235    142     56     21      8      3      1      0      0      0      0
// 各列分别对应 order 0 ~ order 10 的空闲块数量

2.4 slab 分配器

伙伴系统以页为单位分配,对于内核中大量的小对象 (如 struct inode、struct file、驱动私有结构体) 来说粒度太大。slab 分配器建立在伙伴系统之上,提供对象缓存机制。

  • 核心概念:
    • kmem_cache: 针对特定类型对象的缓存池。
    • slab: 由一个或多个连续物理页组成,被划分为多个相同大小的对象。
    • 着色 (Coloring): 通过偏移调整,使不同 slab 中的对象对齐到不同 cache line,减少伪共享。
  • 常用 API:
    • kmem_cache_create() — 创建对象缓存。
    • kmem_cache_alloc() — 从缓存分配对象。
    • kmem_cache_free() — 释放对象回缓存。
    • kmalloc() — 通用分配,底层基于 slab 或 kmem_cache。
驱动最佳实践: 如果驱动频繁分配/释放相同大小的结构体,应使用 kmem_cache_create() 创建专用缓存。这能显著减少分配开销,并提高 CPU 缓存命中率。
slab 分配器结构图 slab 分配器结构图 kmem_cache slab (物理页) slab (物理页) slab (物理页) 对象 1 对象 2 ... 对象 N 每个 slab 占用一个或多个连续物理页 (来自伙伴系统), 被划分为固定大小的对象。kmem_cache 管理一组 slab。

2.5 层级关系与驱动交互总结

驱动开发中,理解这四个层级的关系至关重要。以下表格总结了各层级在驱动开发中的典型交互场景:

层级 驱动典型操作 关键 API / 文件 性能关注点
Node 指定 NUMA 节点分配内存 alloc_pages_node() 跨节点访问延迟
Zone DMA 缓冲区分配 dma_alloc_coherent() Zone 水位线,分配失败回退
Buddy 大块连续物理内存映射 (mmap) alloc_pages(), /proc/buddyinfo 内存碎片化,高阶分配成功率
slab 驱动内部数据结构分配 kmem_cache_alloc(), kmalloc() 对象复用率,cache line 伪共享
故障案例: 某网络驱动频繁使用 kmalloc(64) 分配描述符,未使用专用 slab 缓存。在高并发下,slab 分配器频繁创建新 slab 页,导致 /proc/buddyinfo 中 order-0 页块急剧减少,最终引发伙伴系统分配失败。解决方案:创建 kmem_cache 并设置 SLAB_HWCACHE_ALIGN 标志。