数据结构论坛

首页 » 分类 » 定义 » DatenLord在Rust中管理RDM
TUhjnbcbe - 2023/7/30 20:15:00

作者

施继成

转自《RustMagazine中文精选》

RMDA是近年越来越热门的高速网络传输协议,被广泛应用于超算中心和高端存储领域。RDMA的全称为RemoteDirectMemoryAccess,即允许本地内存被远端机器直接访问,该访问不经过被访问机器的操作系统,直接由网卡代为完成。正式因为网卡完成了大部分的数据传输工作,操作系统的负载被降低,使得其在大量数据传输的情况下具有更好的拓展性(scalability)。

为了保证远端能够正确和安全地访问本地内存,RDMA协议中有一系列规范来约束用户的访问,下面来简单介绍一下。

RDMA的内存管理

远端想要访问本地内存,首先需要本地的“同意”,即本地仅仅暴露想要暴露的内存,其他内存远端则不可访问。该“同意”操作,我们一般称为MemoryRegion(简称MR)注册,操作系统在收到该请求时会锁住该段申请内存,防止内存被swap到硬盘上,同时将这个注册信息告知RDMA专用网卡,RDMA网卡会保存虚拟地址到物理地址的映射。经过此番操作,由MR代表的内存暴露出来了,远端可以对其进行访问。处于安全的考虑,只有被允许的远端才可以访问,这些远端持有远端访问密钥,即RemoteKey(简称RKey),只有带有正确RKey的请求才能够访问成功。为了内存管理的细粒度化,RDMA还提供了MemoryWindow(简称MW),一个MR上可以分列出多块MW,并且每一块MW上都可以自定义访问权限。

除了上述中的MR和MW,RDMA中的内存管理还和ProtectDomain(简称PD)和QueuePair(简称QP)相关,这里不详细阐述这两个概念。下图详细介绍了,这些概念之间的依赖关系:

现有的RDMA开发接口,即InfiniBandVerbs接口(简称IBV接口)并没有显式地展现这种依赖关系,但在实际使用中,任何不按规定顺序的资源释放都会造成错误,而用户找到问题的根本原因则非常困难。更进一步,当MR或者MW中的任何内存段被使用时,对应的MR和MW都不应该被释放或注销,这些在原有的IBV接口中也很难规范化。Rust作为一门内存安全的语言,在处理类似问题上具有天然优势,接下来我们来分析如何用Rust解决上述问题。

利用Rust特性管理RDMA内存

AllocatorAPI

在Rust的nightlyfeature中有一个叫AllocatorAPI,这个feature允许用户创建自己的Allocator,之后创建堆上数据时可以制定使用用户定制的Allocator。大家很自然能想到,MR或者MW很适合作为一种Allocator,后续用户创建的Vector或者Box都可以使用其中的内存,这些内存可以直接开放给远端访问,既方便又高效。

但AllocatorAPI有个核心问题无法保证,即Allocator本身应该比所有从其分配的数据活得更久,不然就会产生数据访问不安全的问题,如useafterfree问题。下列例子很好得阐述这一问题:

在alloc_vec方法中我们创建了一个新的Allocator叫CusAllocator,在方法结束时,该Allocator已经被释放,使用其内存的vector仍然存活着,被后续使用。Rust语言无法判断出潜在的风险,唯一能够解决该问题的办法就是将CusAllocator变成static变量,这样其生命周期和整个程序一样长,也就不存在useafterfree的问题。然而该解决方法不适用MR和MW的场景,原因是MR和MW会随着用户的使用动态注册和注销,无法被注销的MR和MW会影响使用的便利性。若初始化太大的内存块,系统的内存压力太大,其他程序容易触发OOM问题;若初始化内存块太小,用户的使用会受到限制。结合上述考虑,Allocator不是一个可行的方案。

Reference还是ReferenceCount

Rust语言中Reference带有生命周期属性,非常适合用来管理依赖关系,即被依赖Ref的生命周期不短于依赖者的生命周期,但是其在处理自引用时非常困难,当结构复杂到一定成都仅仅依赖Reference很难设计出用户易于使用的接口。因此我们采用了下列的设计方式:

这样核心数据接口都放到了堆上管理,同时保证了被依赖的数据结构一定不会提前释放——RC特性的保证。解决了核心数据结构的管理,内存使用的管理则更加简单,下列方法保证了当有内存被使用时,MR或者MW一定不会被释放。

在此基础上,配合一些序列化方法,MemoryRegion则可以处理各种数据结构的传输。

远端访问

RDMA是为了远端数据访问而存在的,仅仅管理好本地内存还不够,如何保证远端访问时本地内存的可靠性也很重要,不过Rust语言本身的特性只能够维护本地内存的安全性,远程访问需要更上层的设计来完成。我们在我们的设计中提供了类似的接口来完成相应的任务:

timeout表示该MemoryRegion外部能访问的最长时间段,如果fFuture提前结束,则我们可以提前回收MemoryRegion,否则至少等待timeout的时间长度才能回收。其中f可以在以下场景进行不同的操作:

1.在一对一传输的场景中,f将传输的必要信息传递给对方,等待对方完成的回复,一旦收到回复则结束future。

2.在一对多传输的场景中,f将传输的必要信息放到某个看板上,然后等待timeout时间的结束。

这里之所以要在接口中固定一个timeout,是为了防止内存被无限期得占用不能够释放,最终造成内存泄露。例如上述的第一个场景,对方如果由于软硬件的问题程序结束前并没有给出回复,则timeout至少可以保证MemoryRegion在timeout时间段之后还有释放的机会。

值得注意的是,这里的机制并不能完全避免远端访问错误的发生,本次程序无法控制远端程序的弱点仍然存在,因此,RDMA自带的保护机制也能够避免错误数据访问的发生,相应请求的失败会带来一些性能损失,这是无法避免的tradeoff。

总结

Rust语言的内存安全性部分地解决RDMA内存管理问题,同时上层的使用接口设计也部分解决了RDMA远端访问的管理问题。欢迎在我们的RustRDMA封装项目交流讨论,促进项目发展,使得Rust社区能够更方便地使用RDMA网络。

1
查看完整版本: DatenLord在Rust中管理RDM