本文最后更新于:2024年12月31日 上午
世界第一 DMA-BUF 大师是对的!
0x00. 一切开始之前 最近刚好看到一个与 DMA buffer 相关的有意思的东西,所以简单写一篇博客记录一下这是个什么玩意:)
本文涉及到的内核版本为 6.11.2
0x01. DMA-BUF - 简介 基本原理 dma-buf 子系统 为不同设备驱动与其他子系统提供了一个方便统一的 内存共享框架 ,常用于 GPU 及显示驱动等,其主要有三个用于交互的部分:
dma-buf
:表示一个 sg_table
,并作为文件描述符导出到用户空间以允许在进程、子系统、设备间传递(注:UNIX domain socket)
dma-fence
:提供了对异步硬件操作是否完成的检测机制
dma-resv
:为一个特定的 dma-buf
管理一组 dma-fence
,通过隐式的(内核排序的)同步工作以保持表面上的一致访问
在 dma-buf 框架中主要有两个对象,分别是:
exporter
:共享内存的生产者(内核驱动)
负责为内存实现 struct dma_buf_ops
操作函数表
允许通过 dma_buf API 共享内存
通过 struct dma_buf
管理内存分配的细节
决定实际的分配,并负责所有共享用户的 scatterlist (page 为单位的物理连续内存块)迁移
importer/user
:共享内存的消费者(可以是用户态程序也可以是内核态驱动)
不关心内存来源
通过 struct dma_buf_attachment
接口访问共享内存(一般通过共享的文件描述符获取)
扩展阅读:sg_table 与 scatterlist - 离散连续内存 简而言之,单个 scatterlist
用来表示一块 物理连续 的 以页为单位 的内存块,sg_table
则表示一个离散 scatterlist
数组 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct scatterlist { unsigned long page_link; unsigned int offset; unsigned int length; dma_addr_t dma_address;#ifdef CONFIG_NEED_SG_DMA_LENGTH unsigned int dma_length;#endif #ifdef CONFIG_NEED_SG_DMA_FLAGS unsigned int dma_flags;#endif };struct sg_table { struct scatterlist *sgl ; unsigned int nents; unsigned int orig_nents; };
其通常结构如下图所示:
对于分配的 scatterlist 数组长度大于一张内存页的情况,其会被分割成多块,其中单张 scatterlist 数组页面上的最后一个成员的 page_link
字段会用于标识是否存在下一份页面以及下一份 scatterlist 页面所在内存页:
具体而言,dma-buf 的通常使用流程如下:
exporter 侧用 DEFINE_DMA_BUF_EXPORT_INFO()
创建一个临时 dma_buf_export_info
结构体,并定义导出信息,这通常包括:
::ops
:自定义的 struct dma_buf_ops
::priv
:自定义私有数据的指针
::size
:共享内存大小
::flags
:内存读写权限
exporter 侧调用 dma_buf_export()
创建 dma_buf
对象
exporter 侧调用 dma_buf_fd()
将 dma_buf
对象关联到一个新的文件描述符 fd
中(注:使用 fd 进行传递是比较常见的方式,但不唯一)
exporter 侧将 fd
传给 importer
importer 调用 dma_buf_get(fd)
获取 dma_buf
对象
importer 调用 dma_buf_attach()
和 dma_buf_map_attachment()
获取共享缓存的信息
importer 使用 sg_table
的内容
importer 调用 dma_buf_detach()
和 dma_buf_unmap_attachment()
释放对 dma_buf
的引用
importer 调用 dma_buf_put(fd)
释放文件描述符
对于涉及用户态的 DMA-BUF 使用而言,将 dma_buf
封装到 fd 当中通常是必须的:exporter 获取共享内存对应的 dma-buf
的文件描述符 fd
,通过 ioctl 或是 UNIX domain socket 等方式发送给 importer,importer 再 mmap(fd)
以访问共享内存:
需要注意的是 exporter 与 importer 并不总是分离的,也可以有同时存在于同一驱动的情况(例如 DRM)
dma_buf 结构体 在 dmabuf 框架中,struct dma_buf
是一个重要的结构体,其用于表示一个共享内存对象,包括内存大小、绑定的文件描述符等:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 struct dma_buf { size_t size; struct file *file ; struct list_head attachments ; const struct dma_buf_ops *ops ; unsigned vmapping_counter; struct iosys_map vmap_ptr ; const char *exp_name; const char *name; spinlock_t name_lock; struct module *owner ;#if IS_ENABLED(CONFIG_DEBUG_FS) struct list_head list_node ;#endif void *priv; struct dma_resv *resv ; wait_queue_head_t poll; struct dma_buf_poll_cb_t { struct dma_fence_cb cb ; wait_queue_head_t *poll; __poll_t active; } cb_in, cb_out;#ifdef CONFIG_DMABUF_SYSFS_STATS struct dma_buf_sysfs_entry { struct kobject kobj ; struct dma_buf *dmabuf ; } *sysfs_entry;#endif };
比较关键的字段如下:
size
:共享内存的大小,在 dmabuf 的生命周期中不会发生改变
file
: 与 dmabuf 相关联的文件描述符,供用户态操作
attachments
:
ops
:dmabuf 操作函数表
priv
:即 private,私有字段,你可以在其中按需存放数据
对于 exporter 侧而言,其需要根据需求自行实现 struct dma_buf_ops
当中的相应操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct dma_buf_ops { int (*attach)(struct dma_buf *, struct dma_buf_attachment *); void (*detach)(struct dma_buf *, struct dma_buf_attachment *); int (*pin)(struct dma_buf_attachment *attach); void (*unpin)(struct dma_buf_attachment *attach); struct sg_table * (*map_dma_buf )(struct dma_buf_attachment *, enum dma_data_direction ); void (*unmap_dma_buf)(struct dma_buf_attachment *, struct sg_table *, enum dma_data_direction); void (*release)(struct dma_buf *); int (*begin_cpu_access)(struct dma_buf *, enum dma_data_direction); int (*end_cpu_access)(struct dma_buf *, enum dma_data_direction); int (*mmap)(struct dma_buf *, struct vm_area_struct *vma); int (*vmap)(struct dma_buf *dmabuf, struct iosys_map *map ); void (*vunmap)(struct dma_buf *dmabuf, struct iosys_map *map ); };
所有可用操作包括:
attach
:用于建立 dma-buf 与设备间的连接关系,并存放中新创建的 struct dma_buf_attachment
对象中
detach
:
pin
:
unpin
:
map_dma_buf
:
unmap_dma_buf
:
release
:
begin_cpu_access
:
end_cpu_access
:
mmap
:
vmap
:
vunmap
:
0x02. 简易 DMA-BUF 开发 前面讲到 dmabuf 的基本操作序列如下图所示,我们后续将以此为示例进行开发
需要注意的是 DMA-BUF 相关函数在 DMA_BUF
命名空间中,我们应当在内核模块代码中进行导入:
1 MODULE_IMPORT_NS(DMA_BUF);
基本的 exporter 对于 exporter 而言,最重要的是实现 struct dma_buf_ops
中不可或缺的基本操作
map_dma_buf
:映射 dma-buf 共享内存该函数指针的作用主要是将 dma-buf 的共享内存进行映射,并将结果写到到一个 sg_table
结构当中,下面是一个最简单的示例,我们假定在此 dma_buf
中已经通过 kmalloc()
分配了内存并存放到 dma_buf::priv
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 static struct sg_table*a3exporter_map_dma_buf (struct dma_buf_attachment *attachment, enum dma_data_direction direction) { struct sg_table *table ; struct dma_buf *buf ; int ret; void *err; table = kzalloc(sizeof (*table), GFP_KERNEL); if (!table) { printk(KERN_ERR "[a3exporter:] Failed to allocate memory for sg_table!\n" ); err = ERR_PTR(-ENOMEM); goto err_table_no_mem; } ret = sg_alloc_table(table, 1 , GFP_KERNEL); if (unlikely(ret)) { printk(KERN_ERR "[a3exporter:] Failed to allocate scatterlist!\n" ); err = ERR_PTR(ret); goto err_sg_alloc; } buf = attachment->dmabuf; sg_dma_len(table->sgl) = buf->size; sg_dma_address(table->sgl) = dma_map_single(mdev, buf->priv, buf->size, direction); if (dma_mapping_error(mdev, sg_dma_address(table->sgl))) { printk(KERN_ERR "[a3exporter:] Unable to map for dmabuf!\n" ); err = ERR_PTR(-EFAULT); goto err_dma_map; } return table; err_dma_map: sg_free_table(table); err_sg_alloc: kfree(table); err_table_no_mem: return err; }
unmap_dma_buf
: 解除 dma-buf 共享内存映射这个函数指针的作用主要就是将 sg_table
相关的内存释放掉,并解除掉 dmabuf 的内存映射,下面是一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 static void a3exporter_unmap_dma_buf (struct dma_buf_attachment *attachment, struct sg_table *table, enum dma_data_direction direction) { dma_unmap_single(mdev, sg_dma_address(table->sgl), sg_dma_len(table->sgl), direction); sg_free_table(table); kfree(table); }
release
: 释放 dma-buf 的内存该函数指针的作用是真正释放掉 dmabuf 中的内存,下面是一个简单的例子,这里我们假定我们的 dmabuf 中的内存是通过 kmalloc()
进行分配的:
1 2 3 4 static void a3exporter_buf_release (struct dma_buf *buf) { kfree(buf->priv); }
定义 dma_buf
定义了上面这三个函数之后,我们的 dma_buf
的基本功能算是完备了,现在我们可以开始进行 dma_buf
的分配与导出,这里我们给出一个简单的示例,在 exporter 驱动的初始化函数中分配一块常规的内存并包装到 dma_buf
中,为了简化开发流程且方便 importer 驱动使用,这里我们直接将 a3dmabuf
作为一个符号导出:
这里我们在 a3exporter_create_dev()
中创建了一个空白设备节点,仅用作 DMA 占位,其实现比较简单故此处不再给出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 static struct device *mdev = NULL ;static struct dma_buf_ops a3exporter_dmabuf_ops = { .map_dma_buf = a3exporter_map_dma_buf, .unmap_dma_buf = a3exporter_unmap_dma_buf, .release = a3exporter_buf_release, };struct dma_buf *a3dmabuf = NULL ; EXPORT_SYMBOL(a3dmabuf);static int __init a3exporter_init (void ) { DEFINE_DMA_BUF_EXPORT_INFO(exp_info); char *buf; int err; err = a3exporter_create_dev(); if (err) { printk(KERN_ERR "[a3exporter:] Unable to create exporter device, " "error code: %d\n" , err); goto err_dev; } buf = kmalloc(PAGE_SIZE * 8 , GFP_KERNEL); if (!buf) { printk(KERN_ERR "[a3exporter:] Failed to allocate memory for buf!\n" ); err = -ENOMEM; goto err_buf_no_mem; } for (int i = 0 ; i < 8 ; i++) { *(uint64_t *) &buf[PAGE_SIZE * i] = *(uint64_t *) "arttnba0" ; buf[PAGE_SIZE * i + 7 ] += i; } exp_info.ops = &a3exporter_dmabuf_ops; exp_info.size = PAGE_SIZE * 8 ; exp_info.priv = buf; exp_info.flags = O_RDWR | O_CLOEXEC; a3dmabuf = dma_buf_export(&exp_info); if (IS_ERR(a3dmabuf)) { printk(KERN_ERR "[a3exporter:] Failed to allocate dma_buf!\n" ); err = PTR_ERR(a3dmabuf); goto err_exp_info; } printk(KERN_INFO "[a3exporter:] Module initialization done.\n" ); return 0 ; err_exp_info: kfree(buf); err_buf_no_mem: err_dev: class_destroy(mclass); unregister_chrdev(major_num, DEVICE_NAME); return err; }static void __exit a3exporter_exit (void ) { if (a3dmabuf) { dma_buf_put(a3dmabuf); a3dmabuf = NULL ; } class_destroy(mclass); unregister_chrdev(major_num, DEVICE_NAME); printk(KERN_INFO "[a3exporter:] See you next time.\n" ); }
importer 驱动简单示例 接下来编写测试用的 importer 驱动,要在内核空间中使用一个 dma_buf
,我们只需要如下步骤:
获取 dma_buf
并增加引用计数,这里注意 dma_buf
的引用计数是通过文件描述符 dma_buf::fd
进行记录的
将所选设备节点 struct device
给 attach 到 dma_buf
上
调用 map_dma_buf()
获取 sg_table
接下来直接访问 sg_table->scatterlist
即可,需要注意的是这里是一个给外设使用的 DMA 地址,而我们只是在内核中测试使用,因此我们需要再转回内核虚拟地址,下面是一个简易的示例 importer 代码核心部分:
以及需要注意的是 DMA-BUF 依赖于设备节点,因此这里我们使用 a3importer_create_dev()
创造一个占位设备,代码比较简单这里不再展开
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include <linux/module.h> #include <linux/kernel.h> #include <linux/dma-buf.h> MODULE_IMPORT_NS(DMA_BUF);extern struct dma_buf *a3dmabuf ;static struct device *mdev = NULL ;static int a3importer_import_dmabuf (void ) { struct dma_buf_attachment *attach ; enum dma_data_direction direction = DMA_BIDIRECTIONAL; struct sg_table *sgt ; struct scatterlist *sg ; size_t bufsz; char *buf; int i; if (!a3dmabuf) { printk(KERN_ERR "[a3importer:] No dma_buf got! " "Exporter not working properly?\n" ); return -EFAULT; } attach = dma_buf_attach(a3dmabuf, mdev); if (IS_ERR(attach)) { printk(KERN_ERR "[a3importer:] Unable to attach to dma_buf!\n" ); return PTR_ERR(attach); } sgt = dma_buf_map_attachment(attach, direction); if (IS_ERR(sgt)) { printk(KERN_ERR "[a3importer:] Unable to map the dma_buf!\n" ); return PTR_ERR(sgt); } for_each_sg(sgt->sgl, sg, sgt->nents, i) { buf = (char *) sg_dma_address(sg) + page_offset_base; bufsz = sg_dma_len(sg); for (size_t loc = 0 ; loc < bufsz; loc += PAGE_SIZE) { printk(KERN_INFO "[a3importer:] Got data: %s\n" , buf + loc); } } dma_buf_unmap_attachment(attach, sgt, direction); dma_buf_detach(a3dmabuf, attach); return 0 ; }static int __init a3importer_init (void ) { barrier(); return a3importer_create_dev() || a3importer_import_dmabuf(); }
Makefile 编写如下,别忘了替换 kernel 路径:
1 2 3 4 5 6 7 8 9 CURRENT_PATH := $(shell pwd) LINUX_KERNEL_SRC := your_path_to_kernel_build,e.g.,/lib/modules/$(shell uname -r) /build CC=clangall: make CC=$(CC) -C $(LINUX_KERNEL_SRC) M=$(CURRENT_PATH) modulesclean: make CC=$(CC) -C $(LINUX_KERNEL_SRC) M=$(CURRENT_PATH) clean
Kbuild 编写如下:
1 2 3 4 5 6 7 8 EXPORTER_MODULE_NAME ?= exporter-test IMPORTER_MODULE_NAME ?= importer-test obj-m += $(EXPORTER_MODULE_NAME).o obj-m += $(IMPORTER_MODULE_NAME).o $(EXPORTER_MODULE_NAME)-y += exporter.o $(IMPORTER_MODULE_NAME)-y += importer.o
测试,成功完成内核驱动间的数据传递:
importer 应用程序示例 接下来我们看一种可能更常见的 DMA-BUF 的使用场景:由内核态 exporter 驱动导出给用户态应用程序,在这种场景下 exporter 需要将 dma_buf
通过 dma_buf_fd()
封装到一个 新分配的文件描述符 当中传递给用户态,用户态再通过 mmap()
映射该文件描述符以使用这块共享内存
这里我们直接在设备节点的 ioctl()
当中实现该功能,在上面的驱动代码中增加代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 static long a3exporter_ioctl (struct file *filp, unsigned int cmd, unsigned long args) { int buf_fd; if (cmd != 0xdeadbeef ) { return -EINVAL; } buf_fd = dma_buf_fd(a3dmabuf, O_CLOEXEC); if (buf_fd < 0 ) { printk("[a3exporter:] Unable to allocate new file descriptor.\n" ); return -EMFILE; } if (copy_to_user((void *) args, &buf_fd, sizeof (buf_fd))) { printk("[a3exporter:] Unable to copy dma_buf fd to userland.\n" ); return -EFAULT; } return 0 ; }static struct file_operations a3exporter_dev_fops = { .unlocked_ioctl = a3exporter_ioctl,
此外,由于用户态通过调用 mmap()
来使用这块内存,因此我们还需要在 exporter 驱动当中实现 dma_buf_ops::mmap()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 static int a3exporter_buf_mmap (struct dma_buf *buf, struct vm_area_struct *vma) { return remap_pfn_range( vma, vma->vm_start, page_to_pfn(virt_to_page(buf->priv)), buf->size, vma->vm_page_prot ); }static struct dma_buf_ops a3exporter_dmabuf_ops = { .mmap = a3exporter_buf_mmap,
下面我们实现 importer 应用程序,我们只需要用 ioctl()
获取 dma_buf
的文件描述符并用 mmap()
进行映射即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <stdint.h> #include <sys/ioctl.h> #include <sys/mman.h> int main (int argc, char **argv, char **envp) { int dma_fd, dev_fd; char *buf; dev_fd = open("/dev/a3exporter-dev" , O_RDONLY); if (dev_fd < 0 ) { perror("Failed to open dev file" ); exit (EXIT_FAILURE); } if (ioctl(dev_fd, 0xdeadbeef , &dma_fd) < 0 ) { perror("Failed to ioctl dev file" ); exit (EXIT_FAILURE); } buf = mmap(NULL , 0x1000 * 8 , PROT_READ|PROT_WRITE, MAP_FILE | MAP_SHARED, dma_fd, 0 ); if (!buf) { perror("Failed to map dma_fd" ); exit (EXIT_FAILURE); } for (int i = 0 ; i < 8 ; i++) { printf ("Get data: %s\n" , &buf[0x1000 * i]); } return 0 ; }
测试,成功完成内核到用户态的数据传递:
当然,内核的开发实际上是非常灵活的,事实上,我们可以直接实现设备文件接口的 mmap()
并在其中映射 dma_buf
,从而简化代码流程:)
0x03. udmabuf:简易封装接口 得益于 dma_buf
的好用,Linux kernel 于 2018 年 引入了一个封装好的 dma_buf
设备驱动——udmabuf
,其在内核中实现了一个封装好的 exporter,并支持通过 ioctl()
创建 dma_buf
以在用户态程序之间进行内存共享
使用方式 udmabuf
的基本使用方式非常简单:
使用 memfd_create()
创建一个文件描述符
封装 udmabuf_create
请求,通过 ioctl("/dev/udmabuf")
获取 dma_buf
的 fd
通过 mmap()
进行访问
有了 dma_buf
的 fd,后面的内存访问操作就和之前没什么区别了,在进程间进行共享只需要共享这个文件描述符即可(例如使用 UNIX domain socket)
这里给出一个示例程序,文件描述符直接通过 fork()
共享:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <stdint.h> #include <string.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <sys/wait.h> #include <linux/udmabuf.h> int main (int argc, char **argv, char **envp) { int dma_fd, dev_fd, mem_fd; char *buf1, *buf2; struct udmabuf_create info ; mem_fd = memfd_create("test" , MFD_ALLOW_SEALING); if (mem_fd < 0 ) { perror("Failed to create mem_fd" ); exit (EXIT_FAILURE); } if (ftruncate(mem_fd, 0x1000 * 8 ) < 0 ) { perror("Failed to change size of mem_fd" ); exit (EXIT_FAILURE); } if (fcntl(mem_fd, F_ADD_SEALS, F_SEAL_SHRINK) < 0 ) { perror("Failed to seal mem_fd" ); exit (EXIT_FAILURE); } dev_fd = open("/dev/udmabuf" , O_RDWR); if (dev_fd < 0 ) { perror("Failed to open udmabuf dev file" ); exit (EXIT_FAILURE); } memset (&info, 0 , sizeof (info)); info.memfd = mem_fd; info.size = 0x1000 * 8 ; dma_fd = ioctl(dev_fd, UDMABUF_CREATE, &info); if (dma_fd < 0 ) { perror("Failed to create dma_buf" ); exit (EXIT_FAILURE); } buf1 = mmap(NULL , 0x1000 * 8 , PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, dma_fd, 0 ); if (!buf1) { perror("Failed to map dma_fd" ); exit (EXIT_FAILURE); } for (int i = 0 ; i < 8 ; i++) { *(size_t *) &buf1[i * 0x1000 ] = *(size_t *) "arttnba0" ; buf1[i * 0x1000 + 7 ] += i; } if (!fork()) { buf2 = mmap(NULL , 0x1000 * 8 , PROT_READ|PROT_WRITE, MAP_FILE | MAP_SHARED, dma_fd, 0 ); if (!buf2) { perror("Failed to map dma_fd" ); exit (EXIT_FAILURE); } for (int i = 0 ; i < 8 ; i++) { printf ("Get data: %s\n" , &buf2[0x1000 * i]); } } else { wait(NULL ); } return 0 ; }
测试,成功完成数据传递:
内部实现(🕊) udmabuf 的实现非常简单,主要就是注册了一个杂项设备 /dev/udmabuf
,并支持两个自定义的 IOCTL 命令:
/drivers/dma-buf/udmabuf.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 static long udmabuf_ioctl (struct file *filp, unsigned int ioctl, unsigned long arg) { long ret; switch (ioctl) { case UDMABUF_CREATE: ret = udmabuf_ioctl_create(filp, arg); break ; case UDMABUF_CREATE_LIST: ret = udmabuf_ioctl_create_list(filp, arg); break ; default : ret = -ENOTTY; break ; } return ret; }static const struct file_operations udmabuf_fops = { .owner = THIS_MODULE, .unlocked_ioctl = udmabuf_ioctl,#ifdef CONFIG_COMPAT .compat_ioctl = udmabuf_ioctl,#endif };static struct miscdevice udmabuf_misc = { .minor = MISC_DYNAMIC_MINOR, .name = "udmabuf" , .fops = &udmabuf_fops, };