DMA 介绍
Intro
DMA 是一种加速设备访问主机内存的技术。有两种访问设备的方式:DMA 以及 PIO (Programmed I/O)。MMIO 以及 PMIO 都属于 PIO。在没有 DMA 的情况下,为了让设备访问主机内存,需要 CPU 中的寄存器进行中转,在写设备时先将数据读取到 CPU 的寄存器中,再将寄存器中的内容通过 MMIO 或者 PMIO 写入到设备中。设备读取内存与写类似。
使用 PIO 的方式虽然简单但是会增加 CPU 的负载,尤其是在内存以及传输跟不上 CPU 的速度的情况下,这种方式极大地浪费了 CPU 的时间。
DMA 则是解决这一问题的有效途径。DMA 使得设备可以绕过 CPU 读写主机内存,让 CPU 从数据传输的工作中解放出来去做更有效的任务。DMA 有两种类型:third-party DMA 以及 bus mastering (first-party DMA),其中 third-party 是最初的 DMA 方式,需要一个单独的 DMA 控制器来管理 DMA。Third-party 在古老的 ISA 总线和 IBM PC 中使用,而现在普遍使用的都是 PCIe 总线,在 PCIe 中不再需要单独的 DMA 控制器,每个设备都可以是总线的主设备(master),都可以主动发出 DMA 读写请求。
DMA 是与主机使用的总线协议密切相关的,不同的总线协议有不同的 DMA 实现方式。
Type
Third-Party DMA
Third-party DMA 是最初的 DMA 方式,这种方式在 ISA 总线和 IBM PC 上使用,由于当时大多是 共享总线,且外部设备一般只能作为总线的从设备,不能够主动发出请求,所以需要在共享总线上有单独的 DMAC (DMA Controller),DMAC 相当于替代了 CPU 在 PIO 中的位置。DMAC 和 CPU 都可以作为前端总线的 master,所以 DMAC 可以向内存控制器发送内存读写请求。
在该方式中,为了执行 DMA 操作,CPU 需要先设置 DMAC 的寄存器,告诉 DMAC 要操作的内存地址以及读写大小等信息。在 DMAC 可以执行 DMA 操作时,会发起对共享总线的使用请求,在等到 CPU 完成当前的总线占用之后 DMAC 就会成为共享总线的 master。
DMAC 有两种 DMA 的传输方式:flyby 和 fetch-and-deposit。在 flyby 方式下,数据不会在 DMAC 进行中转,DMAC 设置共享总线的地址线,而数据线则是由对应的设备或者内存控制。而在 fetch-and-deposit 方式下,DMAC 和 PIO 中的 CPU 一样,数据需要通过 DMAC 进行中转。
DMAC 会使用 DMA channel 与设备进行连接。这里的 DMA channel 往往指的是逻辑上的通道,每个 channel 的控制信号是单独的,而数据信号则往往是复用的。由于 third-party 往往是在共享总线上使用的,channel 的数据信号线实际上应该就是总线的信号线。似乎与 Channel I/O 有一些关联。
主要参考文献是 DMA Fundamentals on Various PC Platforms1。
Bus Mastering / First-Party DMA
在 PCI/PCIe 总线下,每个设备都可以作为总线的 master,所以不再像 third-party 那样需要一个单独的 DMAC 来管理 DMA。当设备需要进行内存访问时,只需要获取内存地址对应的总线地址,请求总线控制权,并向目标地址发出读写请求即可。
下面以 x86/PCIe 架构下的 Linux 为例描述 DMA 的过程。在该架构下,有三种地址空间:程序的虚拟地址空间、CPU 的物理地址空间以及 PCIe 的总线地址空间。值得注意的是,物理地址空间并不等同于 DRAM 的编址,物理地址空间相当于也进行了一次映射,部分地址对应的是内存控制器访问 DRAM 的地址,还有部分地址是 MMIO 使用的,对应的是外部设备的访问地址。总线地址则与对应的总线协议密切相关。物理地址到总线地址的转换可以由 Root Complex 进行,可以是简单的直接映射,也可以使用 IOMMU 进行管理。
在进行 DMA 时,设备驱动首先分配一部分内存,并获得这部分内存对应的总线地址,之后将包含该总线地址的 DMA 描述信息发送给设备(通过 MMIO 或者 PMIO),之后该 PCIe 设备就可以创建 PCIe Read/Write Transaction,向对应的总线地址通过 PCIe 进行通信。PCIe 总线树的 Root Complex 在看到了该 PCIe 事务之后发现该事务对应的地址是一个内存读写请求,之后的内存访问就取决于 Root Complex 的实现。虽然现在已经没有了单独的北桥芯片,但是仍可以把与内存连接的那部分逻辑抽象为北桥以及前端总线,那么就可以认为 Root Complex 位于北桥上,在 Root Complex 获得了 PCIe 请求后就可以占用前端总线并向内存控制器发送对应的读写请求。
主要参考的是 Linux 内核文档 2 以及 Stackoverflow 上的回答 3。
IOMMU
IOMMU 可以用于对 DMA 地址的管理以及对中断信号的重映射,使得硬件能够支持多个虚拟机。
Intel VT-d
VT-d (Virtualization Technology for Direct I/O) 是 Intel 的 IOMMU 技术。
Bounce Buffer / Double Buffer
由于总线地址位宽和内存地址位宽不一定相同,在总线地址位宽小的时候就不能访问到高地址的内存,为了解决这个问题可以将低地址的内存空间作为一个中转,先对低地址内存进行操作,之后再拷贝到高地址。
Bounce buffers are required for devices that cannot access the full range of memory available to the CPU… A bounce buffer resides in memory low enough for a device to copy from and write data to. It is then copied to the desired user page in high memory… the bounce buffer acts as a type of bridge… it is insignificant in comparison to swapping out pages in low memory.4
DMA for Memory Copy
有些架构可以使用 DMA 来进行内存之间的拷贝,将拷贝任务从 CPU 上卸载下来,比如 Intel 的 I/OAT 技术。但 IOAT 似乎效果并不好,现在似乎很少使用。
Direct Cache Access (DCA)
为了减少访问内存带来的时延,在 DMA 时有些架构可以直接将数据写入到 LLC 中。
由于 LLC 大小是有限的,如果占用了太多的 L3 Cache 可能会造成性能下降。
Intel DDIO (Data Direct I/O)
DDIO 是 DCA 的一种实现。
开启 DDIO 之后 CPU 的 L3 Cache 变为了 DMA 的目的地,避免了 DRAM 的读取。
DDIO 可以与 RDMA 配合使用。但是在使用 RDMA+PMEM 似乎一般会将 DDIO 关闭。