3. 内存分配路径分析:alloc_pages、kmalloc、vmalloc、ioremap 等关键 API 调用链
内存分配故障排查的核心在于理解不同分配路径的底层实现差异。本章深入分析 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 分配器的三层缓存结构:
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 在内核内存管理中的层级关系:
3.7 实战排查案例:从调用链定位 OOM 根因
假设系统出现 OOM 告警,通过以下步骤利用调用链知识定位:
- 检查 OOM 日志:
dmesg | grep -i "oom"查看 OOM killer 输出的调用栈,确定触发分配的 API。 - 分析
/proc/buddyinfo: 如果所有 order 的可用页数都很少,说明是全局物理内存不足;如果低 order 充足但高 order 不足,说明是碎片问题。 - 检查
/proc/zoneinfo: 查看min、low、highwatermark 和nr_free_pages,判断是否因 watermark 触发直接回收。 - 分析
/proc/slabinfo: 如果 OOM 发生在kmalloc路径,检查对应缓存的对象数量和内存占用。 - 检查
/proc/vmallocinfo: 如果 OOM 发生在vmalloc路径,查看虚拟地址空间使用率和碎片程度。
案例: 某系统频繁触发 OOM,但
/proc/buddyinfo 显示仍有大量空闲页。进一步分析发现 /proc/vmallocinfo 中 vmap_area 数量超过 10 万,虚拟地址空间碎片化严重。最终定位为驱动模块反复调用 vmalloc 和 vfree 导致虚拟地址空间泄漏,而非物理内存不足。
通过深入理解四个关键 API 的调用链,工程师可以快速定位内存分配故障的根因,避免在错误的方向上浪费排查时间。