dma-buf 由浅入深(一) —— 最简单的 dma-buf 驱动程序
dma-buf 由浅入深(二) —— kmap / vmap
dma-buf 由浅入深(三) —— map attachment
dma-buf 由浅入深(四) —— mmap
dma-buf 由浅入深(五) —— File
dma-buf 由浅入深(六) —— begin / end cpu_access
dma-buf 由浅入深(七) —— alloc page 版本
dma-buf 由浅入深(八) —— ION 简化版
本篇我们将一起来学习 dma-buf 用于 Cache 同步操作的 begin_cpu_access 和 end_cpu_access 这两个接口。之所以将这两个接口放在第六篇讲解,是因为它们在内核中的使用频率并不高,只有在特殊场景下才派的上用场。
下图显示了 CPU 与 DMA 访问 DDR 之间的区别:
可以看到,CPU 在访问内存时是要经过 Cache 的,而 DMA 外设则是直接和 DDR 打交道,因此这就存在 Cache 一致性的问题了,即 Cache 里面的数据是否和 DDR 里面的数据保持一致。比如 DMA 外设早已将 DDR 中的数据改写了,而 CPU 却浑然不知,仍然在访问 Cache 里面暂存的旧数据。
所以 Cache 一致性问题,只有在 CPU 参与访问的情况下才会发生。如果一个 dma-buf 自始自终都只被一个硬件访问(要么CPU,要么DMA),那么 Cache 一致性问题就不会存在。
当然,如果一个 dma-buf 所对应的物理内存本身就是 Uncache 的(也叫一致性内存),或者说该 buffer 在被分配时是以 coherent 方式分配的,那么这种情况下,CPU 是不经过 cache 而直接访问 DDR 的,自然 Cache 一致性问题也就不存在了。
有关更多 Cache 一致性的问题,推荐阅读宋宝华的文章《关于DMA ZONE和dma alloc coherent若干误解的彻底澄清》,本文不做赘述。
在前面的《dma-buf 由浅入深(三) —— map attachment》文章中,我们了解到 dma_buf_map_attachment()
函数的一个重要功能,那就是同步 Cache 操作。但是该函数通常使用的是 dma_map_{single,sg} 这种流式 DMA 映射接口来实现 Cache 同步操作,这类接口的特点就是 Cache 同步只是一次性的,即在 dma map 的时候执行一次 Cache Flush 操作,在 dma unmap 的时候执行一次 Cache Invalidate 操作,而这中间的过程是不保证 Cache 和 DDR 上数据的一致性的。因此如果 CPU 在 dma map 和 unmap 之间又去访问了这块内存,那么有可能 CPU 访问到的数据就只是暂存在 Cache 中的旧数据,这就带来了问题。
那么什么情况下会出现 CPU 在 dma map 和 unmap 期间又去访问这块内存呢?一般不会出现 DMA 硬件正在传输过程中突然 CPU 发起访问的情况,而更多的是在 DMA 硬件发起传输之前,或 DMA 硬件传输完成之后,并且仍然处于 dma map 和 unmap 操作之间的时候,CPU 对这段内存发起了访问。下面举2个例子:
以上第一个例子是 CPU 在 DMA 传输后发起访问,第二个例子是在 DMA 传输前发起访问。针对这种情况,就需要在 CPU 访问内存前,先将 DDR 数据同步到 Cache 中(Invalidate);在 CPU 访问结束后,将 Cache 中的数据回写到 DDR 上(Flush),以便 DMA 能获取到 CPU 更新后的数据。这也就是 dma-buf 给我们预留 {begin,end}_cpu_access 的原因。
dma-buf 为我们提供了如下内核 API,用来在 dma map 期间发起 CPU 访问操作:
dma_buf_begin_cpu_access()
dma_buf_end_cpu_access()
它们分别对应 dma_buf_ops 中的 begin_cpu_access 和 end_cpu_access 回调接口。
通常在驱动设计时, begin_cpu_access / end_cpu_access 使用如下流式 DMA 接口来实现 Cache 同步:
dma_sync_single_for_cpu()
/ dma_sync_single_for_device()
dma_sync_sg_for_cpu()
/ dma_sync_sg_for_device()
CPU 访问内存之前,通过调用 dma_sync_{single,sg}_for_cpu() 来 Invalidate Cache,这样 CPU 在后续访问时才能重新从 DDR 上加载最新的数据到 Cache 上。
CPU 访问内存结束之后,通过调用 dma_sync_{single,sg}_for_device() 来 Flush Cache,将 Cache 中的数据全部回写到 DDR 上,这样后续 DMA 才能访问到正确的有效数据。
关于更多流式 DMA 映射的介绍,推荐阅读 wowotech 翻译的《Dynamic DMA mapping Guide》文章。
考虑到 mmap() 操作,dma-buf 也为我们提供了 Userspace 的同步接口,通过 DMA_BUF_IOCTL_SYNC
ioctl() 来实现。该 cmd 需要一个 struct dma_buf_sync 参数,用于表明当前是 begin 还是 end 操作,是 read 还是 write 操作。
dma-buf: Add ioctls to allow userspace to flush
常用写法如下:
struct dma_buf_sync sync = { 0 };
sync.flags = DMA_BUF_SYNC_RW | DMA_BUF_SYNC_START;
ioctl(dmabuf_fd, DMA_BUF_IOCTL_SYNC, &sync);
// execute cpu access, for example: memset() ...
sync.flags = DMA_BUF_SYNC_RW | DMA_BUF_SYNC_END;
ioctl(dmabuf_fd, DMA_BUF_IOCTL_SYNC, &sync);
本示仅仅用于演示 dma-buf begin / end API 的调用方法,并未考虑真实使用场景的可靠性,各位读者心领神会即可。
我们基于《dma-buf 由浅入深(四) —— mmap》中的示例一 exporter-fd.c 文件进行修改,新增 begin_cpu_access 和 end_cpu_access 回调接口,并调用 dma_sync_single_for_{cpu,device} 来完成 Cache 的同步。
exporter-sync.c
#include
#include
#include
#include
#include
struct dma_buf *dmabuf_exported;
EXPORT_SYMBOL(dmabuf_exported);
...
static int exporter_begin_cpu_access(struct dma_buf *dmabuf,
enum dma_data_direction dir)
{
dma_addr_t dma_addr = virt_to_phys(dmabuf->priv);
dma_sync_single_for_cpu(NULL, dma_addr, PAGE_SIZE, dir);
return 0;
}
static int exporter_end_cpu_access(struct dma_buf *dmabuf,
enum dma_data_direction dir)
{
dma_addr_t dma_addr = virt_to_phys(dmabuf->priv);
dma_sync_single_for_device(NULL, dma_addr, PAGE_SIZE, dir);
return 0;
}
static const struct dma_buf_ops exp_dmabuf_ops = {
...
.begin_cpu_access = exporter_begin_cpu_access,
.end_cpu_access = exporter_end_cpu_access,
};
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);
return copy_to_user((int __user *)arg, &fd, sizeof(fd));
}
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);
}
module_init(exporter_init);
我们基于《dma-buf 由浅入深(二) —— kmap/vmap》中的 importer-kmap.c 进行修改,如下:
importer-sync.c
#include
#include
#include
extern struct dma_buf *dmabuf_exported;
static int importer_test(struct dma_buf *dmabuf)
{
void *vaddr;
dma_buf_begin_cpu_access(dmabuf, DMA_FROM_DEVICE);
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);
dma_buf_end_cpu_access(dmabuf, DMA_FROM_DEVICE);
return 0;
}
static int __init importer_init(void)
{
return importer_test(dmabuf_exported);
}
module_init(importer_init);
该 importer 驱动将原来的 kmap / vmap 操作放到了 begin / end 操作中间,以确保读取数据的正确性(虽然在本示例中没有任何意义)。
我们基于《dma-buf 由浅入深(四) —— mmap》中的示例一 mmap_dmabuf.c 文件进行修改,如下:
dmabuf_sync.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int fd;
int dmabuf_fd = 0;
struct dma_buf_sync sync = { 0 };
fd = open("/dev/exporter", O_RDONLY);
ioctl(fd, 0, &dmabuf_fd);
close(fd);
sync.flags = DMA_BUF_SYNC_READ | DMA_BUF_SYNC_START;
ioctl(dmabuf_fd, DMA_BUF_IOCTL_SYNC, &sync);
char *str = mmap(NULL, 4096, PROT_READ, MAP_SHARED, dmabuf_fd, 0);
printf("read from dmabuf mmap: %s\n", str);
sync.flags = DMA_BUF_SYNC_READ | DMA_BUF_SYNC_END;
ioctl(dmabuf_fd, DMA_BUF_IOCTL_SYNC, &sync);
return 0;
}
该测试程序将原来的 mmap() 操作放到了 ioctl SYNC_START / SYNC_END 之间,以确保读取数据的正确性(虽然在本示例中没有任何意义)。
内核源码 | 4.14.143 |
示例源码 | hexiaolong2008-GitHub/sample-code/dma-buf/07 |
开发平台 | Ubuntu14.04/16.04 |
运行平台 | my-qemu 仿真环境 |
在 my-qemu 仿真环境中执行如下命令:
# insmod /lib/modules/4.14.143/kernel/drivers/dma-buf/exporter-sync.ko
# insmod /lib/modules/4.14.143/kernel/drivers/dma-buf/importer-sync.ko
输出结果如下:
read from dmabuf kmap: hello world!
read from dmabuf vmap: hello world!
接着执行如下命令:
# ./dmabuf_sync
输出:
read from dmabuf mmap: hello world!
输出的结果其实和之前的程序没有任何区别。
上一篇:《dma-buf 由浅入深(五)—— File》
下一篇:《dma-buf 由浅入深(七) —— alloc page 版本》
文章汇总:《DRM(Direct Rendering Manager)学习简介》