作者简介
1.dma-buf简介
dma-buf是kernel提供的一个框架,它主要是为了解决不同设备驱动之间buf共享的问题。
2.示例说明
"Talk is cheap. Show me the code.",单纯的文字描述比较抽象,我们通过实验先来看下dma-buf是怎么使用的。
2.1 用户空间
/* test.c */int main(void){/* open dma-heap */heap_fd = open("/dev/dma_heap/global_cma@68000000", O_RDWR);data.len = 1024 * 1024; // 1Mdata.fd = 0;data.fd_flags = O_RDWR | O_CLOEXEC;data.heap_flags = 0;/* alloc buf */ret = ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &data);buf_fd = (int)data.fd;p = mmap(NULL, 1024 * 1024, PROT_READ | PROT_WRITE, MAP_SHARED, buf_fd, 0);/* driver A */fda = open("/dev/driverA", O_RDWR);infoa.fd = buf_fd;infoa.buf = p;infoa.size = 1024 * 1024; // 1Mstrcpy((char *)infoa.buf, "driverA: userspace!");ret = ioctl(fda, TEST_DRIVERA, &infoa);printf("driverA buf = %s\n", infoa.buf);/* driver B */fdb = open("/dev/driverB", O_RDWR);infob.fd = buf_fd;infob.buf = p;infob.size = 1024 * 1024; // 1Mret = ioctl(fda, TEST_DRIVERB, &infob);printf("driverB buf = %s\n", infob.buf);return 0;}
2.2 内核空间
/* test-driver.c */static long test_ioctl(struct file *file, unsigned cmd, unsigned long arg){int i;struct buf_info info;struct scatterlist *sg;if (copy_from_user(&info, (void __user *)arg, sizeof(info)) != 0) {printk("copy_from_user failed\n");return -EFAULT;}printk("fd[%d], buf[0x%px], size[0x%x]\n", info.fd, info.buf, info.size);switch(cmd) {case TEST_DRIVERA:/* for dma access */test_dev.dma_buf = dma_buf_get(info.fd);test_dev.attach = dma_buf_attach(test_dev.dma_buf, test_dev.miscdev.this_device);test_dev.sg = dma_buf_map_attachment(test_dev.attach, DMA_TO_DEVICE);for_each_sg(test_dev.sg->sgl, sg, test_dev.sg->nents, i) {printk("<%s: %d>addr = 0x%08llx, len = 0x%x\n",__FUNCTION__, __LINE__, sg->dma_address, sg->length);}dma_buf_unmap_attachment(test_dev.attach, test_dev.sg, DMA_TO_DEVICE);dma_buf_detach(test_dev.dma_buf, test_dev.attach);dma_buf_put(test_dev.dma_buf);/* for cpu access */dma_buf_vmap(test_dev.dma_buf, &test_dev.map);printk("<%s: %d>addr = 0x%px, str = %s\n",__FUNCTION__, __LINE__, test_dev.map.vaddr, (char *)test_dev.map.vaddr);strcpy((char *)test_dev.map.vaddr, "driverA kernel space!");dma_buf_vunmap(test_dev.dma_buf, &test_dev.map);break;case TEST_DRIVERB:/* for dma access */test_dev.dma_buf = dma_buf_get(info.fd);test_dev.attach = dma_buf_attach(test_dev.dma_buf, test_dev.miscdev.this_device);test_dev.sg = dma_buf_map_attachment(test_dev.attach, DMA_TO_DEVICE);for_each_sg(test_dev.sg->sgl, sg, test_dev.sg->nents, i) {printk("<%s: %d>addr = 0x%08llx, len = 0x%x\n",__FUNCTION__, __LINE__, sg->dma_address, sg->length);}dma_buf_unmap_attachment(test_dev.attach, test_dev.sg, DMA_TO_DEVICE);dma_buf_detach(test_dev.dma_buf, test_dev.attach);dma_buf_put(test_dev.dma_buf);/* for cpu access */dma_buf_vmap(test_dev.dma_buf, &test_dev.map);printk("<%s: %d>addr = 0x%px, str = %s\n",__FUNCTION__, __LINE__, test_dev.map.vaddr, (char *)test_dev.map.vaddr);strcpy((char *)test_dev.map.vaddr, "driverB kernel space!");dma_buf_vunmap(test_dev.dma_buf, &test_dev.map);break;}return 0;}
2.3 测试log
/ # /app/test[ 31.678467] fd[4], buf[0x0000ffffb25b9000], size[0x100000][ 31.684938] <test_ioctl: 48>addr = 0x68100000, len = 0x100000[ 31.688969] <test_ioctl: 57>addr = 0xffff800010ba5000, str = driverA: userspace!driverA buf = driverA kernel space![ 31.701777] fd[4], buf[0x0000ffffb25b9000], size[0x100000][ 31.703808] <test_ioctl: 70>addr = 0x68100000, len = 0x100000[ 31.704973] <test_ioctl: 79>addr = 0xffff800010ca6000, str = driverA kernel space!driverB buf = driverB kernel space!/ #
2.4 代码分析

结合上图及测试程序我们分析下dma-buf的使用流程。
步骤1打开
/dev/dma_heap/global_cma@68000000节点,该节点是kernel提供的dma-heap框架创建的一个节点。步骤2通过
ioctl()从global_cma中申请一块1M大小内存(global_cma是从dts中配置的,以0x68000000为起始地址,大小为128M的内存块),对于test程序来说,申请内存是以data.fd的形式体现,也就是buf_fd。步骤3通过
mmap()函数映射后,就可以对申请的内存进行使用了。步骤4,5打开
/dev/driverA这个节点,通过ioctl()函数将infoa这个结构传递到内核空间。然后继续分析测试log。在
test-drvier.c中(30 ~ 35行)代码,也是通过dma_buf_vmap()接口,可以将dma_buf转化为虚拟地址map.vaddr,这意味着我们就可以通过cpu来操作这块内存了另外打印出内存地址内容(测试log第4行),可以看出就是我们在用户空间往这个地址写的内容
driverA: userspace!最后把内存内容修改,并返回到用户空间也可以看到内存空间已经被修改(测试log第5行)
首先通过
buf_fd可以获取dma_buf结构,然后通过dma_buf_attach()和dma_buf_map_attachment(),函数将dma_buf转化为test_dev.sg结构test_dev.sg结构就可以直接给到dma来使用了,另外打印test_dev.sg结构中的地址可以看出,物理地址是以0x68100000为基址,大小为1M的内存块,正好落在global_cma区域(测试log第3行打印)。在
test-driver.c中(15 ~ 37行)分别测试了dma和cpu两种内存访问方式dma访问内存方式(由于使用qemu环境进行实验,实际没有dma只是从地址打印来看)
cpu访问内存方式
步骤5, 6打开
/dev/driverB这个节点,通过ioctl()函数将infob这个结构传递到内核空间。然后继续分析测试log。该部分代码为
test-driver.c中40 ~ 61行代码,也包含dma和cpu两种内存访问方式主要看下测试log第8行,打印内存内容为
driverA kernel space!,也就是driverA中写入的内容,这说明同一块内存可以在不同的驱动之间进行共享
2.5 示例总结
分析完上面的示例程序,简单总结下
首先通过
/dev/dma_heap/global_cma@68000000节点来从global_cma来申请内存,该内存对用户空间来说是以fd的形式体现,经过mmap()函数映射后,我们就可以对这块内存进行操作了。将
fd传递到内核空间后,驱动程序可以通过fd获取其所绑定的dma_buf结构,通过dma_buf结构,我们可以得到sg结构用于dma对内存访问,得到map.vaddr结构用于cpu对内存访问。另外buf可以在不同驱动之间进行共享,对用户空间buf体现为
fd,对kernel空间buf体现为dma_buf结构,而kernel提供的dma-buf框架用于维护两者的关系从而实现不同驱动之间的buf共享。
2.6 示例扩展

上面的示例程序中只有一个buf,可以进一步扩展为多个buf
test程序把待处理的数据通过ioctl()接口put到driverA的链表上,当driverA处理完后,可以把处理好的的数据放到另一个链表上,然后通知上层来取(比如通过poll/select机制来通知上层)。上层
test层拿到driverA处理的数据又可以通过ioctl()接口put到driverB的链表上,当driverB处理完后,又可以把处理好的数据放到另一个链表上,然后通知上层来取。当然可以有很多这样的驱动程序,前一级的输出结果是下一级的输入,这样一级一级处理,而无须数据在用户空间与内核空间不停的拷贝。
3.dma-buf框架
前面也提到了dma-buf框架是用来管理fd与dma_buf之前关系的,接下来我们介绍下dma-buf框架是怎么实现的。

3.1 exporter和importer
在dma-buf框架中,有两个概念exporter和importer,如上图蓝色虚线框为exporter,紫色虚线框为importer。
exporter是buf的管理者,主要作用如下实现对buf的分配管理,比如,buf从那里分配,分配多少;这些是
exporter实现的,像上面cma-heap从名字就可以知道,buf是从cma分配的,用户只需要打开/dev/dma_heap/global_cma@68000000这个节点申请内存就可以,具体内存怎么申请的,用户是无需关心的。实现对buf的操作,比如在用户空间想要操作这块buf需要先map一下,在内核空间相关通过dma或者cpu来访问该buf,或者做cache同步,这些都是由
exporter来实现的,如图中左侧的cma_heap_buf_ops结构,就是该实现的一组操作函数。另外为什么需要由exporter来实现呢?因为exporter是清楚这些内存从那里申请的,因此当然清楚怎么map,所以由exporter来实现这些操作是再合理不过的。importer是buf的使用者,对于buf的使用者来说就很简单了,只需要拿到buf的fd,然后通过dma-buf框架提供的接口,就可以对buf的数据进行操作了(前面示例程序也展示了)。
3.2 dma-heap框架
正常来说exporter和importer都需要我们自己实现的。比如在嵌入式系统中(有图像处理的场景中),一般需要划分出一段内存区域,用于图像数据的处理,对于该段内存的管理可以使用kernel现有的机制比如cma、保留内存等,当然我们也可以实现一个exporter而将内存管理起来;而对于importer往往就是内存的一使用者,一般来说就是我们自己写的驱动程序,比如上面的driverA,driverB测试程序。
kernel提供的dma-heap的框架,就是一个exporter,其实也非常简单,主要包含下面三个源文件。
drivers/dma-buf/dma-heap.c # 实现dma-heap的框架drivers/dma-buf/heaps/cma_heap.c # 管理从cma申请的内存drivers/dma-buf/heaps/system_heap.c # 管理从system申请的内存
dam-heap.c是一个抽象的接口层,为具体的heap(cma/system)提供注册机制,创建字符设备节点cam_heap.c用于管理从cma申请的内存system_heap.c用于管理从system申请的内存
dma-heap.c
/* drivers/dma-buf/dma-heap.c */struct dma_heap *dma_heap_add(const struct dma_heap_export_info *exp_info){...heap = kzalloc(sizeof(*heap), GFP_KERNEL);if (!heap)return ERR_PTR(-ENOMEM);heap->name = exp_info->name;heap->ops = exp_info->ops;heap->priv = exp_info->priv;/* Find unused minor number */ret = xa_alloc(&dma_heap_minors, &minor, heap, XA_LIMIT(0, NUM_HEAP_MINORS - 1), GFP_KERNEL);if (ret < 0) {pr_err("dma_heap: Unable to get minor number for heap\n");err_ret = ERR_PTR(ret);goto err0;}/* Create device */heap->heap_devt = MKDEV(MAJOR(dma_heap_devt), minor);cdev_init(&heap->heap_cdev, &dma_heap_fops);ret = cdev_add(&heap->heap_cdev, heap->heap_devt, 1);dev_ret = device_create(dma_heap_class, NULL, heap->heap_devt, NULL, heap->name);...return heap;}static long dma_heap_ioctl(struct file *file, unsigned int ucmd,unsigned long arg){...switch (kcmd) {case DMA_HEAP_IOCTL_ALLOC:ret = dma_heap_ioctl_allocate(file, kdata);break;default:ret = -ENOTTY;goto err;}...}
dma-heap.c中提供的两个主要函数dma_heap_add()和dma_heap_ioctl(),像cma_heap、systemp_heap会通过dma_heap_add()函数注册到dma-heap框架来,dma-heap会分别为他们创建字符设备节点供用户空间使用,并且提供一个通用的dma_heap_fops(24行代码)。通用的dma_heap_fops中也简单的提供了一个ioctl函数,并且只有一个命令DMA_HEAP_IOCTL_ALLOC(如上dma_heap_ioctl()函数),当然dam-heap具体是不清楚对应的heap就从那里申请内存,怎么申请的,它只是通过回调函数走到对应的heap中(简单粗暴)。
cma_heap.c
/* drivers/dma-buf/heaps/cma_heap.c */static const struct dma_buf_ops cma_heap_buf_ops = {.attach = cma_heap_attach,.detach = cma_heap_detach,.map_dma_buf = cma_heap_map_dma_buf,.unmap_dma_buf = cma_heap_unmap_dma_buf,.begin_cpu_access = cma_heap_dma_buf_begin_cpu_access,.end_cpu_access = cma_heap_dma_buf_end_cpu_access,.mmap = cma_heap_mmap,.vmap = cma_heap_vmap,.vunmap = cma_heap_vunmap,.release = cma_heap_dma_buf_release,};static struct dma_buf *cma_heap_allocate(struct dma_heap *heap,unsigned long len,unsigned long fd_flags,unsigned long heap_flags){...cma_pages = cma_alloc(cma_heap->cma, pagecount, align, false);if (!cma_pages)goto free_buffer;..../* create the dmabuf */exp_info.ops = &cma_heap_buf_ops;exp_info.size = buffer->len;exp_info.flags = fd_flags;exp_info.priv = buffer;dmabuf = dma_buf_export(&exp_info);if (IS_ERR(dmabuf)) {ret = PTR_ERR(dmabuf);goto free_pages;}return dmabuf;...}
书接上文,当用户想要申请一块内存时,通过dma-heap的中转走到cma_heap.c的cma_heap_allocate()函数中,可以看到申请是从cma申请的(第21行);关键看27 ~ 31行代码,通过dma_buf_export()函数就把该内存放到dma-buf框架管理起来了。
另外需要关注cma_heap_buf_ops操作函数集(第27行),对于该组函数从名字来看是不是很熟悉,当使用dma-buf框架提供的map函数时,dma-buf也不知道对应的内存该怎样map,最清楚的还是exporter本身,所以定义了一组回调函数,由exporter来自己来实现(简单粗暴)。
小结
大家可能都知道android的ion,其实它和这里的dma-heap是处于相同地位的,只不过ion的实现更复杂。它除了可以在用户空间申请内存外,内核空间也提供了相应的接口,另外它还有一套自己的buf管理机制以及对不同类型buf管理的支持,但底层都是基于dma-buf来实现的。
3.3 dma-buf框架
前面说了这么多,终于来到了dma-buf框架,但实际dma-buf框架也是相当简单的,主要是学习它这种解决问题的思路。
dma_buf_export()函数
/* driver/dma-buf/dma-buf.c */struct dma_buf *dma_buf_export(const struct dma_buf_export_info *exp_info){...dmabuf = kzalloc(alloc_size, GFP_KERNEL);if (!dmabuf) {ret = -ENOMEM;goto err_module;}dmabuf->priv = exp_info->priv;dmabuf->ops = exp_info->ops;dmabuf->size = exp_info->size;dmabuf->exp_name = exp_info->exp_name;dmabuf->owner = exp_info->owner;spin_lock_init(&dmabuf->name_lock);init_waitqueue_head(&dmabuf->poll);dmabuf->cb_excl.poll = dmabuf->cb_shared.poll = &dmabuf->poll;dmabuf->cb_excl.active = dmabuf->cb_shared.active = 0;if (!resv) {resv = (struct dma_resv *)&dmabuf[1];dma_resv_init(resv);}dmabuf->resv = resv;file = dma_buf_getfile(dmabuf, exp_info->flags);if (IS_ERR(file)) {ret = PTR_ERR(file);goto err_dmabuf;}file->f_mode |= FMODE_LSEEK;dmabuf->file = file;mutex_lock(&db_list.lock);list_add(&dmabuf->list_node, &db_list.head);mutex_unlock(&db_list.lock);return dmabuf;}
前面有提到,对用户空间buf体现为fd,对kernel空间buf体现为dma_buf结构,dma-buf框架用于维护两者的关系,从上面代码可以看到,buf信息首先被包装到一个dma_buf结构中(第12 ~ 26行),然后通过dma_buf_getfile()函数将buf与fd绑定(第28 ~ 35行),最后将dma_buf结构放到一个链表维护起来(38行),是不是相当的简单。
dma_buf_getfile()函数
/* driver/dma-buf/dma-buf.c */static const struct file_operations dma_buf_fops = {.release = dma_buf_file_release,.mmap = dma_buf_mmap_internal,.llseek = dma_buf_llseek,.poll = dma_buf_poll,.unlocked_ioctl = dma_buf_ioctl,.compat_ioctl = compat_ptr_ioctl,.show_fdinfo = dma_buf_show_fdinfo,};static struct file *dma_buf_getfile(struct dma_buf *dmabuf, int flags){struct inode *inode = alloc_anon_inode(dma_buf_mnt->mnt_sb);inode->i_size = dmabuf->size;inode_set_bytes(inode, dmabuf->size);file = alloc_file_pseudo(inode, dma_buf_mnt, "dmabuf", flags, &dma_buf_fops);if (IS_ERR(file))goto err_alloc_file;file->f_flags = flags & (O_ACCMODE | O_NONBLOCK);file->private_data = dmabuf;file->f_path.dentry->d_fsdata = dmabuf;return file;}
再来看一眼dma_buf_getfile()函数,会通过系统申请一个file结构,并为该file绑定一组操作函数集dma_buf_fops,还记得前面test测试程序中,有一步map操作吧,该操作函数集也实现了mmap()函数,我们可以想象mmap()的实现方式,对于dma-buf框架来说,它是不清楚要怎么对内存map的,所以需要exporter自己来实现,如下为dma_buf_mmap_internal()函数实现。
/* driver/dma-buf/dma-buf.c */static int dma_buf_mmap_internal(struct file *file, struct vm_area_struct *vma){struct dma_buf *dmabuf;dmabuf = file->private_data;/* check if buffer supports mmap */if (!dmabuf->ops->mmap)return -EINVAL;return dmabuf->ops->mmap(dmabuf, vma);}
dma-buf框架直接通过回调函数调到exporter中(第12行),也这印证了我们前面的猜想(虽然事先看过代码????) ,这里的mmap()函数是提供给用户空间使用的,像内核空间的dma_buf_attach()、dma_buf_map_attachment()和dma_buf_vmap()等函数,也都是这种回调的实现方式。
小结
经过前面的分析,dma-buf框架还是非常简单的,每一个buf都被包装成一个dma_buf结构,然后每一个dma_buf结构又会绑定一个file结构,最后将所有的dma_buf放到链表上维护,这样在内核空间通过遍历该链表就可以由fd到找对应的dma_buf结构。另外每一个file结构还会绑定一组通用的函数操作集,然后通过回调函数的方式,调到对应的exporter中,这是给用户空间提供的接口;内核空间的dma_buf_xxx()接口也都采用了正常的实现方式。
参考
https://blog.csdn.net/hexiaolong2009/article/details/102596744
https://www.cnblogs.com/yaongtime/p/14594567.html
https://www.kernel.org/doc/html/latest/driver-api/dma-buf.html#
如果你觉得你现在走得辛苦,那就证明你在走上坡路。????