DMA-BUF 由浅入深(四) —— mmap

作者 by adtxl / 2022-02-23 / 暂无评论 / 343 个足迹

转载自https://blog.csdn.net/hexiaolong2009/article/details/102596791

1. 前言

前面的两篇文章《dma-buf 由浅入深(二) —— kmap/vmap》和《dma-buf 由浅入深(三) —— map attachment》都是在 kernel space 对 dma-buf 进行访问的,本篇我们将一起来学习,如何在 user space 访问 dma-buf。当然,user space 访问 dma-buf 也属于 CPU Access 的一种。

2. mmap

为了方便应用程序能直接在用户空间读写 dma-buf 的内存,dma_buf_ops 为我们提供了一个 mmap 回调接口,可以把 dma-buf 的物理内存直接映射到用户空间,这样应用程序就可以像访问普通文件那样访问 dma-buf 的物理内存了。

dma-buf: mmap support

在 Linux 设备驱动中,大多数驱动的 mmap 操作接口都是通过调用 remap_pfn_range() 函数来实现的,dma-buf 也不例外。对于此函数不了解的同学,推荐阅读 参考资料 中彭东林的博客,写的非常好!

除了 dma_buf_ops 提供的 mmap 回调接口外,dma-buf 还为我们提供了 dma_buf_mmap() 内核 API,使得我们可以在其他设备驱动中就地取材,直接引用 dma-buf 的 mmap 实现,以此来间接的实现设备驱动的 mmap 文件操作接口。

2. 示例

image.png

接下来,我们将通过两个示例来演示如何在 Userspace 访问 dma-buf 的物理内存。

  • 示例一:直接使用 dma-buf 的 fd 做 mmap() 操作
  • 示例二:使用 exporter 的 fd 做 mmap() 操作

2.1 示例一

本示例主要演示如何在驱动层实现 dma-buf 的 mmap 回调接口,以及如何在用户空间直接使用 dma-buf 的 fd 进行 mmap() 操作。

2.1.1 exporter 驱动

首先,我们仍然基于第一篇的 exporter-dummy.c 驱动来实现 mmap 回调接口:

exporter-fd.c

#include <linux/dma-buf.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

struct dma_buf *dmabuf_exported;
EXPORT_SYMBOL(dmabuf_exported);

static int exporter_mmap(struct dma_buf *dmabuf, struct vm_area_struct *vma)
{
    void *vaddr = dmabuf->priv;

    return remap_pfn_range(vma, vma->vm_start, virt_to_pfn(vaddr),
                PAGE_SIZE, vma->vm_page_prot);
}

...

static const struct dma_buf_ops exp_dmabuf_ops = {
    ...
    .mmap = exporter_mmap,
};

static struct dma_buf *exporter_alloc_page(void)
{
    DEFINE_DMA_BUF_EXPORT_INFO(exp_info);
    struct dma_buf *dmabuf;
    void *vaddr;

    vaddr = kzalloc(PAGE_SIZE, GFP_KERNEL);

    exp_info.ops = &exp_dmabuf_ops;
    exp_info.size = PAGE_SIZE;
    exp_info.flags = O_CLOEXEC;
    exp_info.priv = vaddr;

    dmabuf = dma_buf_export(&exp_info);

    sprintf(vaddr, "hello world!");

    return dmabuf;
}

static long exporter_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int fd = dma_buf_fd(dmabuf_exported, O_CLOEXEC);
    copy_to_user((int __user *)arg, &fd, sizeof(fd));

    return 0;
}

static struct file_operations exporter_fops = {
    .owner      = THIS_MODULE,
    .unlocked_ioctl = exporter_ioctl,
};

static struct miscdevice mdev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "exporter",
    .fops = &exporter_fops,
};

static int __init exporter_init(void)
{
    dmabuf_exported = exporter_alloc_page();
    return misc_register(&mdev);
}

static void __exit exporter_exit(void)
{
    misc_deregister(&mdev);
}

module_init(exporter_init);
module_exit(exporter_exit);

从上面的示例可以看到,除了要实现 dma-buf 的 mmap 回调接口外,我们还引入了 misc driver,目的是想通过 misc driver 的 ioctl 接口将 dma-buf 的 fd 传递给上层应用程序,这样才能实现应用程序直接使用这个 dma-buf fd 做 mmap() 操作。

为什么非要通过 ioctl 的方式来传递 fd ?这个问题我会在下一篇《dma-buf 由浅入深(五)—— File》中详细讨论。

在 ioctl 接口中,我们使用到了 dma_buf_fd() 函数,该函数用于创建一个新的 fd,并与该 dma-buf 的文件相绑定。关于该函数,我也会在下一篇中做详细介绍。

2.1.2 userspace 程序

mmap_dmabuf.c

