DMA-BUF基本概念

简介

如果设备驱动想要共享DMA缓冲区,可以让一个驱动来导出,一个驱动来使用。类似于消费者生产者模型,Linux DMA-BUF就是基于这种方式来实现的。

Linux内核中的DMA BUF这部分代码主要由Sumit Semwal贡献,可以在Linux内核源码树目录下执行如下命令查看相关提交。

git log --author="Sumit Semwal"

生产者要完成的工作:

  • 实现对缓冲区的管理操作
  • 允许使用者通过dma_buf共享接口来共享内存
  • 管理内存分配的细节
  • 决定实际的存储位置
  • 管理散列表迁移

使用者的好处:

  • 可以有很多使用者共用这部分内存
  • 不需要关心怎样分配,以及分配在何处
  • 需要一个入口去访问散列表,以映射到自己的地址空间

基本用法

DMA BUF只能操作设备DMA缓冲区,使用上包括如下步骤。

  1. 生产者声明要导出缓冲
  2. 用户空间获取关联缓冲区的文件描述符,将描述符传递给潜在的消费者
  3. 在使用前,每个用户将自己连接到缓冲区
  4. 需要使用时,用户向生产者申请访问缓冲
  5. 使用结束时,用户向生产者通知EOD(end of DMA)
  6. 当完全不需要使用缓冲区的时候,用户断开和缓冲区的连接

使用步骤

1. 导出缓冲

struct dma_buf *dma_buf_export_named(void *priv, struct dma_buf_ops *ops,
                                     size_t size, int flags,
                                     const char *exp_name);
#define dma_buf_export(priv, ops, size, flags)                  \
    dma_buf_export_named(priv, ops, size, flags, __FILE__)
return
失败返回NULL
exp_name
生产者的名字
dma_buf_export()
最后一个参数传递KBUILD_MODNAME

分配一个dma_buf将其加入到全局链表db_list中。

2. 获取文件描述符

int dma_buf_fd(struct dma_buf *dmabuf, int flags);
return
失败返回ERROR

获取一个空闲的文件描述符来管理dmabuf->file

3. 连接缓冲

连接的作用是让生产者知道设备缓冲的约束。在这一步完成之后,生产者仍可以不实际分配存储,但是当使用者请求要访问的时候就必须要准备好存储。

struct dma_buf *dma_buf_get(int fd);
struct dma_buf_attachment *dma_buf_attach(struct dma_buf *dmabuf,
                                          struct device *dev)

4. 申请访问

struct sg_table * dma_buf_map_attachment(struct dma_buf_attachment *,
                                         enum dma_data_direction);
void dma_buf_unmap_attachment(struct dma_buf_attachment *, struct sg_table *);

这两个函数必须有生产者来实现,注意的是这两个函数将加锁的任务丢给生产者了。

6. 断开连接

void dma_buf_detach(struct dma_buf *dmabuf,
                    struct dma_buf_attachment *dmabuf_attach);
void dma_buf_put(struct dma_buf *dmabuf);

注意事项

连接和断开的目的是让生产者了解约束。如果在申请访问并已经分配好存储之后,需要迁移存储,而此时又有人试图连接,应当在此时允许连接。如果新的用户有更多的约束,那么应该在申请的时候阻塞,直到其它使用者通知结束,再将缓冲区移动到新的区域。如果生产者不能满足约束,那么在连接的时候就应该返回错误。

工作原理

概念图

生产者和消费者模型如下所示,首先声明要导出缓冲区,并提供一个接口来导出文件描述符。因为是匿名文件,所以不论是用户空间还是内核空间,都没有办法直接看到文件描述符,要共享缓冲区又必须借助文件描述符。内核空间相对好办一点,可以直接调用生产者提供的接口来获取描述符,而用户空间要获取描述就没有标准接口可以使用,所以只好通过特定的ioctl来获取文件描述符。

dma-buf-mechanism.png

Figure 1: DMA BUF机理

更形象的例子如下所示。

dmabuf-example.png

Figure 2: DMA BUF示例

应用示例

以V4L2为例,核心数据结构为vb2_mem_ops,该结构的定义在include/media/videobuf2-core.h中,在drivers/media/v4l2-core/videobuf2-dma-sg.c中实现,在drivers/media/v4l2-core/videobuf2-dma-contig.c中也有实现。这里我们只讨论和DMA BUF相关的部分,并且以videobuf2-dma-sg.c为对象进行研究,由于videobuf2-dma-contig.c内存连续,相对更好处理,详细可以自行对比。

