3. 内存分配路径分析

内存分配故障排查的核心在于理解不同分配路径的底层实现差异。本章深入分析 Linux 内核中四个最关键的分配 API——alloc_pages、kmalloc、vmalloc 和 ioremap——的完整调用链,揭示它们在内核工程中的行为特征、适用场景及故障触发点。

3.1 分配路径全景概览

下表对比了四个 API 的核心属性,帮助建立全局认知:

API 物理连续性 虚拟连续性 分配粒度 最大大小 典型场景
alloc_pages 连续 连续(直接映射) 2^order 页 受 MAX_ORDER 限制(通常 4MB) DMA、页表、内核核心结构
kmalloc 连续 连续(直接映射) 字节(SLUB/SLAB 缓存) 通常 ≤ 8MB(受 kmalloc 缓存限制) 通用小对象、驱动缓冲区
vmalloc 不连续 连续 页 仅受虚拟地址空间限制(通常 128MB~1GB) 大块内存、模块加载、动态映射
ioremap 不连续(物理地址固定) 连续 页 受 I/O 地址空间限制 MMIO、设备寄存器映射
关键洞察: 物理连续性是 OOM 故障的核心分水岭。alloc_pages 和 kmalloc 在内存碎片严重时更容易失败,而 vmalloc 通过页表重映射规避了碎片问题,但引入了 TLB 刷新开销。

3.2 alloc_pages 调用链:伙伴系统的核心路径

alloc_pages 是内核最底层的物理页分配接口,直接操作伙伴系统。其调用链如下:

alloc_pages(gfp_mask, order)
  └─> __alloc_pages_nodemask(gfp_mask, order, preferred_nid, nodemask)
       ├─> get_page_from_freelist()          // 快速路径:从 per-CPU 页框缓存或伙伴系统获取
       │    ├─> rmqueue()                    // 从伙伴系统摘取连续页块
       │    │    ├─> __rmqueue_smallest()    // 从指定 order 的 freelist 获取
       │    │    └─> __rmqueue_fallback()    // 从更高 order 分裂(buddy 分裂)
       │    └─> check_new_pages()            // 校验页结构完整性
       └─> __alloc_pages_slowpath()          // 慢速路径:触发回收、压缩、OOM
            ├─> wake_all_kswapd()            // 唤醒 kswapd 进行页面回收
            ├─> __alloc_pages_direct_compact() // 直接内存压缩
            ├─> __alloc_pages_direct_reclaim() // 直接页面回收
            └─> out_of_memory()              // OOM killer 触发点
                 └─> select_bad_process()    // 选择 victim 进程
故障排查要点: 当 alloc_pages 进入慢速路径时,意味着系统内存压力已经很高。慢速路径中的 out_of_memory() 调用是 OOM 故障的直接入口。通过 /proc/zoneinfo 观察 lowmem_reserve 和 watermark 可以预判分配失败风险。
常见陷阱: __GFP_ATOMIC 标志的分配不允许睡眠,因此无法进入慢速路径。如果原子上下文中的 alloc_pages 在快速路径失败,会直接返回 NULL,不会触发 OOM killer。这是驱动开发中容易忽略的静默失败场景。

3.3 kmalloc 调用链:SLUB 分配器的分层路径

kmalloc 基于 SLUB 分配器实现,其调用链展示了从通用缓存到底层伙伴系统的完整路径:

kmalloc(size, gfp_mask)
  ├─> kmalloc_slab(size)                    // 查找匹配的 kmem_cache(size 对齐到 2^order)
  │    └─> find_kmem_cache(size)            // 从 kmalloc_caches 数组中选择
  └─> slab_alloc(kmem_cache, gfp_mask)      // SLUB 核心分配函数
       ├─> ___slab_alloc()                  // 无锁快速路径
       │    ├─> this_cpu_cmpxchg_double()   // 从 per-CPU freelist 获取对象
       │    └─> new_slab_objects()          // 从 partial 或 new slab 获取
       └─> __slab_alloc()                   // 慢速路径(需要锁)
            ├─> get_partial_node()          // 从 partial 链表获取 slab
            ├─> new_slab()                  // 分配新 slab(调用 alloc_pages)
            │    └─> allocate_slab()        // 内部调用 alloc_pages_node()
            └─> flush_all()                 // 刷新 per-CPU 缓存到 partial 链表