int main(int argc, char *argv[])
{
    int fd;
    int dmabuf_fd = 0;

    fd = open("/dev/exporter", O_RDONLY);
    ioctl(fd, 0, &dmabuf_fd);
    close(fd);

    char *str = mmap(NULL, 4096, PROT_READ, MAP_SHARED, dmabuf_fd, 0);
    printf("read from dmabuf mmap: %s\n", str);

    return 0;
}

可以看到 userspace 的代码非常简单,首先通过 exporter 驱动的 ioctl() 获取到 dma-buf 的 fd,然后直接使用该 fd 做 mmap() 映射,最后使用 printf() 来输出映射后的 buffer 内容。

2.1.3 运行结果

在 my-qemu 仿真环境中执行如下命令:

# insmod /lib/modules/4.14.143/kernel/drivers/dma-buf/exporter-fd.ko
# ./mmap_dmabuf

将看到如下打印结果:

read from dmabuf mmap: hello world!

可以看到,userspace 程序通过 mmap() 接口成功的访问到 dma-buf 的物理内存。

关于应用程序直接使用 dma-buf fd 做 mmap() 操作的案例,Google ADF 的 simple_buffer_alloc 可谓在这一点上发挥的淋漓尽致!详细参考代码如下:

备注:上层 minui 获取到的 surf->fd 其实就是 dma-buf 的 fd。Recovery 模式下应用程序绘图本质上就是 CPU 通过 mmap() 来操作 dma-buf 的物理内存。

2.2 示例二

本示例主要演示如何使用 dma_buf_mmap() 内核 API,以此来简化设备驱动的 mmap 文件操作接口的实现。

2.2.1 exporter 驱动

我们基于示例一中的 exporter-fd.c 文件,删除 exporter_ioctl() 函数,新增 exporter_misc_mmap() 函数, 具体修改如下:

exporter-mmap.c

#include <linux/dma-buf.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

struct dma_buf *dmabuf_exported;
EXPORT_SYMBOL(dmabuf_exported);

static int exporter_mmap(struct dma_buf *dmabuf, struct vm_area_struct *vma)
{
    void *vaddr = dmabuf->priv;

    return remap_pfn_range(vma, vma->vm_start, virt_to_pfn(vaddr),
                PAGE_SIZE, vma->vm_page_prot);
}

...
static const struct dma_buf_ops exp_dmabuf_ops = {
    ...
    .mmap = exporter_mmap,
};

static struct dma_buf *exporter_alloc_page(void)
{
    DEFINE_DMA_BUF_EXPORT_INFO(exp_info);
    struct dma_buf *dmabuf;
    void *vaddr;

    vaddr = kzalloc(PAGE_SIZE, GFP_KERNEL);

    exp_info.ops = &exp_dmabuf_ops;
    exp_info.size = PAGE_SIZE;
    exp_info.flags = O_CLOEXEC;
    exp_info.priv = vaddr;

    dmabuf = dma_buf_export(&exp_info);

    sprintf(vaddr, "hello world!");

    return dmabuf;
}

static int exporter_misc_mmap(struct file *file, struct vm_area_struct *vma)
{
    return dma_buf_mmap(dmabuf_exported, vma, 0);
}

static struct file_operations exporter_fops = {
    .owner  = THIS_MODULE,
    .mmap   = exporter_misc_mmap,
};

static struct miscdevice mdev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "exporter",
    .fops = &exporter_fops,
};

static int __init exporter_init(void)
{
    dmabuf_exported = exporter_alloc_page();
    return misc_register(&mdev);
}

static void __exit exporter_exit(void)
{
    misc_deregister(&mdev);
}

module_init(exporter_init);
module_exit(exporter_exit);

与示例一的驱动相比,示例二的驱动不再需要把 dma-buf 的 fd 通过 ioctl 传给上层,而是直接将 dma-buf 的 mmap 回调接口嫁接到 misc driver 的 mmap 文件操作接口上。这样上层在对 misc device 进行 mmap() 操作时,实际映射的是 dma-buf 的物理内存。

2.2.2 userspace 程序

mmap_exporter.c

int main(int argc, char *argv[])
{
    int fd;

    fd = open("/dev/exporter", O_RDONLY);

    char *str = mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 0);
    printf("read from /dev/exporter mmap: %s\n", str);

    close(fd);

    return 0;
}

与示例一的 userspace 程序相比,示例二不再通过 ioctl() 方式获取 dma-buf 的 fd,而是直接使用 exporter misc device 的 fd 进行 mmap() 操作,此时执行的则是 misc driver 的 mmap 文件操作接口。当然最终输出的结果都是一样的。

2.2.3 运行结果

在 my-qemu 仿真环境中执行如下命令:

# insmod /lib/modules/4.14.143/kernel/drivers/dma-buf/exporter-mmap.ko
# ./mmap_exporter

将看到如下打印结果:

read from /dev/exporter mmap: hello world!

参考资料

  1. 认真分析mmap:是什么 为什么 怎么用
  2. 内存映射函数remap_pfn_range学习——示例分析(1)

独特见解