const struct vb2_mem_ops vb2_dma_sg_memops = {
    .alloc         = vb2_dma_sg_alloc,
    .put           = vb2_dma_sg_put,
    .get_userptr   = vb2_dma_sg_get_userptr,
    .put_userptr   = vb2_dma_sg_put_userptr,
    .prepare       = vb2_dma_sg_prepare,
    .finish        = vb2_dma_sg_finish,
    .vaddr         = vb2_dma_sg_vaddr,
    .mmap          = vb2_dma_sg_mmap,
    .num_users     = vb2_dma_sg_num_users,
    .get_dmabuf    = vb2_dma_sg_get_dmabuf,
    .map_dmabuf    = vb2_dma_sg_map_dmabuf,
    .unmap_dmabuf  = vb2_dma_sg_unmap_dmabuf,
    .attach_dmabuf = vb2_dma_sg_attach_dmabuf,
    .detach_dmabuf = vb2_dma_sg_detach_dmabuf,
    .cookie        = vb2_dma_sg_cookie,
};

函数vb2_expbuf()用于以文件描述符导出缓冲区,这个函数就是调用vb2_mem_ops->get_dmabuf来进行导出的。

int vb2_expbuf(struct vb2_queue *q, struct v4l2_exportbuffer *eb)
{
    // no ERROR check
    struct vb2_buffer *vb = NULL;
    struct vb2_plane *vb_plane;
    int ret;
    struct dma_buf *dbuf;

    vb = q->bufs[eb->index];
    vb_plane = &vb->planes[eb->plane];

    dbuf = call_memop(q, get_dmabuf, vb_plane->mem_priv);
    ret = dma_buf_fd(dbuf, eb->flags);
    dprintk(3, "buffer %d, plane %d exported as %d descriptor\n",
            eb->index, eb->plane, ret);
    eb->fd = ret;

    return 0;
}
static struct dma_buf *vb2_dma_sg_get_dmabuf(void *buf_priv,
                                             unsigned long flags)
{
    struct vb2_dma_sg_buf *buf = buf_priv;
    struct dma_buf *dbuf;

    if (WARN_ON(!buf->dma_sgt))
        return NULL;

    dbuf = dma_buf_export(buf, &vb2_dma_sg_dmabuf_ops, buf->size, flags, NULL);
    if (IS_ERR(dbuf))
        return NULL;

    atomic_inc(&buf->refcount);         // dmabuf keeps reference to vb2 buffer

    return dbuf;
}

这里vb2_dma_sg_buf就是exporter的核心数据结构,这个数据结构中包括了用于管理缓冲区的sg_table,有必要可以了解一下这个结构体是如何初始化的,可以参考vb2_mem_ops->alloc(),也就是vb2_dma_sg_alloc()。比较上层的函数是vb2_reqbufs(),用于申请内存。

要导出缓冲区,最关键是要定义一组缓冲区的操作方法供用户使用,这组操作方法便由vb2_dma_sg_dmabuf_ops提供。

static struct dma_buf_ops vb2_dma_sg_dmabuf_ops = {
    .attach = vb2_dma_sg_dmabuf_ops_attach,
    .detach = vb2_dma_sg_dmabuf_ops_detach,
    .map_dma_buf = vb2_dma_sg_dmabuf_ops_map,
    .unmap_dma_buf = vb2_dma_sg_dmabuf_ops_unmap,
    .kmap = vb2_dma_sg_dmabuf_ops_kmap,
    .kmap_atomic = vb2_dma_sg_dmabuf_ops_kmap,
    .vmap = vb2_dma_sg_dmabuf_ops_vmap,
    .mmap = vb2_dma_sg_dmabuf_ops_mmap,
    .release = vb2_dma_sg_dmabuf_ops_release,
};
map_dma_buf/unmap_dma_buf

前面的attach和detach并没有获取锁,这是因为在函数dma_buf_attach()已经获取了锁了。而在dma_buf_map_attachment()中却没有获取锁,所以这里面就需要去获取锁。

至于获取锁的原因,因为在这里面会检查缓存DMA方向,如果不是NONE就会去unmap,不加锁就可能unmap一个正在使用的散列表,所以加锁是必要的。

至于为什么不在dma_buf_map_attachment()中加锁,可以参考 dma-buf: don't hold the mutex around map/unmap calls,因为在外部没有什么需要保护的,所以直接转移到dmabuf->ops->map_dma_buf()中。

由于代码中采用了取巧的做法,即在map的时候采取检查并做unmap的工作,所以unmap实际是什么工作也没有做。这样做存在一个问题,就是最后一次传递之后就无法unmap了。

可以通过如下方式获取文件描述符。

int buffer_export(int v4lfd, enum v4l2_buf_type bt, int index, int *dmafd)
{
    struct v4l2_exportbuffer expbuf;

    memset(&expbuf, 0, sizeof(expbuf));
    expbuf.type = bt;
    expbuf.index = index;
    // int ioctl(int fd, int request, struct v4l2_exportbuffer *argp);
    if (ioctl(v4lfd, VIDIOC_EXPBUF, &expbuf) == -1) {
        perror("VIDIOC_EXPBUF");
        return -1;
    }

    *dmafd = expbuf.fd;

    return 0;
}