1、背景
在达坦科技实习期间,笔者开发了一个RDMA用户态驱动,用于与达坦科技自研的BlueRDMA进行交互。整个驱动分为内核态和用户态两个部分,内核部分做的仅仅是将必要的内存空间暴露给用户态,例如将bar空间上的csr(control status register)寄存器映射到了用户态,主要的操作由用户态驱动执行。用户态驱动申请了4个巨页,并分别指定这几个巨页为与硬件交互的Ringbuf。当用户态驱动需要向硬件发送信息(即描述符)时,只需要在内存上写入若干个描述符,然后修改csr寄存器中队列头指针的值。当硬件发现Ringbuf队列非空时,就会向对应的内存区域发起DMA请求,获取描述符,执行相应的操作。
BlueRDMA基于BlueSim提供了一个可以在软件上进行模拟的仿真器。通过BlueRDMA中的脚本,软件能够与硬件仿真器交互测试功能。通过这个仿真器,软件能够与硬件代码协同测试。但是由于是全软件模拟硬件,仿真器的速度很慢,不适合跑较大规模的测试。通常是在仿真器上验证行为后,后续在硬件上运行更大规模的测试。小规模的测试往往不够充分,也留下了一些潜在的软件bug。笔者在仿真器上跑了一些测试,一切都工作正常,直到准备在硬件上进行性能测试。
具体的现象是,当关闭日志时,硬件有很大概率会出现丢包的问题。例如,当发送方写入了若干个描述符,但接收方并没有收到全部的包,只收到了一系列连续的报文,但未能收到期望的全部包。
由于与日志打印相关,笔者的第一反应是内存问题。在之前调试一些C/C++项目时,曾遇到过开启打印能消除“segment fault”的神奇bug。虽然这是一个Rust项目,内存安全性相对较好一些,但在涉及到Ringbuf相关内存申请时,不可避免会引入一些unsafe。经过反复检查各个unsafe和写入操作,感觉问题不在这上面。
会不会是发送速度的问题呢?开启打印日志不可避免会带来锁等开销,由于目前的程序在每个描述符下发到硬件上时都加上了一个日志,用于判断描述符是否正常写入,因此相当于每次描述符写入都要上锁。这操作相对较慢。关闭日志后,发送速度变快了。但是为什么变快了会导致发送数据不完整呢?
2、cache一致性的问题
与此同时,还遇到了另外一个bug,这主要与cache一致性相关。硬件向软件上报描述符时,同样是通过Ringbuf实现的。硬件会首先将描述符拷贝到Ringbuf上,然后修改Ringbuf的头指针,表示有新的描述符产生。软件会轮询Ringbuf的头指针,一旦头指针发生变化,就会从对应的内存区域读走描述符,同时修改尾指针,表示软件已读取。在实际测试中,这个机制一直工作得很好,但有时软件会读取到一些“全0”描述符。这种描述符的特征在于,即使你给DMA buffer提前填充了一些别的数据(例如全部填1),读取描述符时的数据仍然是0。
笔者一开始怀疑是硬件上报了“全0”描述符。在经过沟通和一些试验后,发现如果不立即读取这个新的描述符,而是稍等一会再去读取,就能读取到其内容。这可能与cache一致性相关。上文提到,用户态驱动使用了巨页作为与硬件交互的Ringbuf,巨页本身能提供超过4K的连续物理内存,但巨页本身并不具有DMA cache coherence。也就是说,可能存在内存上发生了DMA拷贝,但用户态程序读取的仍然是之前的cache内容。
会不会上面“丢包”的问题也是cache一致性导致的呢?毕竟如果写入描述符的速度变快了,很可能cache没有同步到物理内存上,然后就发生了连续丢包。在mentor的建议下,笔者决定先从cache一致性入手,或许解决cache一致性的问题,上面的丢包问题也能一并解决。
在阅读了erdma等项目mr注册部分的实现后,发现只需要使用dma_alloc_coherent接口就能拿到一块物理内存连续且设备读/写都能马上与CPU同步的内存。同样地,用户态驱动只需要通过mmap就能将这块Ringbuf挂载在自己的虚拟内存上。完成这一部分工作后,读取到“0描述符”的bug已经消失了,但发送方丢包的问题仍然存在。
经过再次检查写入Ringbuf的代码,并阅读内核中其他驱动读写bar空间的代码后,笔者发现用户态可能还缺少了volatile读/写操作。尝试使用Rust标准库提供的volatile_read/volatile_write,并在写入Buffer时加上fence,然而该问题依然未得到解决。
3、从单独的模块入手
为了进一步确定丢包的原因,笔者决定做一些实验进行探究。
上述的硬件测试是在两台机器上进行的。两台机器各有一张F