Linux 内核 genalloc/genpool 通用内存池子系统深度解析
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
导读
genalloc/genpool 是 Linux 内核中一套轻量级的通用内存分配子系统,专为管理那些不受常规kmalloc/kfree支配的"特殊用途内存"而设计——典型场景包括设备片上 SRAM、非缓存内存(uncached memory)、DMA 缓冲区等。本文以 Documentation/core-api/genalloc.rst 为主线,结合 lib/genalloc.c 与 include/linux/genalloc.h 的源码实现,完整讲解池的创建、内存注入、分配/释放、分配算法选择与查询辅助接口,并给出驱动开发中可直接套用的实战示例,帮助读者掌握为任意地址区间定制内存分配器的标准方法。
为什么内核需要 genpool:从特殊内存到通用模块
内核中存在大量内存分配子系统,各有明确分工:页分配器、slab 分配器、CMA、vmalloc……但有时驱动作者需要为一段"特殊用途内存"编写自己的分配器——例如某块设备上带有的片上 SRAM、一段不可缓存的内存区域。如果每个驱动都各自实现一个微型分配器,内核中就会堆积大量测试不充分、行为各异的分配代码。
genalloc 正是为解决这一痛点而生的通用模块:它把"在任意一段地址区间上做按位图分配"的能力抽象出来,供所有需要管理特殊内存的代码复用。从源码注释(lib/genalloc.c)可以看到其定位:
Basic general purpose allocator for managing special purpose memory, for example, memory that is not managed by the regular kmalloc/kfree interface. Uses for this includes on-device special memory, uncached memory etc.
这一子系统源自 2005 年 Jes Sorensen 从sym53c8xx_2驱动中提炼出的分配器,于内核 2.6.13 版本合入主线,此后经过大量演进。如今,任何使用该分配器的代码只需包含头文件<linux/genalloc.h>。
核心设计特性
从 lib/genalloc.c 的头部注释与实现中可以提炼出该子系统的几项关键设计:
- 按位图管理内存:每个内存块(chunk)配一张位图,每一位代表
min_alloc_order字节的粒度; - 可在 NMI 等不可阻塞上下文中安全使用:分配/释放路径基于
cmpxchg原子操作与重试实现,不依赖自旋锁,因此不会在特殊上下文中因锁竞争而死锁;代价是极端情况下可能出现活锁(livelock); - 无锁路径的前提是内存充足:只有当池中已有足够内存时分配/释放才是完全无锁的;向池中新增内存块时仍需要拿锁,因此依赖无锁特性的使用者必须确保内存预先注入完毕(见 lib/genalloc.c);
- 架构依赖:其基本原子操作是对
long的cmpxchg。在不具备 NMI 安全cmpxchg实现的架构上(即未配置CONFIG_ARCH_HAVE_NMI_SAFE_CMPXCHG),分配/释放函数内部会执行BUG_ON(in_nmi()),明确禁止在 NMI 处理器中使用(见 lib/genalloc.c 与 lib/genalloc.c)。
核心数据结构:pool 与 chunk
在深入 API 之前,先理解两个基础数据结构(定义于 include/linux/genalloc.h):
struct gen_pool { spinlock_t lock; /* 保护 chunks 链表与池配置 */ struct list_head chunks; /* 该池管理的所有内存块链表 */ int min_alloc_order; /* 最小分配阶(log2 字节数) */ genpool_algo_t algo; /* 分配算法回调 */ void *data; /* 传给算法的附加数据 */ const char *name; /* 池名称(managed 接口使用) */ }; struct gen_pool_chunk { struct list_head next_chunk; /* 链表节点 */ atomic_long_t avail; /* 本块剩余可用字节数(原子读) */ phys_addr_t phys_addr; /* 内存块物理起始地址 */ void *owner; /* 注入时可携带的私有数据 */ unsigned long start_addr; /* 内存块虚拟起始地址 */ unsigned long end_addr; /* 内存块虚拟结束地址(含) */ unsigned long bits[]; /* 柔性数组:跟踪分配状态的位图 */ };可以看到,一个gen_pool由一串gen_pool_chunk组成,每个 chunk 记录一段连续内存的起止地址、物理地址、剩余量,并用bits[]位图标记每个分配粒度单元的占用情况。分配算法的本质就是在位图上寻找足够长的连续"0"位区间。
池的生命周期:创建、注入内存、销毁
创建池:gen_pool_create 与 devm_gen_pool_create
池的创建有两个入口(原型声明见 include/linux/genalloc.h 与 include/linux/genalloc.h):
struct gen_pool *gen_pool_create(int min_alloc_order, int nid); struct gen_pool *devm_gen_pool_create(struct device *dev, int min_alloc_order, int nid, const char *name);gen_pool_create()的实现(lib/genalloc.c)通过kmalloc_node分配池结构、初始化自旋锁与 chunk 链表,并把默认分配算法设为gen_pool_first_fit。
两个关键参数:
- min_alloc_order:分配粒度,取以 2 为底的对数,但单位是字节而非页(与页分配器的 order 语义不同)。例如传入
3,则所有分配都将是 8 字节的整数倍。该值越大,每块内存所需的跟踪位图越小(位图总位数 = chunk 字节数 >> order),管理开销越低;代价是内部碎片(granularity 浪费)越大。 - nid:池的"家务结构"(pool 描述符、chunk 描述符与位图)分配所在的 NUMA 节点,
-1表示不关心(使用NUMA_NO_NODE语义)。
devm_gen_pool_create()是"受管(managed)"版本(lib/genalloc.c):它通过 devres 机制把池绑定到指定设备上,当设备被销毁时由devm_gen_pool_release()自动调用gen_pool_destroy()清理池(lib/genalloc.c)。它还要求同一设备上池名称唯一——如果gen_pool_get(dev, name)已能找到同名池,则返回-EINVAL(lib/genalloc.c)。name参数用于按名查找池,可为NULL。
注入内存:gen_pool_add / gen_pool_add_owner
新建的池是空的,没有任何内存可分。第一步通常是向池中添加内存块:
static inline int gen_pool_add(struct gen_pool *pool, unsigned long addr, size_t size, int nid); int gen_pool_add_owner(struct gen_pool *pool, unsigned long virt, phys_addr_t phys, size_t size, int nid, void *owner);gen_pool_add()把从addr(内核虚拟地址空间)开始的size字节放入池中,nid用于辅助结构的内存节点。它其实是gen_pool_add_owner()的简化包装(include/linux/genalloc.h),传入phys = -1与owner = NULL:
static inline int gen_pool_add(struct gen_pool *pool, unsigned long addr, size_t size, int nid) { return gen_pool_add_virt(pool, addr, -1, size, nid); }gen_pool_add_virt()则显式关联物理地址,仅当池将用于 DMA 分配时才必须使用(include/linux/genalloc.h)。
gen_pool_add_owner()的实现(lib/genalloc.c)会:
- 计算位图位数
nbits = size >> min_alloc_order; - 用
vzalloc_node分配 chunk 描述符 + 位图(零初始化保证"全部空闲"); - 填充
phys_addr、start_addr、end_addr = virt + size - 1、owner,并将chunk->avail初始化为size; - 在
pool->lock保护下把 chunk 挂入pool->chunks链表(list_add_rcu)。
owner是注入时携带的私有数据,可在分配时通过owner出参取回(见下文分配接口),适合传递"这块内存属于哪个客户/子模块"之类的信息。
销毁池:gen_pool_destroy
void gen_pool_destroy(struct gen_pool *pool);销毁逻辑(lib/genalloc.c)遍历每个 chunk,通过find_first_bit()检查位图中第一个被占用位:只要还有未归还的分配,就执行BUG_ON(bit < end_bit),直接崩溃整个系统。这是文档中明确警告的"极端手段"——销毁前必须确保所有分配都已gen_pool_free()归还。
分配与释放:gen_pool_alloc / gen_pool_dma_alloc / gen_pool_free
常规分配 gen_pool_alloc
static inline unsigned long gen_pool_alloc(struct gen_pool *pool, size_t size);从池中分配size字节,返回内核虚拟地址(unsigned long类型),失败返回 0。它使用池当前的默认算法(pool->algo)与附加数据(pool->data),即gen_pool_alloc_algo_owner()的薄封装(include/linux/genalloc.h)。
核心路径gen_pool_alloc_algo_owner()(lib/genalloc.c)的工作流程:
- 非 NMI 安全架构上先
BUG_ON(in_nmi())防护; - 把请求的
size向上取整为粒度整数倍,得到所需位数nbits; - 在
rcu_read_lock()保护下遍历各 chunk,跳过size > chunk->avail的块; - 调用分配算法
algo()在块位图上定位候选位置start_bit; - 用无锁的
bitmap_set_ll()尝试置位,若与其他并发分配者冲突则清位重试(goto retry); - 成功后计算返回地址
chunk->start_addr + (start_bit << order),并原子地atomic_long_sub(size, &chunk->avail)扣减剩余量。
DMA 分配系列
void *gen_pool_dma_alloc(struct gen_pool *pool, size_t size, dma_addr_t *dma); void *gen_pool_dma_alloc_algo(struct gen_pool *pool, size_t size, dma_addr_t *dma, genpool_algo_t algo, void *data); void *gen_pool_dma_alloc_align(struct gen_pool *pool, size_t size, dma_addr_t *dma, int align); void *gen_pool_dma_zalloc(struct gen_pool *pool, size_t size, dma_addr_t *dma); void *gen_pool_dma_zalloc_algo(struct gen_pool *pool, size_t size, dma_addr_t *dma, genpool_algo_t algo, void *data); void *gen_pool_dma_zalloc_align(struct gen_pool *pool, size_t size, dma_addr_t *dma, int align);gen_pool_dma_alloc()(lib/genalloc.c)用于分配供 DMA 操作使用的内存:它内部调用常规分配拿到虚拟地址,再通过gen_pool_virt_to_phys()把对应的物理地址写入dma出参(lib/genalloc.c)。前提是这些内存必须以gen_pool_add_virt()方式注入(即带上了物理地址),否则物理地址换算无意义。
与常规 API 的一个明显差异:它返回void *而非unsigned long,符合"返回虚拟地址指针"的语义。配套变体还包括:
gen_pool_dma_alloc_align():要求起始地址按align字节对齐,内部构造genpool_data_align并改用gen_pool_first_fit_align算法(lib/genalloc.c);gen_pool_dma_zalloc()系列:分配后用memset将内存清零(lib/genalloc.c)。
归还内存 gen_pool_free
static inline void gen_pool_free(struct gen_pool *pool, unsigned long addr, size_t size); void gen_pool_free_owner(struct gen_pool *pool, unsigned long addr, size_t size, void **owner);gen_pool_free()归还之前分配的size字节,必须与分配时传入的size匹配。实现(lib/genalloc.c)先在rcu_read_lock()下定位包含addr的 chunk,然后:
- 若
addr + size - 1超出该 chunk 范围,触发BUG_ON(释放越界属于编程错误); - 用无锁的
bitmap_clear_ll()清位,冲突时同样BUG_ON; - 原子地把释放量加回
chunk->avail; - 若遍历完所有 chunk 都没找到该地址,直接
BUG()。
gen_pool_free_owner()额外通过owner出参返回注入时记录的 chunk 私有数据,便于调用方做资源归属统计。
分配算法:从 first-fit 到 fixed-alloc
上述接口只解决"从池里拿内存",却不控制"具体拿哪一段"。如果需要精细控制,就要用到算法接口:
unsigned long gen_pool_alloc_algo_owner(struct gen_pool *pool, size_t size, genpool_algo_t algo, void *data, void **owner); void gen_pool_set_algo(struct gen_pool *pool, genpool_algo_t algo, void *data);gen_pool_alloc_algo()系列在单次分配时显式指定算法;gen_pool_set_algo()则设置池的默认算法(传NULL会回退为gen_pool_first_fit,见 lib/genalloc.c)。data参数会原样传给算法,多数算法忽略它,但对齐与定点分配算法会用到。
算法回调的类型定义(include/linux/genalloc.h):
typedef unsigned long (*genpool_algo_t)(unsigned long *map, unsigned long size, unsigned long start, unsigned int nr, void *data, struct gen_pool *pool, unsigned long start_addr);内核预置了五套算法,全部实现于 lib/genalloc.c:
| 算法 | 行为 | 源码位置 |
|---|---|---|
gen_pool_first_fit | 简单首次适配,无对齐约束,默认算法 | lib/genalloc.c |
gen_pool_first_fit_align | 强制按genpool_data_align.align字节对齐 | lib/genalloc.c |
gen_pool_first_fit_order_align | 按请求大小的 2 的幂阶对齐(如 60 字节分配按 64 字节对齐) | lib/genalloc.c |
gen_pool_best_fit | 最佳适配,选出最小的足以容纳请求的空闲区 | lib/genalloc.c |
gen_pool_fixed_alloc | 在池内指定偏移处定点分配,该处不可用则分配失败 | lib/genalloc.c |
配套的数据描述符(include/linux/genalloc.h):
struct genpool_data_align { int align; /* 起始地址按字节对齐 */ }; struct genpool_data_fixed { unsigned long offset; /* 定点分配的区域偏移 */ };几个值得注意的实现细节:
- first_fit_align:先把
align换算成位图粒度的align_mask,再考虑 chunk 起始地址本身的偏移align_off = (start_addr & (align - 1)) >> order,最终调用bitmap_find_next_zero_area_off()完成带对齐的搜索(lib/genalloc.c); - first_fit_order_align:
align_mask = roundup_pow_of_two(nr) - 1,即按请求位数向上取整为 2 的幂后减一作为掩码(lib/genalloc.c)。文档示例:60 字节请求在 order=0 时nr=60,对齐到 64 字节; - fixed_alloc:若
offset不是粒度整数倍,会WARN_ON并返回失败;随后只在start + offset_bit处尝试,若bitmap_find_next_zero_area()的结果不是offset_bit本身(说明该区域已被占用),则直接返回size表示"找不到"(lib/genalloc.c); - best_fit:遍历所有候选空洞,跟踪最小空洞长度
len,找到与nr完全匹配的区间即提前返回(lib/genalloc.c)。
查询与辅助接口
文档还列出了一组查询/遍历辅助函数(全部实现于 lib/genalloc.c):
- gen_pool_virt_to_phys:把池内虚拟地址换算为物理地址,未命中返回
-1。实现为遍历各 chunk,若addr落在[start_addr, end_addr]区间内则paddr = chunk->phys_addr + (addr - chunk->start_addr)(lib/genalloc.c); - gen_pool_for_each_chunk:对池内每个 chunk 调用回调函数(回调在
rcu_read_lock持有时执行)(lib/genalloc.c); - gen_pool_has_addr:判断
[start, start+size)区间是否完整落在池的某个 chunk 内(lib/genalloc.c); - gen_pool_avail:累加所有 chunk 的
avail,返回当前空闲总字节数(lib/genalloc.c); - gen_pool_size:累加所有 chunk 的
chunk_size,返回池管理的总字节数(lib/genalloc.c); - gen_pool_get:按设备与名称查找与设备关联的受管池(lib/genalloc.c);
- of_gen_pool_get:按设备树 phandle 属性查找池(仅在
CONFIG_OF下可用)(lib/genalloc.c)。
实战:驱动中的典型用法
场景一:设备树 + SRAM 池(remoteproc 实例)
genpool 最典型的现实应用之一是把设备树描述的片上 SRAM 纳入池管理。以 drivers/remoteproc/meson_mx_ao_arc.c 为例,该驱动在probe中通过of_gen_pool_get()按设备树属性"sram"获取 SRAM 池:
priv->sram_pool = of_gen_pool_get(dev->of_node, "sram", 0); /* L163 */ if (!priv->sram_pool) { ... } priv->sram_size = gen_pool_avail(priv->sram_pool); /* L169 */ priv->sram_va = gen_pool_alloc(priv->sram_pool, priv->sram_size); /* L171 */ priv->sram_pa = gen_pool_virt_to_phys(priv->sram_pool, priv->sram_va); /* L177 */在移除路径中则对称地归还:
gen_pool_free(priv->sram_pool, priv->sram_va, priv->sram_size); /* L227 / L237 */这段代码几乎完整覆盖了本文章介绍的核心 API:按设备树取池 → 查询可用量 → 分配 → 虚拟地址转物理地址 → 释放。SRAM 池本身由mm/shmem之外的 SRAM 驱动(如drivers/misc/sram.c体系的of_gen_pool_get链路)预先创建并注入内存,genpool 只负责位图化分配。
场景二:驱动内自建受管池(模板)
当驱动需要管理设备自带的特殊内存(如片上 SRAM 区、专用缓冲区)时,标准流程如下:
#include <linux/genalloc.h> #include <linux/of.h> struct my_dev { struct device *dev; struct gen_pool *pool; unsigned long buf_va; dma_addr_t buf_pa; }; static int my_dev_probe(struct platform_device *pdev) { struct my_dev *mdev = devm_kzalloc(&pdev->dev, sizeof(*mdev), GFP_KERNEL); /* 1. 创建受管池:粒度 4 字节(order=2),节点不关心 */ mdev->pool = devm_gen_pool_create(&pdev->dev, 2, -1, "mydev-pool"); if (IS_ERR(mdev->pool)) return PTR_ERR(mdev->pool); /* 2. 注入内存:设备寄存器/平台代码提供的 SRAM 区间 */ /* 若后续要做 DMA,必须用 gen_pool_add_virt() 带上物理地址 */ if (gen_pool_add_virt(mdev->pool, (unsigned long)sram_va, (phys_addr_t)sram_pa, sram_size, -1)) return -ENOMEM; /* 3. 分配并取得 DMA 物理地址 */ mdev->buf_va = (unsigned long) gen_pool_dma_alloc(mdev->pool, buf_size, &mdev->buf_pa); if (!mdev->buf_va) return -ENOMEM; /* 4. 设备销毁时 devm 自动销毁池;此处只需显式归还内存 */ /* gen_pool_free(mdev->pool, mdev->buf_va, buf_size); */ return 0; }要点归纳:
- 池在设备生命周期内有效,
devm_gen_pool_create()保证设备移除时自动销毁,无需手工调用gen_pool_destroy(); - 需要 DMA 时务必用
gen_pool_add_virt()(而非gen_pool_add())注入带物理地址的内存,否则gen_pool_virt_to_phys()无法得到正确结果; - 销毁池之前必须归还所有分配,否则
gen_pool_destroy()会BUG()崩掉系统; - 若对起始地址有对齐要求,改用
gen_pool_dma_alloc_align()或显式gen_pool_set_algo(pool, gen_pool_first_fit_align, &align_data)。
总结与设计建议
genpool 为"管理任意一段特殊用途内存"提供了标准答案:位图跟踪 + 原子位操作的无锁分配 + 可插拔分配算法 + 设备受管生命周期。理解它的关键在于把握几条主线:
- 粒度 trade-off:
min_alloc_order越大,位图越小、管理开销越低,但分配粒度越粗; - 无锁的代价:NMI 安全的分配依赖
CONFIG_ARCH_HAVE_NMI_SAFE_CMPXCHG与预注入的充足内存,两者缺一不可; - 算法即插即用:默认 first-fit 适合大多数场景,对齐/定点/最佳适配算法通过
data描述符扩展,也可自行实现genpool_algo_t回调; - 严格的对称性:
gen_pool_free()必须与分配的size严格一致,越界与未归还都是致命编程错误; - 物理地址只服务于 DMA:
gen_pool_add_virt()注入的物理地址是gen_pool_dma_alloc()与gen_pool_virt_to_phys()的前提。
正如文档结语所言:更广泛地了解该模块,有助于避免未来继续出现大量"特制"的专用内存分配器——需要管理特殊内存时,先看看 genpool 是否已经够用。
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考