Linux多媒体驱动共享内存深度解析:DMA-BUF与ION框架的生产级完整实战
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分配CMAContiguous 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: paddr0x%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, paddr0x%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 IOCTLCache一致性管理*/ 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 CacheDMA读到的是旧数据。如果DMA写了数据但没Invalidate CacheCPU读到的是旧数据。DMA-BUF提供了两种Cache管理机制。第一种是显式同步Explicit Sync通过DMA_BUF_IOCTL_SYNCioctl。用户态在访问缓冲区前后调用Sync IOCTL。STARTInvalidate Cache丢弃CPU Cache中的旧数据。ENDFlush 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(flagsflags) try: ioctl(fd, DMA_BUF_IOCTL_SYNC, sync) return True except OSError as e: print(fDMA_BUF_SYNC失败: {e}) return False def test_cache_coherency(buf_fd: int, size: int) - bool: 测试Cache一致性 1. CPU写数据 - Sync ENDFlush Cache 2. 读取数据 - Sync STARTInvalidate Cache 3. 验证数据正确性 # mmap缓冲区到用户态 buf mmap.mmap(buf_fd, size, mmap.MAP_SHARED) # 测试模式1CPU写入后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(fCache一致性测试: {通过 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_accessION是Android基于DMA-BUF实现的内存分配器核心概念是Heap堆标准Heap类型包括System Heap普通内核内存物理可能不连续、CMA Heap物理连续DMA内存基于Contiguous Memory Allocator、Carveout Heap预留固定物理地址范围用户态通过ion_alloc分配并返回dma-buf fdAndroid 13推荐使用新DMA-BUF Heap接口/dev/dma_heap/设备节点替代已Deprecated的ION接口Cache一致性管理的两种机制显式同步用户态通过DMA_BUF_IOCTL_SYNCioctlSTARTInvalidate CPU CacheENDFlush 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系统内存耗尽的主要原因之一