基于 Linux Kernel v5.4.123 源码分析,ION 位于
drivers/staging/android/ion/
ION 是 Android 引入 Linux 内核的统一内存分配框架,解决一个核心问题:多个硬件设备(GPU、Camera、Display、Video 编解码器)如何零拷贝共享同一块物理内存。
在没有 ION 之前,每个设备驱动各自管理内存,跨设备传递数据需要多次拷贝。ION 提供统一的用户态接口,分配出的内存通过 Linux dma-buf 框架导出为文件描述符(fd),任何设备驱动都可以通过这个 fd 直接访问底层物理页面。
用户态程序 │ │ open("/dev/ion") │ ioctl(ION_IOC_ALLOC) → 得到 dma-buf fd │ ├── mmap(fd) ─────────────── CPU 直接读写 │ ├── 传 fd 给 GPU 驱动 ────── GPU DMA 访问(零拷贝) │ ├── 传 fd 给 Camera 驱动 ── Camera DMA 写入(零拷贝) │ └── 传 fd 给 Display 驱动 ─ Display DMA 读取(零拷贝)
ION 采用分层 + 策略模式设计,共 6 个源文件,约 1600 行代码:
┌─────────────────────────────────────────────────────────┐ │ 用户空间 │ │ open("/dev/ion") → ioctl(ION_IOC_ALLOC) │ └────────────────────────┬────────────────────────────────┘ │ ioctl ┌────────────────────────▼────────────────────────────────┐ │ ion.c — 核心框架层(668 行) │ │ ┌──────────┐ ┌───────────┐ ┌──────────────────────┐ │ │ │/dev/ion │ │ioctl 分发 │ │dma_buf_ops 实现 │ │ │ │misc 设备 │ │ALLOC/QUERY│ │attach/map/mmap/sync │ │ │ └──────────┘ └─────┬─────┘ └──────────────────────┘ │ │ │ │ │ ion_heap_ops(虚函数表) │ │ ┌───────────────┼───────────────┐ │ │ ▼ ▼ ▼ │ │ ┌──────────┐ ┌────────────┐ ┌───────────┐ │ │ │System │ │System │ │CMA Heap │ │ │ │Heap │ │Contig Heap │ │ │ │ │ │(377 行) │ │(同文件) │ │(138 行) │ │ │ └────┬─────┘ └────────────┘ └───────────┘ │ │ │ │ │ ┌────▼──────────┐ ┌──────────────────┐ │ │ │ion_page_pool │ │ion_heap.c │ │ │ │页面缓存池 │ │通用辅助函数 │ │ │ │(155 行) │ │(315 行) │ │ │ └───────────────┘ └──────────────────┘ │ └──────────────────────────────────────────────────────────┘
| 文件 | 职责 |
|---|---|
ion.c | 核心框架:设备注册、ioctl 分发、dma-buf 集成、buffer 生命周期 |
ion.h | 所有内部数据结构和接口声明 |
uapi/ion.h | 用户态 API:ioctl 命令、堆类型枚举、分配参数结构体 |
ion_heap.c | 堆通用操作:内核/用户映射、deferred free、shrinker |
ion_page_pool.c | 页面缓存池:避免频繁进出 buddy allocator |
ion_system_heap.c | System Heap + System Contig Heap 实现 |
ion_cma_heap.c | CMA Heap 实现 |
cstruct ion_device {
struct miscdevice dev; // /dev/ion 字符设备
struct rw_semaphore lock; // 保护 heaps 链表
struct plist_head heaps; // 优先级排序链表,所有已注册的 heap
struct dentry *debug_root; // debugfs 根目录 /sys/kernel/debug/ion/
int heap_cnt; // 已注册 heap 数量
};
全局唯一实例 internal_dev,内核启动时通过 subsys_initcall(level 4)创建。使用 plist(优先级链表)管理 heap,保证分配时按优先级遍历。
cstruct ion_buffer {
struct list_head list; // 挂到 deferred free list
struct ion_device *dev;
struct ion_heap *heap; // 来自哪个 heap
unsigned long flags; // 用户标志(ION_FLAG_CACHED)
unsigned long private_flags; // 内部标志(ION_PRIV_FLAG_SHRINKER_FREE)
size_t size;
void *priv_virt; // heap 私有数据
struct mutex lock;
int kmap_cnt; // 内核映射引用计数
void *vaddr; // 内核虚拟地址
struct sg_table *sg_table; // scatter-gather 表 —— 核心数据
struct list_head attachments; // 所有 attach 的设备列表
};
sg_table 是 ION buffer 的核心表示。 它描述了 buffer 对应的物理页面集合(可能不连续)。所有后续操作(DMA 映射、用户态 mmap、内核映射)都基于 sg_table 完成。每种 heap 在 allocate 时必须填充 buffer->sg_table。
cstruct ion_heap {
struct plist_node node; // 优先级链表节点
struct ion_device *dev;
enum ion_heap_type type; // SYSTEM / SYSTEM_CONTIG / DMA
struct ion_heap_ops *ops; // 虚函数表 —— 策略模式的核心
unsigned long flags; // ION_HEAP_FLAG_DEFER_FREE
unsigned int id; // 自动递增,也决定优先级
/* deferred free */
struct shrinker shrinker; // 注册到内核回收框架
struct list_head free_list; // 待回收 buffer 队列
size_t free_list_size;
spinlock_t free_lock;
wait_queue_head_t waitqueue;
struct task_struct *task; // 后台回收线程
/* 统计 */
u64 num_of_buffers;
u64 num_of_alloc_bytes;
u64 alloc_bytes_wm; // 分配高水位标记
spinlock_t stat_lock;
};
cstruct ion_heap_ops {
int (*allocate)(heap, buffer, len, flags); // 分配物理内存,填充 sg_table
void (*free)(buffer); // 释放物理内存
void*(*map_kernel)(heap, buffer); // 映射到内核虚拟地址
void (*unmap_kernel)(heap, buffer); // 解除内核映射
int (*map_user)(heap, buffer, vma); // 映射到用户虚拟地址
int (*shrink)(heap, gfp_mask, nr_to_scan); // 可选:回收缓存页面
};
不同 heap 类型实现不同的 ops,核心框架通过函数指针调用,实现接口统一、实现可替换。
cstruct ion_page_pool {
int high_count; // highmem 缓存页数
int low_count; // lowmem 缓存页数
struct list_head high_items; // highmem 页面链表
struct list_head low_items; // lowmem 页面链表
struct mutex mutex;
gfp_t gfp_mask; // 从系统分配新页时的 GFP 标志
unsigned int order; // 页面阶数(0, 4, 8)
};
v5.4 的 ION 只暴露两个 ioctl,相比早期版本大幅简化(移除了 handle、share、import 等概念):
cstruct ion_allocation_data {
__u64 len; // 请求大小(字节)
__u32 heap_id_mask; // 位掩码,指定从哪些 heap 分配
__u32 flags; // ION_FLAG_CACHED 等
__u32 fd; // 输出:dma-buf 文件描述符
__u32 unused;
};
用户态示例:
cint ion_fd = open("/dev/ion", O_RDONLY);
struct ion_allocation_data alloc = {
.len = 1024 * 1024, // 1MB
.heap_id_mask = (1 << system_heap_id), // 从 system heap 分配
.flags = ION_FLAG_CACHED, // 可缓存
};
ioctl(ion_fd, ION_IOC_ALLOC, &alloc);
// alloc.fd 现在是一个 dma-buf fd
// CPU 访问
void *ptr = mmap(NULL, alloc.len, PROT_READ | PROT_WRITE,
MAP_SHARED, alloc.fd, 0);
// 用完后
munmap(ptr, alloc.len);
close(alloc.fd); // 引用计数归零时自动释放底层内存
因为 heap id 是动态分配的,用户态需要先查询再分配:
c// 第一步:获取 heap 数量
struct ion_heap_query query = { .cnt = 0, .heaps = 0 };
ioctl(ion_fd, ION_IOC_HEAP_QUERY, &query);
// 第二步:获取 heap 详情
struct ion_heap_data heaps[query.cnt];
query.heaps = (uintptr_t)heaps;
ioctl(ion_fd, ION_IOC_HEAP_QUERY, &query);
// 第三步:按 type 或 name 找到目标 heap
for (int i = 0; i < query.cnt; i++) {
printf("heap[%d]: name=%s type=%u id=%u\n",
i, heaps[i].name, heaps[i].type, heaps[i].heap_id);
}
ion_alloc(len, heap_id_mask, flags) │ ├── 1. len = PAGE_ALIGN(len) // 对齐到页边界 │ ├── 2. 遍历 heaps(plist 按优先级从高到低) │ │ │ └── if ((1 << heap->id) & heap_id_mask) // mask 匹配 │ │ │ └── ion_buffer_create(heap, dev, len, flags) │ │ │ ├── kzalloc(buffer) // 分配元数据 │ ├── heap->ops->allocate() // 具体 heap 分配物理内存 │ │ │ │ │ └── 失败且有 DEFER_FREE? │ │ ├── drain freelist // 回收延迟释放的内存 │ │ └── 重试 allocate │ │ │ ├── 校验 buffer->sg_table != NULL │ └── 更新统计(num_of_buffers, alloc_bytes 等) │ ├── 3. dma_buf_export(buffer) // 包装为 dma-buf 对象 │ └── 4. dma_buf_fd(dmabuf, O_CLOEXEC) // 创建 fd 返回用户态
分配失败重试机制: 当 heap 设置了 ION_HEAP_FLAG_DEFER_FREE 时,首次分配失败不会立即报错,而是先 drain freelist(释放延迟回收队列中的 buffer),然后重试。这显著提高了内存紧张时的分配成功率。
ION 分配出的每个 buffer 都被包装为 dma-buf,通过 dma_buf_ops 实现与内核 DMA 框架的对接。这是零拷贝跨设备共享的基础。
cstatic const struct dma_buf_ops dma_buf_ops = {
.attach = ion_dma_buf_attach, // 设备注册
.detach = ion_dma_buf_detatch, // 设备注销
.map_dma_buf = ion_map_dma_buf, // DMA 地址映射
.unmap_dma_buf = ion_unmap_dma_buf, // DMA 地址解映射
.mmap = ion_mmap, // 用户态 mmap
.release = ion_dma_buf_release, // buffer 释放
.begin_cpu_access = ion_dma_buf_begin_cpu_access, // CPU 访问前 cache 同步
.end_cpu_access = ion_dma_buf_end_cpu_access, // CPU 访问后 cache 同步
.map = ion_dma_buf_kmap, // 内核态按页映射
.unmap = ion_dma_buf_kunmap, // 内核态解映射(空操作)
};
每个设备 attach 时,ION 会 dup 一份 sg_table(dup_sg_table),因为不同设备经过 IOMMU 映射后看到的 DMA 地址不同:
同一块物理内存 (phys: 0x8000_0000) │ ├── GPU (有 IOMMU) → dma_address = 0x0010_0000 ├── Display (无 IOMMU) → dma_address = 0x8000_0000(直通) └── Camera (另一 IOMMU) → dma_address = 0xFF00_0000
物理页面共享(零拷贝),但 sg_table 元数据各自独立。
在有硬件 cache 的系统上(ARM),CPU 写入的数据可能停留在 cache 中,设备通过 DMA 读到的是过时数据。ION 通过配对的 begin/end 回调解决:
begin_cpu_access(DMA_TO_DEVICE) → map buffer 到内核(kmap_get) → dma_sync_sg_for_cpu() // invalidate cache,看到设备最新写入 │ │ CPU 在此区间安全地读写 buffer │ end_cpu_access(DMA_TO_DEVICE) → dma_sync_sg_for_device() // flush cache,让设备看到 CPU 写入 → unmap(kmap_put)
cstatic int ion_mmap(struct dma_buf *dmabuf, struct vm_area_struct *vma)
{
if (!(buffer->flags & ION_FLAG_CACHED))
vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
ret = buffer->heap->ops->map_user(buffer->heap, buffer, vma);
}
非 cached buffer 使用 write-combine 映射,避免 cache 一致性问题;cached buffer 使用普通映射,性能更好但需要显式 sync。
最复杂、最重要的 heap,Android 上最常用。
使用三个阶数的复合页(compound page),贪心选择最大可用阶数:
cstatic const unsigned int orders[] = {8, 4, 0};
// order 8 = 256 页 = 1MB 大页
// order 4 = 16 页 = 64KB
// order 0 = 1 页 = 4KB
分配 3MB 的过程: size_remaining = 3MB → 尝试 order 8 (1MB) ✓ → 剩余 2MB, max_order=8 → 尝试 order 8 (1MB) ✓ → 剩余 1MB, max_order=8 → 尝试 order 8 (1MB) ✓ → 剩余 0, 完成 分配 100KB 的过程: size_remaining = 100KB (对齐后 100KB) → 尝试 order 8 (1MB) 跳过(太大) → 尝试 order 4 (64KB) ✓ → 剩余 36KB, max_order=4 → 尝试 order 4 (64KB) 跳过(太大) → 尝试 order 0 (4KB) ✓ → 剩余 32KB, max_order=0 → ... 逐页分配,共 9 次
结果是一个 sg_table,包含多个不同大小的 sg entry。内存不要求物理连续,通过 scatter-gather DMA 访问。
c// 高阶分配(order > 4):快速失败,不回收不等待
static gfp_t high_order_gfp_flags = (GFP_HIGHUSER | __GFP_ZERO |
__GFP_NOWARN | __GFP_NORETRY) & ~__GFP_RECLAIM;
// 低阶分配(order <= 4):允许回收等待,保证成功
static gfp_t low_order_gfp_flags = GFP_HIGHUSER | __GFP_ZERO;
高阶大页分配失败很正常(内存碎片),快速 fallback 到低阶即可。低阶分配必须成功,允许内核做内存回收。
每个 order 对应一个缓存池,释放的页面放回 pool 而非还给 buddy:
ion_system_heap ├── pools[0] → ion_page_pool { order=8 } // 缓存 1MB 大页 ├── pools[1] → ion_page_pool { order=4 } // 缓存 64KB 页 └── pools[2] → ion_page_pool { order=0 } // 缓存 4KB 页
分配路径: ion_page_pool_alloc(pool) → pool 有缓存?取出(O(1))→ 返回 → pool 空?alloc_pages() → 走 buddy allocator 释放路径: ion_system_heap_free(buffer) → ion_heap_buffer_zero(buffer) // 先清零(安全) → ion_page_pool_free(pool, page) // 放回 pool,不还系统
Pool 带来显著的性能提升:避免频繁进出 buddy allocator,且缓存的页面已清零、cache 状态已知,下次分配可直接使用。
totalram_pages() / 2)ION_PRIV_FLAG_SHRINKER_FREE 标志时直接归还系统简单直接,适用于需要物理连续且大小不大的场景:
cstatic int ion_system_contig_heap_allocate(...)
{
int order = get_order(len);
struct page *page = alloc_pages(gfp, order); // buddy allocator
split_page(page, order); // 分裂为单页
// 释放 order 对齐多出来的页面
for (i = len >> PAGE_SHIFT; i < (1 << order); i++)
__free_page(page + i);
// sg_table 只有 1 个 entry(物理连续)
sg_set_page(table->sgl, page, len, 0);
}
没有 page pool,没有 deferred free,没有 shrink op。
cstruct ion_cma_heap {
struct ion_heap heap;
struct cma *cma; // 对应一个 CMA 预留区域
};
通过 cma_for_each_area() 自动为系统中每个 CMA 区域创建一个 heap:
cstatic int ion_add_cma_heaps(void)
{
cma_for_each_area(__ion_add_cma_heaps, NULL);
}
device_initcall(ion_add_cma_heaps);
CMA 区域在设备树(DTS)或内核启动参数中预留,分配出的内存保证物理连续,适用于不支持 scatter-gather DMA 的设备(如某些低端 Display 控制器)。
| 特性 | System Heap | System Contig | CMA Heap |
|---|---|---|---|
| 物理连续 | 不保证 | 保证 | 保证 |
| 内存来源 | buddy (全系统) | buddy (全系统) | CMA 预留区 |
| Page Pool | 有(三阶) | 无 | 无 |
| Deferred Free | 有 | 无 | 无 |
| Shrinker | 有 | 无 | 无 |
| 释放前清零 | 是 | 否 | 否 |
| sg_table entries | 多个(散布) | 1 个 | 1 个 |
| 典型用途 | GPU/Camera buffer | 小型连续 DMA | 大块连续 DMA |
System Heap 设置了 ION_HEAP_FLAG_DEFER_FREE,buffer 释放时不立即回收:
用户 close(fd) → dma-buf 引用计数归零 → ion_dma_buf_release() → _ion_buffer_destroy() → ion_heap_freelist_add(heap, buffer) // 加入 freelist,不真正释放 → wake_up(&heap->waitqueue)
后台内核线程(SCHED_IDLE 优先级,最低调度优先级)逐个回收:
cstatic int ion_heap_deferred_free(void *data)
{
while (true) {
wait_event_freezable(heap->waitqueue,
ion_heap_freelist_size(heap) > 0);
// 取出第一个 buffer,真正释放
buffer = list_first_entry(&heap->free_list, ...);
list_del(&buffer->list);
ion_buffer_destroy(buffer); // zero + 放回 page pool
}
}
好处:
每个有 deferred free 或 shrink op 的 heap 都注册 shrinker:
cint ion_heap_init_shrinker(struct ion_heap *heap)
{
heap->shrinker.count_objects = ion_heap_shrink_count; // "我有多少可回收"
heap->shrinker.scan_objects = ion_heap_shrink_scan; // "请回收 N 页"
return register_shrinker(&heap->shrinker);
}
内存压力时的回收链:
系统内存不足 → kswapd 唤醒 → shrink_slab() 遍历所有 shrinker → ion_heap_shrink_count() │ 返回 freelist 页数 + page pool 页数 │ → ion_heap_shrink_scan(nr_to_scan) │ ├── 1. 先回收 deferred freelist │ ion_heap_freelist_shrink() │ 设置 ION_PRIV_FLAG_SHRINKER_FREE → 跳过 page pool,直接 __free_pages() │ └── 2. 不够再回收 page pool heap->ops->shrink() → ion_system_heap_shrink() → ion_page_pool_shrink(pool[0]) // order 8 → ion_page_pool_shrink(pool[1]) // order 4 → ion_page_pool_shrink(pool[2]) // order 0
ION_PRIV_FLAG_SHRINKER_FREE 标志的精妙设计:
正常释放路径: page → zero 清零 → 放回 page pool(缓存,加速下次分配) shrinker 释放路径: page → 设置 SHRINKER_FREE → 跳过 page pool → __free_pages()(归还系统)
这保证了内存压力时物理内存真正被释放回系统,而不是从一个缓存挪到另一个缓存。
cvoid *ion_heap_map_kernel(struct ion_heap *heap, struct ion_buffer *buffer)
{
// 1. 收集 sg_table 中所有页面到 pages[] 数组
for_each_sg(table->sgl, sg, table->nents, i) {
for (j = 0; j < npages_this_entry; j++)
*(tmp++) = page++;
}
// 2. 选择页保护属性
pgprot = (buffer->flags & ION_FLAG_CACHED) ?
PAGE_KERNEL : pgprot_writecombine(PAGE_KERNEL);
// 3. vmap 映射到连续虚拟地址
return vmap(pages, npages, VM_MAP, pgprot);
}
将散布在物理内存各处的页面映射到连续的内核虚拟地址空间,让内核代码可以用普通指针访问 buffer 内容。
cint ion_heap_map_user(struct ion_heap *heap, struct ion_buffer *buffer,
struct vm_area_struct *vma)
{
for_each_sg(table->sgl, sg, table->nents, i) {
// 逐 sg entry 调用 remap_pfn_range 映射到用户 VMA
remap_pfn_range(vma, addr, page_to_pfn(page), len, vma->vm_page_prot);
addr += len;
}
}
支持 vm_pgoff 偏移,用户态可以 mmap buffer 的任意位置。
c// 分 32 页一批处理,避免一次性大面积 vmap
static int ion_heap_sglist_zero(struct scatterlist *sgl, ...)
{
struct page *pages[32];
for_each_sg_page(sgl, &piter, nents, 0) {
pages[p++] = sg_page_iter_page(&piter);
if (p == 32) {
vmap → memset(0) → vunmap // 批量清零
p = 0;
}
}
}
内核启动 │ ├── subsys_initcall (level 4) │ └── ion_device_create() │ ├── kzalloc(ion_device) │ ├── misc_register("/dev/ion") // 注册字符设备 │ ├── debugfs_create_dir("ion") // 创建 debugfs 目录 │ ├── plist_head_init(&heaps) // 初始化优先级链表 │ └── internal_dev = idev // 设置全局指针 │ └── device_initcall (level 6) ├── ion_system_heap_create() │ ├── 创建 3 个 page pool (order 8/4/0) │ ├── 设置 ION_HEAP_FLAG_DEFER_FREE │ └── ion_device_add_heap() │ ├── 初始化 deferred free 线程 │ ├── 注册 shrinker │ ├── 创建 debugfs 节点 │ └── plist_add (id=0, priority=0) │ ├── ion_system_contig_heap_create() │ └── ion_device_add_heap() (id=1, priority=-1) │ └── ion_add_cma_heaps() └── cma_for_each_area(__ion_add_cma_heaps) └── ion_device_add_heap() (id=2+, priority=-2-)
subsys_initcall 先于 device_initcall,保证 internal_dev 在各 heap 注册时已就绪。
每个 heap 在 /sys/kernel/debug/ion/{heap_name}/ 下暴露:
| 节点 | 权限 | 说明 |
|---|---|---|
num_of_buffers | 0444 | 当前存活的 buffer 数量 |
num_of_alloc_bytes | 0444 | 当前已分配字节数 |
alloc_bytes_wm | 0444 | 历史分配字节数高水位 |
{name}_shrink | 0644 | 读:可回收页数;写 N:回收 N 页;写 0:全部回收 |
使用示例:
bash# 查看 system heap 状态
cat /sys/kernel/debug/ion/ion_system_heap/num_of_buffers
cat /sys/kernel/debug/ion/ion_system_heap/num_of_alloc_bytes
# 查看可回收页面数
cat /sys/kernel/debug/ion/ion_system_heap/ion_system_heap_shrink
# 手动回收所有缓存
echo 0 > /sys/kernel/debug/ion/ion_system_heap/ion_system_heap_shrink
| 设计 | 手段 | 收益 |
|---|---|---|
| C 语言多态 | ion_heap_ops 虚函数表 | 框架与实现解耦,厂商可扩展自定义 heap |
| 零拷贝共享 | 基于 dma-buf,每个 attach dup sg_table | 多设备共享物理页面,各自维护 DMA 地址 |
| 抗碎片化 | 多阶贪心分配 (order 8→4→0) | 大请求尽量用大页,减少 sg entry 数量 |
| 分配加速 | Page Pool 缓存已清零页面 | 避免反复进出 buddy allocator |
| 释放不阻塞 | Deferred Free + SCHED_IDLE 后台线程 | 用户态释放路径零延迟 |
| 系统级回收 | Shrinker 注册到内核框架 | 内存压力时自动归还,避免 OOM |
| 安全 | 释放前清零 buffer 内容 | 防止信息通过复用页面泄漏 |
| 分配容错 | 失败时 drain freelist 重试 | 内存紧张时提高分配成功率 |
| 简化接口 | 只有 ALLOC 和 QUERY 两个 ioctl | 用户态使用简单,分配直接返回 fd |
| EXPORT_SYMBOL | ion_device_add_heap 导出 | SoC 厂商可通过内核模块注册私有 heap |
| 内核版本 | 变化 |
|---|---|
| 3.3 | ION 进入 staging,有 client/handle/share/import 等复杂概念 |
| 4.12+ | 逐步简化,移除 client 和 handle |
| 5.4 | 最终简化形态:只有 ALLOC/QUERY 两个 ioctl,分配直接返回 fd |
| 5.6+ | dma-buf heaps 框架(drivers/dma-buf/heaps/)开始取代 ION |
| 5.10+ | System Heap 和 CMA Heap 迁移到 dma-buf/heaps/ |
| 5.18 | ION 被完全移除 |
v5.4.123 是理解 ION 设计思想的最佳版本 — 足够成熟、足够简洁,去除了历史包袱但保留了所有核心机制。
drivers/staging/android/ion/(6 个文件,约 1600 行)drivers/staging/android/uapi/ion.hdrivers/dma-buf/dma-buf.cdrivers/dma-buf/heaps/