性能特征: kmalloc 的快速路径(per-CPU freelist 命中)仅需几十条指令,是内核中最高效的分配路径。当 per-CPU 缓存耗尽时,路径会退化为需要锁保护的 partial 链表操作,延迟增加约 10 倍。

下图展示了 SLUB 分配器的三层缓存结构:

SLUB 分配器三层缓存结构 L1: per-CPU Freelist(无锁快速路径) 每个 CPU 维护一个对象链表,使用 cmpxchg 原子操作 命中率 > 90%,延迟约 10-20 纳秒 L2: Partial Slab 链表(加锁路径) 包含部分空闲对象的 slab,需要获取 node 锁 命中率约 8-9%,延迟约 100-200 纳秒 L3: alloc_pages 伙伴系统(慢速路径) 分配新 slab 时调用 alloc_pages,可能触发回收/压缩/OOM 命中率 < 1%,延迟 > 1 微秒,可能进入睡眠 故障排查重点:当大量分配进入 L3 时,系统内存压力已非常严重,应检查 SLUB 缓存使用情况

3.4 vmalloc 调用链:非连续内存映射路径

vmalloc 通过页表重映射实现虚拟地址连续而物理地址不连续的内存区域。其调用链涉及虚拟地址空间管理和页表操作:

vmalloc(size)
  └─> __vmalloc_node_range(size, align, start, end, gfp_mask, prot, node, caller)
       ├─> __get_vm_area_node()             // 在 vmalloc 虚拟地址空间分配区域
       │    ├─> alloc_vmap_area()           // 从 vmap_area 红黑树查找空闲区间
       │    │    ├─> vmap_area_root.rb_root // 红黑树查找
       │    │    └─> free_vmap_area_list    // 空闲链表(加速小区域分配)
       │    └─> setup_vmalloc_vm()          // 初始化 vm_struct 结构
       ├─> __vmalloc_area_node()            // 分配物理页并建立映射
       │    ├─> alloc_pages_bulk_array()    // 批量分配物理页(可能不连续)
       │    │    └─> __alloc_pages_nodemask() // 每个页单独分配
       │    └─> map_vm_area()               // 建立页表映射
       │         ├─> vmap_pages_range()     // 遍历虚拟地址范围
       │         │    └─> vmap_pfn()        // 设置页表项(PTE/PMD/PUD)
       │         └─> flush_cache_vmap()     // 刷新 TLB(架构相关)
       └─> clear_page()                     // 清零分配的内存(除非指定 __GFP_ZERO)
故障排查要点: vmalloc 失败通常不是因为物理内存不足,而是虚拟地址空间耗尽。通过 /proc/vmallocinfo 可以查看所有 vmalloc 区域的使用情况。当 vmap_area 红黑树中碎片化严重时,即使总空闲空间足够,也可能因找不到足够大的连续虚拟地址区间而失败。

3.5 ioremap 调用链:设备 I/O 映射路径

ioremap 用于将设备物理地址空间映射到内核虚拟地址空间,其调用链与 vmalloc 共享部分虚拟地址管理逻辑,但物理地址来源不同:

