Linux多媒体驱动共享内存深度解析:DMA-BUF与ION框架的生产级完整实战
一、多媒体场景的内存共享难题:为何需要DMA-BUF
多媒体处理链路涉及多个硬件模块协同工作。Camera采集图像数据,ISP做图像信号处理。GPU做渲染,Display Controller做显示输出。视频编码器将原始帧压缩为H.264/H.265。
每个硬件模块都只能访问物理上连续的内存。因为硬件DMA控制器无法理解虚拟地址。DMA控制器直接读取物理地址,要求内存在物理上连续。
传统做法是每个模块各自分配自己的缓冲区。Camera分配一块,ISP分配一块,GPU再分配一块。数据在不同模块之间传递时,需要来回拷贝。一次4K视频帧(3840×2160×3字节)约25MB。每个模块拷贝一次,延迟增加、带宽浪费、功耗上升。
Zero-Copy是多媒体系统的核心需求。多个硬件模块共享同一块物理内存。数据不产生任何拷贝,直接在各模块间流转。这就是DMA-BUF要解决的核心问题。
DMA-BUF是Linux内核的通用DMA缓冲区共享框架。它提供了一套标准的接口,让不同设备驱动之间共享内存。无需关心对方驱动的实现细节,只需遵循DMA-BUF接口。
ION是Android特有的内存分配器。它基于DMA-BUF框架实现,增加了更灵活的内存分配策略。支持多种堆类型(System Heap、Carveout Heap、CMA Heap)。
sequenceDiagram
participant App as 用户态应用
participant Camera as Camera驱动
participant ION as ION分配器
participant DMA as DMA-BUF框架
participant ISP as ISP驱动
participant GPU as GPU驱动
App->>Camera: 请求采集一帧图像
Camera->>ION: ion_alloc(size, flags)
ION->>DMA: dma_buf_export(scatterlist)
DMA–>>ION: dma_buf文件描述符
ION–>>Camera: ION缓冲区句柄+fd
Camera->>Camera: DMA写入图像数据到缓冲区
Camera->>ISP: 传递fd, 共享缓冲区
ISP->>DMA: dma_buf_get(fd)
DMA–>>ISP: dma_buf指针
ISP->>DMA: dma_buf_attach(dma_buf, dev)
ISP->>DMA: dma_buf_map_attachment()
DMA–>>ISP: sg_table(物理地址列表)
ISP->>ISP: 读取缓冲区数据做ISP处理
ISP->>GPU: 传递fd, 共享缓冲区
GPU->>DMA: dma_buf_get(fd)
GPU->>DMA: dma_buf_map_attachment()
GPU->>GPU: GPU渲染处理后的帧
GPU->>App: 返回处理结果
App->>DMA: close(fd), 释放DMA-BUF
DMA->>ION: 引用计数为0, 释放物理内存
DMA-BUF框架的核心设计理念是引用计数。只有当所有使用方都释放了缓冲区,内存才真正释放。这避免了Use-After-Free问题,保证了安全性。
二、DMA-BUF框架内核态接口深度解析与代码实现
DMA-BUF框架在内核态提供了一套完整的接口。任何设备驱动都可以作为Exporter或Importer。Exporter是分配内存的一方(比如Camera驱动)。Importer是使用已分配内存的一方(比如ISP驱动)。
Exporter需要实现struct dma_buf_ops中的回调函数。最关键的两个函数是map_dma_buf和unmap_dma_buf。map_dma_buf返回struct sg_table,包含物理内存的散射聚集列表。Importer通过sg_table获取物理地址,配置到硬件DMA寄存器中。
用户态通过dma_buf_fd系统调用获取文件描述符。这个文件描述符可以跨进程传递(通过Unix Domain Socket)。这就是多媒体框架中Buffer共享的底层机制。
ION在DMA-BUF基础上增加了Heap管理。每种Heap对应一种内存分配策略。System Heap分配普通内核内存(可能不连续)。CMA Heap分配CMA(Contiguous Memory Allocator)区域的内存(物理连续)。Carveout Heap分配预留的固定物理地址范围的内存。
/*
* DMA-BUF Exporter的生产级内核模块示例
* 功能:实现一个简单的DMA-BUF Exporter,
* 分配一块连续内存并导出为DMA-BUF
*
* 编译: make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
* 加载: insmod dmabuf_exporter.ko
* 测试: 用户态通过ioctl获取dma-buf fd
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/dma-buf.h>
#include <linux/dma-mapping.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/platform_device.h>
/* 自定义DMA-BUF私有数据结构 */
struct my_dma_buf {
struct dma_buf *dma_buf;
void *vaddr; /* 内核虚拟地址 */
dma_addr_t paddr; /* 物理地址 */
size_t size; /* 缓冲区大小 */
struct device *dev;
struct sg_table *sgt; /* 散射聚集表 */
refcount_t refcount;
};
/* DMA-BUF操作函数集 */
static struct dma_buf_ops my_dma_buf_ops = {
.map_dma_buf = my_map_dma_buf,
.unmap_dma_buf = my_unmap_dma_buf,
.release = my_release,
.begin_cpu_access = my_begin_cpu_access,
.end_cpu_access = my_end_cpu_access,
.mmap = my_mmap,
};
/*
* map_dma_buf: 将缓冲区映射到请求设备的地址空间
* 返回sg_table,包含物理内存的DMA地址列表
*/
static struct sg_table *my_map_dma_buf(
struct dma_buf_attachment *attach,
enum dma_data_direction dir
) {
struct my_dma_buf *my_buf = attach->dmabuf->priv;
struct sg_table *sgt;
int ret;
sgt = kmalloc(sizeof(*sgt), GFP_KERNEL);
if (!sgt)
return ERR_PTR(-ENOMEM);
/* 分配散射聚集表 */
ret = sg_alloc_table(sgt, 1, GFP_KERNEL);
if (ret) {
kfree(sgt);
return ERR_PTR(ret);
}
/* 将物理地址填入sg_table */
sg_dma_address(sgt->sgl) = my_buf->paddr;
sg_dma_len(sgt->sgl) = my_buf->size;
/* 同步DMA缓冲区(确保Cache一致性) */
dma_sync_single_for_device(
attach->dev, my_buf->paddr,
my_buf->size, dir
);
dev_dbg(attach->dev,
"map_dma_buf: paddr=0x%llx, size=%zu\\n",
(unsigned long long)my_buf->paddr,
my_buf->size);
return sgt;
}
/* unmap_dma_buf: 取消映射 */
static void my_unmap_dma_buf(
struct dma_buf_attachment *attach,
struct sg_table *sgt,
enum dma_data_direction dir
) {
struct my_dma_buf *my_buf = attach->dmabuf->priv;
dma_sync_single_for_cpu(
attach->dev, my_buf->paddr,
my_buf->size, dir
);
sg_free_table(sgt);
kfree(sgt);
}
/* release: 所有引用释放后调用,真正释放物理内存 */
static void my_release(struct dma_buf *dmabuf) {
struct my_dma_buf *my_buf = dmabuf->priv;
dev_dbg(my_buf->dev, "释放DMA-BUF, size=%zu\\n", my_buf->size);
/* 释放物理内存 */
dma_free_coherent(
my_buf->dev, my_buf->size,
my_buf->vaddr, my_buf->paddr
);
/* 释放sg_table */
if (my_buf->sgt)
sg_free_table(my_buf->sgt);
kfree(my_buf);
}
/* begin_cpu_access: CPU即将访问缓冲区,需要Invalidate Cache */
static int my_begin_cpu_access(
struct dma_buf *dmabuf,
enum dma_data_direction dir
) {
struct my_dma_buf *my_buf = dmabuf->priv;
dma_sync_single_for_cpu(
my_buf->dev, my_buf->paddr,
my_buf->size, dir
);
return 0;
}
/* end_cpu_access: CPU访问结束,需要Flush Cache */
static int my_end_cpu_access(
struct dma_buf *dmabuf,
enum dma_data_direction dir
) {
struct my_dma_buf *my_buf = dmabuf->priv;
dma_sync_single_for_device(
my_buf->dev, my_buf->paddr,
my_buf->size, dir
);
return 0;
}
/* mmap: 允许用户态映射缓冲区到进程地址空间 */
static int my_mmap(struct dma_buf *dmabuf,
struct vm_area_struct *vma) {
struct my_dma_buf *my_buf = dmabuf->priv;
size_t size = vma->vm_end – vma->vm_start;
if (size > my_buf->size)
return -EINVAL;
/* 将物理内存映射到用户态 */
return dma_mmap_coherent(
my_buf->dev, vma,
my_buf->vaddr, my_buf->paddr,
my_buf->size
);
}
/* 导出DMA-BUF的API:供其他驱动调用 */
struct dma_buf *my_dma_buf_export(
struct device *dev, size_t size
) {
struct my_dma_buf *my_buf;
DEFINE_DMA_BUF_EXPORT_INFO(exp_info);
/* 分配私有数据结构 */
my_buf = kzalloc(sizeof(*my_buf), GFP_KERNEL);
if (!my_buf)
return ERR_PTR(-ENOMEM);
my_buf->dev = dev;
my_buf->size = size;
refcount_set(&my_buf->refcount, 1);
/* 分配物理连续内存(DMA Coherent API) */
my_buf->vaddr = dma_alloc_coherent(
dev, size, &my_buf->paddr, GFP_KERNEL | __GFP_ZERO
);
if (!my_buf->vaddr) {
kfree(my_buf);
return ERR_PTR(-ENOMEM);
}
dev_dbg(dev, "分配DMA缓冲区: vaddr=%p, paddr=0x%llx, size=%zu\\n",
my_buf->vaddr, (unsigned long long)my_buf->paddr, size);
/* 填充导出信息 */
exp_info.ops = &my_dma_buf_ops;
exp_info.size = size;
exp_info.flags = O_CLOEXEC;
exp_info.priv = my_buf;
exp_info.name = "my_dmabuf";
return dma_buf_export(&exp_info);
}
EXPORT_SYMBOL(my_dma_buf_export);
/* 字符设备ioctl接口:允许用户态获取dma-buf fd */
static long my_ioctl(struct file *file, unsigned int cmd,
unsigned long arg) {
struct my_dma_buf *my_buf;
int fd;
switch (cmd) {
case 0xDEADBEEF: /* 自定义ioctl命令:分配并导出DMA-BUF */
size_t size = arg;
struct dma_buf *dbuf = my_dma_buf_export(
/* dev */ NULL, size
);
if (IS_ERR(dbuf))
return PTR_ERR(dbuf);
/* 将dma_buf转换为文件描述符 */
fd = dma_buf_fd(dbuf, O_CLOEXEC);
if (fd < 0) {
dma_buf_put(dbuf);
return fd;
}
return fd; /* 返回给户态 */
default:
return -ENOTTY;
}
}
static const struct file_operations my_fops = {
.unlocked_ioctl = my_ioctl,
.compat_ioioctl = my_ioctl,
};
static struct miscdevice my_miscdev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "dmabuf_exporter",
.fops = &my_fops,
};
static int __init my_init(void) {
return misc_register(&my_miscdev);
}
static void __exit my_exit(void) {
misc_deregister(&my_miscdev);
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
三、ION内存分配器:Android多媒体系统的内存管理核心
ION是Android特有的内存分配框架。它在DMA-BUF框架出现之前就已经存在。后来ION重构,底层完全基于DMA-BUF实现。现在的ION本质上是DMA-BUF的一个高级封装。
ION的核心概念是Heap(堆)。每种Heap代表一种内存类型和分配策略。系统启动时,ION根据设备树或命令行参数注册多个Heap。用户态通过ION接口指定从哪个Heap分配内存。
Android系统中有以下几种标准Heap。System Heap:分配普通内核内存,使用alloc_pages,物理上可能不连续。适用于对物理连续性无要求的场景。PSS Heap:分配页片式物理连续内存,基于CMA。适用于需要物理连续DMA缓冲的场景(Camera、Display)。Carveout Heap:使用预留的固定物理内存区域。适用于需要在特定物理地址范围分配内存的硬件(某些老款SoC的Video Encoder)。
用户态通过 ion_alloc()分配内存。指定size、align、flags和heap_mask。返回的是一个文件描述符,指向底层的DMA-BUF。这个fd可以传递给其他进程或内核驱动。
/*
* ION用户态接口的生产级使用示例
* 编译: gcc -o ion_test ion_test.c -lion
* 注意: 新版本Android使用DMA-BUF接口,
* ION接口已被标记为Deprecated
*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
/* ION旧接口(兼容旧版本Android) */
#include <linux/ion.h>
/* 使用新DMA-BUF接口分配内存(推荐) */
#include <linux/dma-buf.h>
#include <linux/dma-heap.h>
/*
* 方法一:通过ION旧接口分配内存
* 适用于Android 12及以下版本
*/
static int alloc_via_ion_old(int ion_fd, size_t size) {
struct ion_allocation_data alloc_data = {
.len = size,
.align = 0,
.heap_id_mask = 1 << 0, /* System Heap */
.flags = 0,
};
int ret = ioctl(ion_fd, ION_IOC_ALLOC, &alloc_data);
if (ret < 0) {
perror("ION_IOC_ALLOC");
return -1;
}
printf("ION分配成功: fd=%d, len=%zu\\n",
alloc_data.fd, alloc_data.len);
return alloc_data.fd;
}
/*
* 方法二:通过DMA-BUF Heap接口分配内存(新接口)
* 适用于Android 13+和主线Linux内核
* DMA-BUF Heap在/sys/kernel/dma_heap/下暴露设备节点
*/
static int alloc_via_dma_buf_heap(const char *heap_name, size_t size) {
char path[256];
int heap_fd;
int buf_fd;
void *addr;
/* 打开DMA-BUF Heap设备节点 */
snprintf(path, sizeof(path),
"/dev/dma_heap/%s", heap_name);
heap_fd = open(path, O_RDWR);
if (heap_fd < 0) {
/* 尝试旧路径 */
snprintf(path, sizeof(path),
"/sys/kernel/dma_heap/%s", heap_name);
heap_fd = open(path, O_RDWR);
if (heap_fd < 0) {
perror("open dma-heap");
return -1;
}
}
/*
* 使用DMA_BUF_IOCTL_ALLOC分配缓冲区
* 这是新的标准接口,替代ION_IOC_ALLOC
*/
struct dma_buf_alloc_request alloc_req = {
.len = size,
.fd = 0,
.flags = 0,
};
int ret = ioctl(heap_fd, DMA_BUF_IOCTL_ALLOC, &alloc_req);
if (ret < 0) {
perror("DMA_BUF_IOCTL_ALLOC");
close(heap_fd);
return -1;
}
buf_fd = alloc_req.fd;
printf("DMA-BUF Heap分配成功: heap=%s, fd=%d, size=%zu\\n",
heap_name, buf_fd, size);
/* 可选:mmap到用户态访问 */
addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, buf_fd, 0);
if (addr != MAP_FAILED) {
printf(" mmap成功: addr=%p\\n", addr);
/* 写入测试数据 */
memset(addr, 0xAB, 16);
munmap(addr, size);
}
close(heap_fd);
return buf_fd;
}
/*
* 方法三:跨进程共享DMA-BUF(通过Unix Domain Socket)
* 这是多媒体框架中Buffer共享的标准做法
*/
static int share_buf_fd(int sock_fd, int buf_fd) {
/* 通过SCM_RIGHTS辅助消息发送文件描述符 */
struct msghdr msg = {0};
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(sizeof(int))] = {0};
struct iovec iov = {.iov_base = "FD", .iov_len = 3};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
*(int *)CMSG_DATA(cmsg) = buf_fd;
if (sendmsg(sock_fd, &msg, 0) < 0) {
perror("sendmsg");
return -1;
}
printf("已通过Unix Socket共享fd=%d\\n", buf_fd);
return 0;
}
static int recv_buf_fd(int sock_fd) {
/* 接收文件描述符 */
struct msghdr msg = {0};
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(sizeof(int))] = {0};
char dummy[3];
struct iovec iov = {.iov_base = dummy, .iov_len = 3};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
if (recvmsg(sock_fd, &msg, 0) < 0) {
perror("recvmsg");
return -1;
}
cmsg = CMSG_FIRSTHDR(&msg);
if (!cmsg || cmsg->cmsg_type != SCM_RIGHTS) {
fprintf(stderr, "未收到文件描述符\\n");
return -1;
}
int recv_fd = *(int *)CMSG_DATA(cmsg);
printf("接收到共享fd=%d\\n", recv_fd);
return recv_fd;
}
int main(int argc, char *argv[]) {
size_t size = 4 * 1024 * 1024; /* 4MB */
printf("===== DMA-BUF/ION内存分配测试 =====\\n");
/* 测试新接口(DMA-BUF Heap) */
int fd = alloc_via_dma_buf_heap("system", size);
if (fd >= 0) {
/* 测试Sync IOCTL(Cache一致性管理)*/
struct dma_buf_sync sync = {
.flags = DMA_BUF_SYNC_START | DMA_BUF_SYNC_RW
};
if (ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync) == 0) {
printf("DMA_BUF_SYNC成功(Cache Flush/Invalidate)\\n");
}
close(fd);
}
printf("===== 测试完成 =====\\n");
return 0;
}
四、Cache一致性管理与跨设备同步的完整解决方案
DMA-BUF框架最复杂的部分是Cache一致性管理。CPU和DMA控制器共享同一块物理内存。CPU有L1/L2/L3 Cache,数据可能在Cache中。DMA直接访问物理内存,看不到CPU Cache中的数据。
如果CPU写了数据但没Flush Cache,DMA读到的是旧数据。如果DMA写了数据但没Invalidate Cache,CPU读到的是旧数据。
DMA-BUF提供了两种Cache管理机制。第一种是显式同步(Explicit Sync),通过DMA_BUF_IOCTL_SYNC ioctl。用户态在访问缓冲区前后调用Sync IOCTL。START:Invalidate Cache(丢弃CPU Cache中的旧数据)。END:Flush Cache(将CPU写入的数据刷到物理内存)。
第二种是隐式同步,通过begin_cpu_access和end_cpu_access回调。内核驱动在映射/取消映射时自动处理Cache一致性。这种机制对的用户态透明,但灵活性较低。
生产环境中,推荐始终使用显式同步。它让Cache管理的意图更清晰,也更容易调试问题。
"""
DMA-BUF Cache一致性测试工具(Python封装)
用于验证CPU与DMA之间的Cache一致性是否正确
生产环境可用于回归测试和问题排查
"""
import os
import mmap
import ctypes
import ctypes.util
from fcntl import ioctl
# DMA_BUF_IOCTL_SYNC相关常量
DMA_BUF_SYNC_START = 1 << 0
DMA_BUF_SYNC_END = 1 << 1
DMA_BUF_SYNC_RW = 1 << 2
class DmaBufSync(ctypes.Structure):
"""对应内核struct dma_buf_sync"""
_fields_ = [
("flags", ctypes.c_uint64),
]
def dma_buf_sync(fd: int, flags: int) -> bool:
"""调用DMA_BUF_IOCTL_SYNC ioctl"""
DMA_BUF_IOCTL_SYNC = 0xC0044407 # _IOW(DMA_BUF_BASE, 0, struct dma_buf_sync)
sync = DmaBufSync(flags=flags)
try:
ioctl(fd, DMA_BUF_IOCTL_SYNC, sync)
return True
except OSError as e:
print(f"DMA_BUF_SYNC失败: {e}")
return False
def test_cache_coherency(buf_fd: int, size: int) -> bool:
"""
测试Cache一致性:
1. CPU写数据 -> Sync END(Flush Cache)
2. 读取数据 -> Sync START(Invalidate Cache)
3. 验证数据正确性
"""
# mmap缓冲区到用户态
buf = mmap.mmap(buf_fd, size, mmap.MAP_SHARED)
# 测试模式1:CPU写入后Sync
test_data = bytes(range(256)) * (size // 256)
dma_buf_sync(buf_fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_RW)
buf.seek(0)
buf.write(test_data[:1024])
dma_buf_sync(buf_fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_RW)
# 重新读取并验证
dma_buf_sync(buf_fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_RW)
buf.seek(0)
read_back = buf.read(1024)
dma_buf_sync(buf_fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_RW)
passed = read_back == test_data[:1024]
print(f"Cache一致性测试: {'通过' if passed else '失败'}")
buf.close()
return passed
if __name__ == "__main__":
# 需要通过实际dma-heap分配获取fd
# 这里仅展示接口用法
print("DMA-BUF Cache一致性测试工具")
print("使用方法: 传入有效的dma-buf fd")
五、总结
DMA-BUF是Linux内核通用DMA缓冲区共享框架,解决多媒体处理链路中多个硬件模块(Camera→ISP→GPU→Display)之间的Zero-Copy共享内存问题,避免4K视频帧(约25MB)在各模块间拷贝,核心是引用计数管理防止Use-After-Free,用户态通过文件描述符跨进程传递缓冲区
DMA-BUF内核态接口三要素:Exporter(分配内存并实现struct dma_buf_ops回调,关键函数map_dma_buf返回sg_table物理地址列表)、Importer(通过dma_buf_get(fd)获取dma_buf并调用dma_buf_map_attachment获取物理地址配置到硬件DMA寄存器)、dma_buf_fd系统调用将dma_buf转换为可跨进程传递的文件描述符,生产级Exporter需正确实现Cache一致性回调(begin_cpu_access/end_cpu_access)
ION是Android基于DMA-BUF实现的内存分配器,核心概念是Heap(堆),标准Heap类型包括System Heap(普通内核内存,物理可能不连续)、CMA Heap(物理连续DMA内存,基于Contiguous Memory Allocator)、Carveout Heap(预留固定物理地址范围),用户态通过ion_alloc分配并返回dma-buf fd,Android 13+推荐使用新DMA-BUF Heap接口(/dev/dma_heap/设备节点)替代已Deprecated的ION接口
Cache一致性管理的两种机制:显式同步(用户态通过DMA_BUF_IOCTL_SYNC ioctl,START=Invalidate CPU Cache,END=Flush CPU Cache到物理内存,推荐生产环境使用,意图清晰易调试)、隐式同步(内核驱动通过begin_cpu_access/end_cpu_access回调自动处理,对用户态透明但灵活性低),Cache不一致是导致多媒体画面花屏、数据错误的首要原因,必须通过Sync IOCTL正确管理
生产环境ION/DMA-BUF调试方法论:①检查/sys/kernel/debug/dma_buf/目录下的缓冲区引用计数和附件列表、②用lsof /dev/dma_heap/system确认是否有文件描述符泄漏、③通过trace_event dma_buf:* 启用内核Trace跟踪缓冲区生命周期、④跨进程共享时必须用Unix Domain Socket的SCM_RIGHTS辅助消息传递fd(禁止直接传递整数fd值),缓冲区泄漏是导致Android系统内存耗尽的主要原因之一







