3. slab分配器:slab/slub/slob对比、kmalloc与kfree、内存池mempool

3.1 为什么需要slab分配器?

讲DMA配置之前,我得先聊聊内核里最常用的内存分配器——slab。你可能会问:内核不是已经有页分配器(buddy system)了吗?为什么还要搞个slab?

原因其实很简单。内核里大量分配的是小对象,比如进程描述符、inode、socket结构体。这些对象通常只有几十到几百字节。如果用页分配器去分配,一个4KB的页只放一个对象,那浪费太严重了。而且频繁的分配释放会导致内存碎片。

slab分配器就是专门解决这个问题的。它把内存划分成一个个"缓存池",每个池子专门存放固定大小的对象。分配和释放都很快,而且不会产生外部碎片。

核心思想:slab分配器是一种"对象缓存"机制。它预先把内存划分成相同大小的对象槽位,分配时直接拿一个槽位,释放时还回去。避免了频繁的页分配操作。

3.2 slab / slub / slob 三兄弟对比

内核里其实有三个分配器实现:slab、slub、slob。它们功能一样,但设计哲学和适用场景不同。我当年刚接触内核时也被这三个名字搞晕过,后来在项目中逐个用过,才算真正理解它们的区别。

特性 slab slub slob
设计年代 最早(1994年) 较新(2007年) 最简(2005年)
代码复杂度
内存开销 较大(每个CPU维护队列) 较小 极小
分配速度 快(有per-CPU缓存) 快(简化了per-CPU逻辑) 慢(链表遍历)
适用场景 大型SMP系统 大多数通用场景(默认) 嵌入式/小内存系统
调试能力 强(有red zone检测)

我个人习惯:在嵌入式Linux开发中,如果内存小于64MB,我会选slob。如果是通用服务器或桌面系统,用slub就对了。slab现在用得少了,除非你在维护老内核或者需要强大的调试功能。

避坑指南:我曾经在一个ARM Cortex-A7的项目上默认用了slub,结果发现内存占用比预期高了不少。后来换成slob,内存占用直接降了30%。所以小内存系统一定要考虑slob。

3.3 kmalloc 与 kfree:最常用的分配接口

kmalloc和kfree是驱动开发中最常用的内存分配函数。它们底层就是基于slab分配器实现的。

#include <linux/slab.h>

void *kmalloc(size_t size, gfp_t flags);
void kfree(const void *objp);

flags参数是重点。它决定了分配行为:

  • GFP_KERNEL:最常用。可以睡眠,用于进程上下文。
  • GFP_ATOMIC:不会睡眠。用于中断上下文或持有自旋锁时。
  • GFP_DMA:分配DMA可访问的内存(低16MB区域)。
  • GFP_DMA32:分配32位DMA地址空间的内存。

注意:在中断处理函数里千万别用GFP_KERNEL!我见过有人这么写,结果系统直接死锁。中断上下文只能用GFP_ATOMIC。

kmalloc分配的内存物理上是连续的。这对DMA来说很重要,因为DMA控制器通常需要连续的物理地址。不过kmalloc最大只能分配几MB(取决于slab配置),更大的连续内存得用__get_free_pagesdma_alloc_coherent

3.4 内存池 mempool

mempool是什么?说白了就是一个"应急储备池"。它预先分配一批对象放在池子里,当系统内存紧张时,kmalloc可能失败,但mempool保证你至少能从池子里拿到一个对象。

我当年做网络驱动时遇到过一个问题:在内存压力下,skb分配失败导致网络断连。后来用mempool给skb分配加了一层保障,问题就解决了。

#include <linux/mempool.h>

// 创建内存池
mempool_t *mempool_create(int min_nr, 
                          mempool_alloc_t *alloc_fn,
                          mempool_free_t *free_fn,
                          void *pool_data);

// 从池中分配
void *mempool_alloc(mempool_t *pool, gfp_t gfp_mask);

// 释放回池中
void mempool_free(void *element, mempool_t *pool);

// 销毁内存池
void mempool_destroy(mempool_t *pool);

使用场景

  • 中断处理中必须成功分配内存的地方
  • 文件系统元数据分配
  • 网络协议栈的关键路径
  • DMA描述符分配

经验之谈:mempool不是银弹。它只是保证在内存紧张时不会返回NULL,但池子本身也占用内存。我建议只在"分配失败会导致严重问题"的关键路径上使用。普通的数据缓冲区,用kmalloc就够了。

3.5 知识体系结构图

下面这张图展示了slab分配器、kmalloc/kfree、mempool之间的关系:

slab分配器知识体系 驱动/内核模块 kmalloc / kfree mempool dma_alloc_coherent slab分配器(三种实现) slab(大型SMP) slub(通用默认) slob(嵌入式) 伙伴系统(Buddy System)— 页分配器 物理内存页框管理 GFP标志 GFP_KERNEL GFP_ATOMIC GFP_DMA GFP_DMA32 __GFP_HIGH

3.6 实际项目中的选择建议

说了这么多理论,最后给点实战建议:

  1. 通用场景:直接用slub,内核默认就是它。kmalloc/kfree随便用。
  2. 小内存嵌入式:换成slob。记得在内核配置里选CONFIG_SLOB。
  3. DMA缓冲区:用dma_alloc_coherent,别自己用kmalloc加GFP_DMA。后者在有些架构上不靠谱。
  4. 关键路径:考虑mempool。但池子大小别设太大,够用就行。
  5. 调试阶段:打开slub调试(CONFIG_SLUB_DEBUG),能帮你抓到内存越界和释放后使用的问题。

小技巧:想知道当前系统用的是什么分配器?看看/proc/slabinfo是否存在。如果存在且内容详细,就是slab或slub。如果不存在或者内容很少,可能是slob。

嗯,slab分配器这块就聊到这儿。记住一句话:kmalloc是日常,mempool是保险,选对分配器是基本功。


专注资料整理