ioremap(phys_addr, size)
  └─> ioremap_prot(phys_addr, size, prot)  // prot 通常为 PAGE_KERNEL_IO
       └─> ioremap_pte_range()             // 架构相关实现(x86/ARM/ARM64 不同)
            ├─> get_vm_area_caller()        // 从 ioremap 虚拟地址空间分配区域
            │    └─> __get_vm_area_node()   // 与 vmalloc 共享 vmap_area 管理
            ├─> ioremap_page_range()        // 建立页表映射
            │    ├─> ioremap_pud_range()    // 逐级建立 PUD/PMD/PTE
            │    │    └─> ioremap_pte_range()
            │    │         └─> pte_alloc_kernel()  // 分配页表页
            │    └─> flush_tlb_kernel_range()      // 刷新 TLB
            └─> set_memory_decrypted()     // 如果设备需要解密(如 SME/TEE 场景)
关键区别: ioremap 不分配物理内存,只建立页表映射。其故障模式主要是虚拟地址空间耗尽或页表页分配失败。在嵌入式系统中,ioremap 的虚拟地址空间通常较小(如 2MB~16MB),容易成为瓶颈。

3.6 分配路径对比与故障模式总结

下表总结了四个 API 在故障排查中的关键差异:

故障维度 alloc_pages kmalloc vmalloc ioremap
主要失败原因 物理内存碎片、watermark 不足 SLUB 缓存耗尽、伙伴系统失败 虚拟地址空间碎片、物理页分配失败 虚拟地址空间耗尽、页表页分配失败
OOM 触发 是(慢速路径) 是(通过 alloc_pages) 是(通过 alloc_pages) 否(不分配物理页)
诊断文件 /proc/buddyinfo
/proc/zoneinfo
/proc/slabinfo
/sys/kernel/slab/
/proc/vmallocinfo
/proc/meminfo
/proc/iomem
/proc/vmallocinfo
典型错误码 NULL 指针 NULL 指针 NULL 指针 NULL 指针
碎片敏感度 高 中(SLUB 内部管理) 低(物理不连续) 低
排查优先级: 当遇到内存分配失败时,首先通过 /proc/buddyinfo 检查物理碎片程度;如果碎片严重,优先排查 alloc_pages 和 kmalloc 路径;如果 buddyinfo 显示空闲页充足,则转向 /proc/vmallocinfo 检查虚拟地址空间使用情况。

下图展示了四个 API 在内核内存管理中的层级关系:

内核内存分配 API 层级关系 用户态接口:malloc / mmap / brk 内核通用分配 API:kmalloc / vmalloc / ioremap SLUB 分配器 vmap 虚拟地址管理 ioremap 页表映射 伙伴系统(Buddy System):alloc_pages / free_pages 物理内存页(Page Frame) 故障排查路径:从顶层 API 向下追踪,定位失败发生在哪个层级

3.7 实战排查案例:从调用链定位 OOM 根因

假设系统出现 OOM 告警,通过以下步骤利用调用链知识定位:

  1. 检查 OOM 日志: dmesg | grep -i "oom" 查看 OOM killer 输出的调用栈,确定触发分配的 API。
  2. 分析 /proc/buddyinfo: 如果所有 order 的可用页数都很少,说明是全局物理内存不足;如果低 order 充足但高 order 不足,说明是碎片问题。
  3. 检查 /proc/zoneinfo: 查看 min、low、high watermark 和 nr_free_pages,判断是否因 watermark 触发直接回收。
  4. 分析 /proc/slabinfo: 如果 OOM 发生在 kmalloc 路径,检查对应缓存的对象数量和内存占用。
  5. 检查 /proc/vmallocinfo: 如果 OOM 发生在 vmalloc 路径,查看虚拟地址空间使用率和碎片程度。
案例: 某系统频繁触发 OOM,但 /proc/buddyinfo 显示仍有大量空闲页。进一步分析发现 /proc/vmallocinfo 中 vmap_area 数量超过 10 万,虚拟地址空间碎片化严重。最终定位为驱动模块反复调用 vmalloc 和 vfree 导致虚拟地址空间泄漏,而非物理内存不足。

通过深入理解四个关键 API 的调用链,工程师可以快速定位内存分配故障的根因,避免在错误的方向上浪费排查时间。