DMA-BUF 由浅入深(二) —— kmap / vmap

作者 by adtxl / 2022-02-22 / 暂无评论 / 326 个足迹

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

1. 前言

在上一篇《最简单的 dma-buf 驱动程序》中,我们学习了编写 dma-buf 驱动程序的三个基本步骤,即 dma_buf_opsdma_buf_export_infodma_buf_export()。在本篇中,我们将在 exporter-dummy 驱动的基础上,对其 dma_buf_ops 的 kmap / vmap 接口进行扩展,以此来演示这两个接口的使用方法。

2. dma-buf只能用于DMA硬件访问吗?

在内核代码中,我们见得最多的 dma-buf API 莫过于 dma_buf_attach()dma_buf_map_attachment(),以至于有些小伙伴会问:dma-buf 难道只能给 DMA 硬件来访问吗?当然不是!在上一篇的 概念 小节中就曾讲过,dma-buf 本质上是 buffer 与 file 的结合,因此它仍然是一块 buffer。不要看它带了 dma 字样就被迷惑了,dma-buf 不仅能用于 DMA 硬件访问,也同样适用于 CPU 软件访问,这也是 dma-buf 在内核中大受欢迎的一个重要原因。

正因如此,我才决定将 dma_buf_kmap() / dma_buf_vmap() 作为 dma-buf 系列教程的第二篇文章来讲解,因为这两个接口使用起来实在是比 DMA 操作接口简单太多了!

3. dma-buf 只能分配离散 buffer 吗?

当然不是!就和内核中 dma-mapping 接口一样,dma-buf 既可以是物理连续的 buffer,也可以是离散的 buffer,这最终取决于 exporter 驱动采用何种方式来分配 buffer。
因此为了尽量让读者易于理解,本篇特意使用了内核中最简单、最常见的 kzalloc() 函数来分配 dma-buf,自然,这块 buffer 就是物理连续的了。

4. cpu access

从 linux-3.4 开始,dma-buf 引入了 CPU 操作接口,使得开发人员可以在内核空间里直接使用 CPU 来访问 dma-buf 的物理内存。

如下 dma-buf API 实现了 CPU 在内核空间对 dma-buf 内存的访问:

  • dma_buf_kmap()
  • dma_buf_kmap_atomic()
  • dma_buf_vmap()

(它们的反向操作分别对应各自的 unmap 接口)

通过 `dma_buf_kmap() / dma_buf_vmap() 操作,就可以把实际的物理内存,映射到 kernel 空间,并转化成 CPU 可以连续访问的虚拟地址,方便后续软件直接读写这块物理内存。因此,无论这块 buffer 在物理上是否连续,在经过 kmap / vmap 映射后的虚拟地址一定是连续的。

上述的3个接口分别和 linux 内存管理子系统(MM)中的 kmap()、 kmap_atomic() 和 vmap() 函数一一对应,三者的区别如下:

函数 说明
kmap() 一次只能映射1个page,可能会睡眠,只能在进程上下文中调用
kmap_atomic() 一次只能映射1个page,不会睡眠,可在中断上下文中调用
vmap() 一次可以映射多个pages,且这些pages物理上可以不连续,只能在进程上下文中调用

从 linux-4.19 开始,dma_buf_kmap_atomic() 不再被支持。

dma_buf_ops 中的 map / map_atomic 接口名,其实原本就叫 kmap / kmap_atomic,只是后来发现与 highmem.h 中的宏定义重名了,为了避免开发人员在自己的驱动中引用 highmem.h 而带来的命名冲突问题,于是去掉了前面的“k”字。
想了解更多关于 kmap() 、vmap() 的信息,推荐阅读参考资料中的《Linux内核内存管理架构》一文。

5. 示例程序

本示例分为 exporter 和 importer 两个驱动。

首先是 exporter 驱动,我们基于上一篇的 exporter-dummy.c,对其 exporter_kmap() 和 exporter_vmap() 进行扩展,具体如下:

exporter-kmap.c

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

struct dma_buf *dmabuf_exported;
EXPORT_SYMBOL(dmabuf_exported);

static void *exporter_kmap(struct dma_buf *dmabuf, unsigned long page_num)
{
    return dmabuf->priv;
}

static void *exporter_kmap_atomic(struct dma_buf *dmabuf, unsigned long page_num)
{
    return dmabuf->priv;
}

static void *exporter_vmap(struct dma_buf *dmabuf)
{
    return dmabuf->priv;
}

static void exporter_release(struct dma_buf *dmabuf)
{
    kfree(dmabuf->priv);
}

...

static const struct dma_buf_ops exp_dmabuf_ops = {
    .map = exporter_kmap,
    .map_atomic = exporter_kmap_atomic,
    .vmap = exporter_vmap,
    .release = exporter_release,
    ...
};

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 __init exporter_init(void)
{
    dmabuf_exported = exporter_alloc_page();
    return 0;
}

module_init(exporter_init);

然后我们再编写一个 importer 驱动,用于演示如何在 kernel 空间,通过 dma_buf_kmap() / dma_buf_vmap() 接口操作 exporter 驱动导出的 dma-buf。

importer-kmap.c

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

extern struct dma_buf *dmabuf_exported;

static int importer_test(struct dma_buf *dmabuf)
{
    void *vaddr;

    vaddr = dma_buf_kmap(dmabuf, 0);
    pr_info("read from dmabuf kmap: %s\n", (char *)vaddr);
    dma_buf_kunmap(dmabuf, 0, vaddr);

    vaddr = dma_buf_vmap(dmabuf);
    pr_info("read from dmabuf vmap: %s\n", (char *)vaddr);
    dma_buf_vunmap(dmabuf, vaddr);

    return 0;
}

static int __init importer_init(void)
{
    return importer_test(dmabuf_exported);
}

module_init(importer_init);

示例描述:

exporter 通过 kzalloc 分配了一个 PAGE 大小的物理连续 buffer,并向该 buffer 写入了“hello world!” 字符串;
importer 驱动通过 extern 关键字导入了 exporter 的 dma-buf,并通过 dma_buf_kmap()dma_buf_vmap() 函数读取该 buffer 的内容并输出到终端显示。

6. 运行

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

# insmod /lib/modules/4.14.143/kernel/drivers/dma-buf/exporter-kmap.ko
# insmod /lib/modules/4.14.143/kernel/drivers/dma-buf/importer-kmap.ko

将看到如下打印结果:

read from dmabuf kmap: hello world!
read from dmabuf vmap: hello world!

注意:执行 insmod 命令时,必须先加载 exporter-kmap.ko,后加载 importer-kmap.ko,否则将出现符号依赖错误。
或者直接使用“modprobe importer_kmap”命令来自动解决模块依赖问题。

7. 总结

通过本篇,我们学习了 dma_buf_kmap()dma_buf_vmap() 函数的底层实现,以及如何使用这两个 API,它们是 CPU 在 kernel 空间访问 dma-buf 的典型代表。在下一篇,我们将一起来学习如何通过 DMA 硬件来访问 dma-buf 的物理内存。

独特见解