【RDMA】21. RDMA之内存地址基础知识
转自:https://zhuanlan.zhihu.com/p/463199854作者:Savir
本文欢迎非商业转载,转载请注明出处。
RDMA技术实现的是对远程内存的直接读写,整个体系都是围绕着内存访问和管理所搭建的。以前有一些读者在评论区留言或者私信问我一些关于内存的问题,相信很多刚入行的朋友也跟曾经的我一样被各种地址概念搞的焦头烂额。所以我就在这篇文章中讲解和澄清一些关于内存地址的基础知识,作为RDMA内存管理系列文章的“开胃菜”,扫除大家阅读后续文章的障碍。
本文是“Memory Region”和“Memory Window”等的前置文章,不需要任何RDMA背景,主要目的是科普基础知识,不做比较深入的探讨。内容如有错误之处欢迎大家在评论区指出。
硬件架构
我们先来看一下常见的服务器架构中几个硬件组件之间的关系,下图中的CPU通过总线和内存、以及PCIe RC(Root Complex)相连接,PCIe RC通过PCIe总线直接连接了GPU和PCIe Switch,Switch又连接了多个外设:
一种典型的服务器架构
简单介绍下各个组件:
CPU
CPU不用解释,需要注意图中的CPU内部集成了内存控制器Memory Controller,内存控制器主要负责处理CPU核发出的内存访问请求,对内存进行读写。
PCIe RC
全称为Root Complex,它是PCIe总线树状结构的“根”节点,主要负责处理CPU对PCIe设备的控制请求、完成地址转换等工作。
系统总线
CPU、内存控制器和PCIe RC是由高速总线连接到一起的,一般称这个总线为系统总线。
请注意它们三者的关系也不一定跟上图完全一样。PCIe规范没有对RC具体怎么实现做出规定,因此RC的概念比较模糊。有的芯片架构中,PCIe RC被集成到了CPU内部,而且内存控制器也可以被划分到PCIe RC中。
PCIe Switch
虽然PCIe RC上面可以直接连接终端设备(PCIe中称为EP,End Point),但是数量是有限的。所以一般情况下只有GPU直接连接到PCIe RC上,其他的外设通过PCIe Switch进行连接。PCIe Switch跟网络中的交换机的作用差不多,下面也可以增加更多层级,从而连接更多的PCIe EP到CPU上。
NIC
Network Interface Controller,网络接口控制器,也就是我们常说的网卡,插上网线并进行配置之后就可以接入网络了。
HCA(RNIC)
它就是我们关注的重点,即支持RDMA技术的网卡。在Infiniband/RoCE规范中,将RDMA网卡称为HCA,全称为Host Channel Adapter,即主机通道适配器;而在iWARP协议族中,将RDMA网卡称为RNIC,全称为RDMA enabled Network Interface Controller,即支持RDMA的网络接口控制器。
PCIe总线上可能会连接可种各样的设备,这里我们只列出了NIC和HCA。
地址
既然RDMA技术的核心是内存,自然离不开如何对内存进行寻址、也就是如何访问的问题。RDMA技术中的内存既要供软件(或者说CPU)访问,也要供网卡访问(HCA/RNIC),这其中涉及多种地址,我们将在本节中讲解。
物理地址
内存中的数据是按照字节连续排布的,每个字节都可以有一个索引,这个索引就是内存的地址。
内存地址
如果我们的CPU想要访问内存,最朴素的想法就是CPU直接指定一个内存的地址就可以了,这个地址就是物理地址,即Physical Address,简称PA。
CPU通过物理地址访问内存
虚拟地址
直接使用物理地址虽然方便,但是在操作系统上直接用物理地址访问内存产生了一些问题,比如:
- 地址之间不隔离:难以避免一个程序恶意写入另一个程序所使用的内存。
- 内存容易不够用:当同时运行的程序比较多时,内存很容易就不够用了。
- 内存使用效率低:即使当一个程序执行完毕后释放了自己的内存,它留下的“内存空洞”不太可能完全匹配另一个程序所需要的内存大小,可能会产生一些难以利用的“内存碎片”
后来,人们设计出了虚拟地址来解决这些问题。虚拟地址和物理地址之间经过了一层转换,软件或者说CPU通过虚拟地址来访问内存,并不能看到真实的地址,而在中间起到转换作用的是一个专用的模块——MMU(Memory Management Unit)。
当CPU发起对一个虚拟地址的访问时,MMU会去查询页表,将虚拟地址转换为物理地址,这个过程如下图所示:
CPU通过虚拟地址访问内存
一般情况下MMU都是被集成在CPU内部的,所以以后的图中我们会把MMU放到CPU中。
页表本身也储存在内存当中,每个进程都有一份,也就是每个进程都有一份虚拟-物理地址的映射关系。当不同的进程访问相同的地址时,最终对应的内存的物理地址是不同的。
进程初始化的时候,页表的基地址会被储存在特殊的寄存器中,这个基地址是物理地址还是虚拟地址?——自然是物理地址,要不然CPU怎么在没有页表的情况下找到页表的基地址呢?
逻辑地址与线性地址
在Linux操作系统中,它们与虚拟地址相等。这两个概念属于历史产物,目前已经很少涉及。我们就不介绍了,如果读者感兴趣的话可以阅读参考链接[2]。
现在我们介绍完了CPU访问内存的方式,在上述模型中加入RDMA网卡等外设之前,我们先来介绍一下地址空间的概念。
地址空间
所有能够被索引的地址,就构成了一个地址空间。所以我们可以说,所有CPU能够访问的虚拟地址构成了虚拟地址空间,所有物理地址构成了物理地址空间。对于一个64位的操作系统来说,理论上CPU最大能够访问0~2^64 Bytes的地址空间,但是目前一般是只实现48位,也就是能够访问2^48=256TB的空间。
需要注意的是,地址空间里并不是所有地址都会映射到内存。比如物理地址空间中,有一部分地址会映射给BIOS,有一部分会映射给PCIe设备。BIOS大家应该都知道,就是硬件上电之后先执行的一小段程序,负责对包括内存在内的一些硬件进行初始化,之后会引导操作系统启动;而映射给PCIe设备指的是什么呢?
MMIO
Memory mapping Input/Output,是PCIe规范设计的一种机制。前文中说的把一部分物理地址映射给PCIe设备,指的就是当CPU发起对MMIO地址的读写操作时,会被PCIe控制器接管,然后转化为对PCIe总线上连接的设备的访问请求。而最终转化对设备寄存器的读写,还是对设备内部存储空间的读写,是由设备注册时的配置决定的。
总线地址
CPU访问MMIO空间之后经过PCIe RC转化的地址,就是(PCIe)总线地址。外设也可以通过PCIe总线地址访问其他挂在PCIe总线上的设备,所有这些总线地址构成了总线地址空间。
CPU要访问总线地址空间中的地址,是需要PCIe RC将物理地址转换为总线地址的,画图来表示就是:
CPU通过虚拟地址访问寄存器的总线地址
CPU访问总线地址空间的问题介绍完了,那么外设如何访问内存呢?
有些设备不具备直接访问内存的能力,这个时候就需要CPU来帮助实现。下图中的例子中,如果想要把内存中的数据写入硬件内部的存储空间,需要CPU先把内存中的数据读入寄存器,再写入硬件。
在没有DMA的情况下外设访问内存
但是毕竟CPU的主要功能是计算,不能总让它做这种搬运数据的苦力活。所以后来诞生了DMA(Direct Memory Access),即直接存储器访问。DMA是一个数据搬运工,硬件可以通过它读写内存,DMA一般会被集成到设备当中。
在有DMA的情况下外设访问内存
外设发出DMA访存请求时,会在PCIe总线上发出总线地址,而一般情况下这个总线地址的值等于物理地址,我们可以认为外设发出的就是物理地址。这个请求被PCIe RC收到之后,它会通过系统总线来访问内存。
外设通过物理地址访问内存
IO虚拟地址
外设虽然可以通过物理地址直接访问内存,但是也产生了跟CPU通过物理地址访存类似的问题,其中对于外设最重要的一点便是:有些设备可能会需要大量地址连续的物理内存,比如HCA/RNIC就会耗费许多内存来放置软硬件进行交互的队列,以及队列的属性等等。
虽然可以通过自己设计多级寻址的方式来解决这个问题,但是这大大增加了软硬件的复杂度。那么能不能把MMU的设计思想也用在外设上呢?当然可以,于是就产生了IOMMU(x86平台)/SMMU(ARM平台)这种专门用于给外设进行地址翻译的设备。
有了IOMMU/SMMU之后,外设跟使用MMU的CPU一样,看到的是一整片连续的虚拟地址。区别于CPU的VA,我们称这些地址为IO虚拟地址,即IOVA(Input/Output Virtual Address)。
IOMMU/SMMU记录着地址的转换关系,外设发出的IOVA,会被它翻译为PA,根据SMMU/IOMMU的位置不同,可能有两种情况:它有可能在设备内,也有可能挂在PCIe总线上,为多个EP提供地址翻译功能,这两种情况分别如下:
外设通过IOVA访问内存(1)
外设通过IOVA访问内存(2)
大家可能对IOVA和Bus Address的关系表示困惑,这两个地址可以认为是相等的。概念的差别在于,当我们强调SMMU/IOMMU的输入时,也就是被转换前的地址时,一般将地址称为IOVA;而当我们强调在总线上的地址时,比如PCIe,我们一般将使用的地址称为Bus Address。不必纠结它们的关系,我们RDMA领域更常使用IOVA的概念。
DMA地址
外设(PCIe EP)通过DMA访问内存时,发出的地址就是DMA地址。结合上面的描述我们可以知道,当设备使用了IOMMU/SMMU时,DMA地址是IO虚拟地址IOVA。当未使用IOMMU/SMMU时,DMA地址是物理地址PA。
总结
我们结合Linux系统中DMA API HOWTO[3]文档中的图以及实际流程来总结一下本文的内容。请注意如果外设使用PCIe总线的话,图中的B转为A时的host bridge是PCIe RC的一部分;而在相反的方向上,无论是否使用IOMMU,Z到Y之间的转换都需要经过PCIe RC,只不过一般情况下PCIe是不对外设访问的物理地址做地址转换的。
地址空间之间的关系
现在假设我们的RDMA网卡需要一片IOVA连续的内存用于记录某个连接的状态,运行在CPU侧的驱动程序是如何申请内存,并且将内存的DMA地址告知网卡的呢?
一般情况下,网卡会使用下面的接口申请DMA Buffer:
cpu_virt_addr = dma_alloc_coherent(dev, size, &dma_addr, gfp);
其中dev是设备的指针,size是申请内存的大小。dma_addr是硬件访问这片内存所需要提供的DMA地址,gfp用于属性控制,我们目前不关心。而返回值cpu_virt_addr则是CPU访问这片内存所需要使用的虚拟地址。
调用接口之后我们获得的dma_addr就是上图中总线地址空间上的IO虚拟地址Z,返回的cpu_virt_addr对应上图中CPU虚拟地址空间中的虚拟地址X,这两个地址分别会在使用时由IOMMU/SMMU和MMU进行转换,转换的结果都是物理地址空间中的物理地址Y。
但是到目前为止,硬件还没有拿到这个dma_addr,所以还是要以某种方式告知硬件,这就用到了我们上面提到的MMIO。
外设在PCIe总线上注册时,会将一些寄存器配置到MMIO空间,然后驱动程序在初始化时,会将物理地址空间中的MMIO映射到虚拟地址空闲中。这样当驱动程序申请到表项的DMA地址之后,就可以把这个地址写入到初始化时映射的对应的寄存器上了,此外也会把内存区域的大小也通过寄存器告知硬件。如此一来,硬件就拿到了表项的地址信息,可以在需要的时候自行通过DMA访问这些内存了。
对应上图,某个总线地址是A的用于储存表项基地址的寄存器被配置到了MMIO空间,对应的物理地址是B,然后驱动将MMIO空间映射到CPU虚拟地址空间后,B对应的虚拟地址是C。CPU想把表项基地址告知硬件的时候,只需要将DMA地址写入虚拟地址C中就可以了,后面的工作将由MMU和PCIe RC完成。
好了,关于地址的基础知识我们就介绍到这里,大家如果有疑问欢迎在评论区提出。
下期预告:计划介绍下RDMA软件栈中关于MR的更多内容。
参考链接
[2] 关于逻辑地址、线性地址、虚拟地址、物理地址的理解 - 广漠飘羽 - 博客园 (cnblogs.com)
[3] DMA API HOWTO. Linux Kernel. https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt