news 2026/9/15 1:42:38

Linux 内核 genalloc/genpool 通用内存池子系统深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux 内核 genalloc/genpool 通用内存池子系统深度解析

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);
  • 架构依赖:其基本原子操作是对longcmpxchg。在不具备 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 = -1owner = 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)会:

  1. 计算位图位数nbits = size >> min_alloc_order
  2. vzalloc_node分配 chunk 描述符 + 位图(零初始化保证"全部空闲");
  3. 填充phys_addrstart_addrend_addr = virt + size - 1owner,并将chunk->avail初始化为size
  4. 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)的工作流程:

  1. 非 NMI 安全架构上先BUG_ON(in_nmi())防护;
  2. 把请求的size向上取整为粒度整数倍,得到所需位数nbits
  3. rcu_read_lock()保护下遍历各 chunk,跳过size > chunk->avail的块;
  4. 调用分配算法algo()在块位图上定位候选位置start_bit
  5. 用无锁的bitmap_set_ll()尝试置位,若与其他并发分配者冲突则清位重试(goto retry);
  6. 成功后计算返回地址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_alignalign_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 为"管理任意一段特殊用途内存"提供了标准答案:位图跟踪 + 原子位操作的无锁分配 + 可插拔分配算法 + 设备受管生命周期。理解它的关键在于把握几条主线:

  1. 粒度 trade-offmin_alloc_order越大,位图越小、管理开销越低,但分配粒度越粗;
  2. 无锁的代价:NMI 安全的分配依赖CONFIG_ARCH_HAVE_NMI_SAFE_CMPXCHG与预注入的充足内存,两者缺一不可;
  3. 算法即插即用:默认 first-fit 适合大多数场景,对齐/定点/最佳适配算法通过data描述符扩展,也可自行实现genpool_algo_t回调;
  4. 严格的对称性gen_pool_free()必须与分配的size严格一致,越界与未归还都是致命编程错误;
  5. 物理地址只服务于 DMAgen_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 1:42:35

RPA选型三大核心:实施、售后与培训体系深度评估指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 1:40:16

YOLOv8飞鸟检测全流程:数据集构建、模型训练与推理优化

简介&#xff1a;面向目标检测初学者与无人机巡检、生态监测方向开发者&#xff0c;这套基于YOLOv8的飞鸟检测工程完整集成了训练好的模型权重、Python推理与训练代码&#xff0c;以及近1000张已标注的鸟类图像&#xff1b;标注文件同时提供xml与txt两种格式&#xff0c;类别统…

作者头像 李华
网站建设 2026/9/15 1:39:18

通达信爆涨临界点副图指标:源码详解与实战应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 1:38:43

原生JavaScript实现全屏轮播:触摸手势、视口适配与性能优化

简介&#xff1a;面向网页前端初学者的HTML5全屏图片左右滑动轮播特效代码&#xff0c;适用于站点头图、产品展示或摄影作品集等大图场景的交互切换与多屏适配。压缩包共12个文件&#xff0c;结构紧凑&#xff1a;HTML页面负责轮播结构&#xff0c;CSS样式实现过渡动画与响应式…

作者头像 李华