ibv_fork_init 原理,局限性与最佳实践

08 Oct 2022 | RDMA | | ˚C

本文介绍RDMA verbs函数: ibv_fork_init() 的出现原因、作用原理、局限性,并提出RDMA编程中关于fork场景的最佳实践。

RDMA内存读写原理

  1. 应用申请MR内存块

  2. 调用ibv_reg_mr,在RNIC中注册内存块

  3. RNIC对内存块的读写采用DMA方式,读写过程应用进程无感知

具体过程不多赘述。

fork 带来的问题

Copy-On-Write (COW)

COW是对fork创建子进程过程的一个性能优化手段。

当fork刚刚创建好子进程时,父子进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。

同时,相关内存页被标记为只读,无论父子进程对其进行的首次写入操作,都会触发page fault,进而会重新分配内存页,进行内存拷贝,然后谁触发的写操作,就更新谁的进程页表 (见: Does parent process lose write ability during copy on write?)。

问题

考察一个典型的场景:

某进程P注册的MR对应内存页记为R。P调用fork创建子进程C,C调用exec系列函数加载新的程序。此时存在如下2种可能的时序:

  1. C执行exec系列函数完成之前,P没有对R的写入(例如,P主动block直到C运行结束): 此后,C执行exec完,替换自身页表,P会一直保留原有的内存页R,不会触发COW,不会出问题

  2. 在C执行完成exec之前,P对R进行写入操作: 此时,P触发COW,系统复制了一份R的副本R’,并用R’更新了P的页表,如图:

但是,RNIC仍持有原先的R做DMA,而P由于页表改变,错误的从R’ 读写RDMA数据,导致与RNIC不一致,通信数据出现错误。

注意,虽然rdma mr的内存受mlock保护,也不会改变这种行为。mlock只是保证这些内存页不会被换到swap当中,即所谓的pinned内存。

ibv_fork_init 原理

为了解决上述问题,RDMA开发人员向内核贡献了MADV_DOFORK/MADV_DONTFORK 两个madvise的flag (madvise MADV_DONTFORK/MADV_DOFORK),并且新增了ibv_fork_init函数 (Make fork() work for verbs consumers)。

这些措施仍然没有完美解决问题,见下一节

MADV_DONTFORK

madvise 通过将某内存页标记为MADV_DONTFORK,强迫fork场景下该页不会被复制

ibv_fork_init

该函数需要在使用RDMA功能之前开启。开启后,基本只会影响ibv_reg_mribv_dereg_mr两个函数。

原理:

开启ibv_fork_init后,调用ibv_reg_mr时,会对相关内存,调用madvise 打标记MADV_DONTFORK

由于madvise需要对整页进行做标记,但是mr内存不一定是整页,所以verbs库内部对所有注册的MR内存地址段进行了记录:

  1. 建立红黑树,记录全部mr,以及mr所在的内存页

  2. 每一次调用ibv_reg_mr时,判断当前mr所在的内存页的起止地址,检查是否红黑树中存在记录

  3. 如果不存在,则加入红黑树并调用madvise打标记MADV_DONTFORK

  4. 每一次调用ibv_dereg_mr,使用当前mr地址查询红黑树

  5. 如果该mr所在内存页没有包含其他已注册的mr,则调用madvise打标记MADV_DOFORK

通过对所有已注册的MR所在内存页打MADV_DONTFORK标记,创建子进程后,MR所在内存页不会触发COW拷贝,避免了前面所说的COW带来网卡DMA内存地址不一致的问题。

ibv_fork_init的局限性

ibv_fork_init仍然存在问题,根源是设计上的一个矛盾:ibv_reg_mr允许任意内存地址段,而madvise只能对整个内存页进行标记。

通过上面对ibv_fork_init的行为描述,可以很容易看出,如果mr的地址段不是整内存页,则ibv_reg_mr会对mr之外的内存做标记,如图:

蓝色部分是注册的MR,红色部分是其他内存,但是红色部分也会被标记为MADV_DONTFORK

如果是fork-exec的用法,子进程不会使用父进程的任何内存,则多标记这一部分内存不会造成影响。

如果是单纯fork,并且子进程运行时会访问到红色部分的内存,则实际上子进程访问的是父进程的内存,就会出现内存访问的问题 (见 A Cursed Bugrdma-core: ibv_dontfork_range should not round up to page boundaries)。

相关解决方案

MR整页限制

最新的rdma-core版本中合入了一个提交 ( verbs: Allow aligned address & size only for fork init #1222) :在开启ibv_fork_init后,强迫所有mr的起始地址必须是内存页对齐,长度必须是内存页整数倍,其实就是强迫只能按照整页去注册mr,修复了ibv_fork_init的局限性

copy-on-fork

最新的内核代码包含了copy-on-fork的功能 (https://github.com/torvalds/linux/commit/70e806e4e645019102d0e09d4933654fb5fb58ce 、 https://lore.kernel.org/linux-rdma/20210418121025.66849-1-galpress@amazon.com/) ,在fork时,DMA内存不再执行COW策略,而是直接复制到子进程,因此没有必要再做MADV_DONTFORK的标记。

相应的,为了使用该内核功能,verbs提供了一个新的功能 (见Report when ibv_fork_init() is not needed #975) :

引入函数:ibv_is_fork_initialized(),如果返回的是IBV_FORK_UNNEEDED,说明当前内核支持上述copy-on-fork特性,不必开启ibv_fork_init()

最佳实践


Older · View Archive (22)

C++14的异构比较

Newer

TCP开启SYN Cookies后导致数据丢失