LWN: Noncoherent DMA mappings!
关注了就能看到更多这么棒的文章哦~
Noncoherent DMA mappings
By Jonathan Corbet
May 7, 2021
DeepL assisted translation
https://lwn.net/Articles/855328/
虽然有时可以通过 CPU 直接读写数据来实现 I/O 操作,但通常来说要想获得满足性能要求的 I/O 操作的唯一方法就是由 device 自己直接来跟内存进行数据搬移操作。从很早期开始 Linux 内核就已经支持了直接内存访问(DMA,direct memory access)方式进行 I/O 操作,但是总是能有一些新方法可以对这个功能进行进一步的改进,尤其是当硬件自己增加了一些新的功能的时候。在内核 5.13 版本中增加的 "non-contiguous" DMA API 虽然名字可能让人有点困惑,但它很好地展示了要在当前系统上获得最佳性能所必须要进行的改动。
当然,DMA 给我们带来了很多挑战,尤其是在 CPU 和 device 之间没有明确出谁可以在特定时间来对这块内存范围进行读写时,就可能会出来一些竞态问题(race condition)。但是还有另一个问题。CPU 总是会尽量将内存内容缓存到 CPU cache 中,从而避免每次引用时都要去耗费时间进行真正的内存访问。但是如果已经放入 CPU cache 的数据在内存中被后续 DMA 操作覆盖了,那么会导致 CPU 从 cache 中读取到错误数据,出现数据损坏问题。同样,如果 cache 中包含了由 CPU 写入的、尚未真正写入内存的数据,那么这些数据必须得在 device 访问这块内存之前被 flush 出来,否则很可能也会导致错误结果。
x86 架构提供了 cache snooping(缓存嗅探)功能,于是内核开发者的工作可以相对容易一些。例如,如果观察到有一个 device 正在向内存的某个范围内写东西,那么 CPU 的 cache 就会失效。这种 "高速缓存一致性(cache-coherent)" 的效果意味着开发者不需要担心 cache 的内容会导致数据被破坏。但是其他架构就不那么方便了。Arm 等一些其他架构,cache 中都有可能继续保留那些不再与相应内存内容相匹配的数据。在这些系统上开发者必须注意正确地管理 cache,因为 device 和 CPU 之间需要交替拥有 DMA buffer 的控制权。
有很多方法来实现,但是如果 DMA buffer 也有内核或用户空间发起的大量读写操作,那么就变得更加难以处理。有时候人们会采取的解决就是将这段内存区域配置成 uncached。但是不用 cache 之后虽然不会发生数据损坏了,但是用户马上就会开始怀念起 cache 来了,因为读写那些 uncached 内存非常慢。如果可能的话,最好还是避免使用 uncached 模式。
新增的 API 对某些类型的设备来说是一种不错的解决方法。驱动程序可以用以下方式来分配一个 DMA buffer:
struct sg_table *dma_alloc_noncontiguous(struct device *dev, size_t size,
enum dma_data_direction direction,
gfp_t gfp, unsigned long attrs);
它会试着根据指定的 direction 以及 gfp flag 来为 dev 设备分配大小为 size 字节的 DMA 内存。这个 buffer 在系统内存中可能不是物理连续的,但返回 scatter/gather table 将会用来让 DMA device 以为是一个单一的、连续的范围。显然,这必须要一个 I/O memory-management unit(IOMMU)来帮忙了。但这仍然是一个重要的功能,因为有些 device 如果没有 IOMMU 的帮助的话就不能进行 scatter/gather IO 操作。attrs 的唯一可用取值是 DMA_ATTR_ALLOC_SINGLE_PAGES,这是一个提示,用来告诉 DMA 映射代码不用为这个 buffer 来分配 huge page。
这个 buffer 很可能不具有缓存一致性(cache-coherent)。跟其他那些 noncoherent mapping(非一致性的映射)情况一样,必须由代码主动进行 cache 维护操作。例如在把内存交给 device 进行 I/O 之前,必须调用 dma_sync_sgtable_for_device(),这将确保所有 dirty cache line 都会被 flush 到内存中去(当然还做了其他一些工作)。为了从 device 来收回控制权,就必须调用 dma_sync_sgtable_for_cpu()。
释放 buffer 可以通过以下方式:
void dma_free_noncontiguous(struct device *dev, size_t size,
struct sg_table *sgtable,
enum dma_data_direction dir);
这些参数必须跟分配 buffer 时使用的参数能对得上。
这个 buffer 返回之后仍然不能被 CPU 直接访问。如果内核需要把它映射到内核空间来进行 CPU 访问,可以用以下方式:
void *dma_vmap_noncontiguous(struct device *dev, size_t size,
struct sg_table *sgtable);
void dma_vunmap_noncontiguous(struct device *dev, void *vaddr);
不过,如果用这种方式建立了内核空间的映射的话,又会出现缓存一致性问题。因此如果内核如果已经写入过这个缓冲区,那么就必须调用 flush_kernel_vmap_range() 以确保在把内存交给 device 之前,cache 中的数据都已经写入了内存。同样,也需要调用 invalidate_kernel_vmap_range()来确保那些可能已经被 device 写入过的内存内容在 CPU cache 中的旧数据。
最后,可以通过如下调用来把 buffer 映射到用户空间:
int dma_mmap_noncontiguous(struct device *dev, struct vm_area_struct *vma,
size_t size, struct sg_table *sgt);
这通常是用在 application 发起了 mmap() 调用的情况下,当不再需要内存时,applicatin 可以调用 munmap()。不用说,如果用户空间在此 buffer 受 device 控制的时候如果访问了这个 buffer,也可能会出现不好的结果。在用户空间明确拥有 buffer 管理权的情况下(比如 Video4Linux2 API 这种情况),在错误的时间访问也应该不会有问题。
在 5.13 中还合并了一个 uvcvideo 驱动程序的 patch,把它从原来使用的 coherent mapping 改成用这组新的 API。根据 chagnelog,在那些 non-cache-coherent 的系统上,这个改动可以将性能提高 20 倍,这似乎值得一试。有可能其他一些驱动程序也会在今后什么时候改成用这组 API。这种改动对用户来说并不显眼,但最终会使系统的性能获得很大的提升。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~