惊人的 IOMMU Overhead 与不合理的 AQC 网卡驱动

惊人的 IOMMU Overhead 与不合理的 AQC 网卡驱动

属于CYY自己的世界 (Yangyu Chen)

背景

给家里的 AMD AI MAX 395 小主机加了个 Thunderbolt 万兆网卡,期望给这台小主机通上 10Gbps 的信息高速公路,这样我可以利用家里的 NAS 的大容量存储快速拉 LLM ,网卡用的是几年前买给 Mac 用的 QNAP QNA-T310G1S 网卡(反正平时也在吃灰),采用的是 JHL6240 + AQC100 的方案。

然而,性能问题却困扰着我。

性能测试

Thunderbolt 接上去直接打开 iperf3 测试接收性能,直接傻眼:

➜  linux git:(aqc_fix) ✗ iperf3 -c fe80::3a63:bbff:fe2e:1a68%enp99s0 -R
Connecting to host fe80::3a63:bbff:fe2e:1a68%enp99s0, port 5201
Reverse mode, remote host fe80::3a63:bbff:fe2e:1a68%enp99s0 is sending
[ 5] local fe80::265e:beff:fe6a:4da1 port 39588 connected to fe80::3a63:bbff:fe2e:1a68 port 5201
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 271 MBytes 2.27 Gbits/sec
[ 5] 1.00-2.00 sec 270 MBytes 2.27 Gbits/sec
[ 5] 2.00-3.00 sec 268 MBytes 2.25 Gbits/sec
[ 5] 3.00-4.00 sec 270 MBytes 2.26 Gbits/sec
[ 5] 4.00-5.00 sec 268 MBytes 2.25 Gbits/sec
[ 5] 5.00-6.00 sec 269 MBytes 2.26 Gbits/sec
[ 5] 6.00-7.00 sec 268 MBytes 2.25 Gbits/sec
[ 5] 7.00-8.00 sec 268 MBytes 2.25 Gbits/sec
[ 5] 8.00-9.00 sec 268 MBytes 2.25 Gbits/sec
[ 5] 9.00-10.00 sec 268 MBytes 2.25 Gbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 2.63 GBytes 2.26 Gbits/sec 1 sender
[ 5] 0.00-10.00 sec 2.63 GBytes 2.26 Gbits/sec receiver

iperf Done.

作为对比,同等条件用板载的 2.5G 网卡都能打 2.32Gbps 的速率,虽然上行可以接近打满 10Gbps,但约等于升级了个寂寞。

调试过程

通过观察 htop 发现, iperf3 -R 测试时 CPU 内核时间占用极高,于是我便通过 perf record iperf3 -c $SERVER -R 采样了一下内核的热点函数,结果如下:

熟悉 IOMMU 的朋友这里应该很容易发现一个异常,那就是出现了 swiotlb

为了让更多的读者理解,这里补充一个小知识,在没有 IOMMU 之前,所有的 DMA 外设都可以访问并修改整个物理内存地址空间,对于 Thunderbolt 来说这是个相当危险的事情,意味着通过 Type-C 接口连接一个外设即可 dump 整个物理内存,这样就毫无隐私可言了。在有了 IOMMU 后,通常每个 PCIe Port 会划分为一个单独的 IOMMU Group ,在这个 Group 内的 DMA 访问都通过 IO 页表进行映射,只有允许外设访问的地址才会在 IO 页表中创建映射,由此外设只能访问其所被操作系统允许访问的物理内存地址空间,并通过虚拟地址的方式外设既不知道真实的物理地址,也不需要保证物理地址的连续。

而 swiotlb 则是一个很特殊的 DMA Buffer,在没有 IOMMU 的时代最初用于解决外设无法访问完整的物理地址空间的情况(例如外设的 DMA 只能访问 32bit 寻址范围内的地址),而如果外设太多的情况下低 32位 的空间可能不够如此多的外设分配。由于 Linux 中大多数的驱动的单个 DMA 地址段都是单向的,即要么是 Device 写 Host 读,要么是 Host 读 Device 写,因此 Linux 可以将这些单向的 DMA 分配到一个 bounce buffer 中,在使用时再进行拷贝即可。而在有了 IOMMU 的时代,它的出现通常用于解决非对齐的 DMA 映射问题。例如我们的 IOMMU 最小映射的粒度是 4KB,但是驱动在调用 dma_map_single 这样的内核函数时,开始地址和结束地址不一定对齐到了页面,此时就会存在外设应该仅能访问页面内的一部分地址段的问题。为此,Linux 在这种情况下继续提供了 swiotlb 作为 work around,前后进行一次拷贝,以实现这段映射空间内不泄漏外设不应该访问的数据。

到此,我们已经发现了驱动可能存在的性能问题,随后,我进行了一个小测试,如果我们完全不管 IO 安全,也就是在 Linux 的启动参数设置 iommu=off ,性能会如何?

我重启了机器测试了一下,结果直接打满 10Gbps:

➜  ~ iperf3 -c fe80::3a63:bbff:fe2e:1a68%enp99s0 -R
Connecting to host fe80::3a63:bbff:fe2e:1a68%enp99s0, port 5201
Reverse mode, remote host fe80::3a63:bbff:fe2e:1a68%enp99s0 is sending
[ 5] local fe80::265e:beff:fe6a:4da1 port 36924 connected to fe80::3a63:bbff:fe2e:1a68 port 5201
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 1.08 GBytes 9.27 Gbits/sec
[ 5] 1.00-2.00 sec 1.08 GBytes 9.28 Gbits/sec
[ 5] 2.00-3.00 sec 1.08 GBytes 9.28 Gbits/sec
[ 5] 3.00-4.00 sec 1.08 GBytes 9.28 Gbits/sec
[ 5] 4.00-5.00 sec 1.08 GBytes 9.28 Gbits/sec
[ 5] 5.00-6.00 sec 1.08 GBytes 9.28 Gbits/sec
[ 5] 6.00-7.00 sec 1.08 GBytes 9.28 Gbits/sec
[ 5] 7.00-8.00 sec 1.08 GBytes 9.27 Gbits/sec
[ 5] 8.00-9.00 sec 1.08 GBytes 9.25 Gbits/sec
[ 5] 9.00-10.00 sec 1.08 GBytes 9.25 Gbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 10.8 GBytes 9.28 Gbits/sec 29 sender
[ 5] 0.00-10.00 sec 10.8 GBytes 9.27 Gbits/sec receiver

iperf Done.

由此,我们已经将性能问题的范围 reduce 到 IOMMU 相关了。

顺藤摸瓜,我开始思考为什么会存在 swiotlb,我开始观测网卡驱动中的 dma_map* 有关函数:

➜  atlantic git:(aqc_fix) pwd        
/home/cyy/linux/drivers/net/ethernet/aquantia/atlantic
➜ atlantic git:(aqc_fix) grep -r "dma_map"
atlantic.mod.c: { 0x3d77a902, "dma_map_page_attrs" },
aq_ring.c: daddr = dma_map_page(dev, page, 0, PAGE_SIZE << order,
aq_ring.c: if (unlikely(dma_mapping_error(dev, daddr)))
aq_nic.c: dx_buff->pa = dma_map_single(dev, xdpf->data, dx_buff->len,
aq_nic.c: if (unlikely(dma_mapping_error(dev, dx_buff->pa)))
aq_nic.c: frag_pa = skb_frag_dma_map(dev, frag, buff_offset,
aq_nic.c: if (unlikely(dma_mapping_error(dev, frag_pa)))
aq_nic.c: dx_buff->pa = dma_map_single(dev,
aq_nic.c: if (unlikely(dma_mapping_error(dev, dx_buff->pa))) {
aq_nic.c: frag_pa = skb_frag_dma_map(dev,
aq_nic.c: if (unlikely(dma_mapping_error(dev,

这里我们观测到两个 dma_map_single 非常可疑,于是我进行了一番修改,我们把整个页面边界都 map 过去:

diff --git a/drivers/net/ethernet/aquantia/atlantic/aq_nic.c b/drivers/net/ethernet/aquantia/atlantic/aq_nic.c
index b24eaa5283fa..7c6828a0eb2c 100644
--- a/drivers/net/ethernet/aquantia/atlantic/aq_nic.c
+++ b/drivers/net/ethernet/aquantia/atlantic/aq_nic.c
@@ -731,12 +731,16 @@ unsigned int aq_nic_map_skb(struct aq_nic_s *self, struct sk_buff *skb,
}

dx_buff->len = skb_headlen(skb);
+ uintptr_t align_page = ((uintptr_t)skb->data) & PAGE_MASK;
+ uintptr_t align_offset = (uintptr_t)skb->data - align_page;
+ u32 aligned_len = (align_offset + dx_buff->len + PAGE_SIZE - 1U) &
+ PAGE_MASK;
dx_buff->pa = dma_map_single(dev,
- skb->data,
- dx_buff->len,
- DMA_TO_DEVICE);
+ (void*)align_page,
+ aligned_len,
+ DMA_TO_DEVICE) + align_offset;

- if (unlikely(dma_mapping_error(dev, dx_buff->pa))) {
+ if (unlikely(dma_mapping_error(dev, dx_buff->pa - align_offset))) {
ret = 0;
goto exit;
}

刚好 liunx-perf 也有 swiotlb 的 event,那我们就试试看效果如何吧:

修改前:

➜  linux git:(master) ✗ sudo perf stat -e swiotlb:swiotlb_bounced iperf3 -c fe80::3a63:bbff:fe2e:1a68%enp99s0 -R    
Connecting to host fe80::3a63:bbff:fe2e:1a68%enp99s0, port 5201
Reverse mode, remote host fe80::3a63:bbff:fe2e:1a68%enp99s0 is sending
[ 5] local fe80::265e:beff:fe6a:4da1 port 57850 connected to fe80::3a63:bbff:fe2e:1a68 port 5201
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 268 MBytes 2.24 Gbits/sec
[ 5] 1.00-2.00 sec 266 MBytes 2.24 Gbits/sec
[ 5] 2.00-3.00 sec 266 MBytes 2.23 Gbits/sec
[ 5] 3.00-4.00 sec 267 MBytes 2.24 Gbits/sec
[ 5] 4.00-5.00 sec 266 MBytes 2.23 Gbits/sec
[ 5] 5.00-6.00 sec 266 MBytes 2.23 Gbits/sec
[ 5] 6.00-7.00 sec 266 MBytes 2.23 Gbits/sec
[ 5] 7.00-8.00 sec 266 MBytes 2.23 Gbits/sec
[ 5] 8.00-9.00 sec 266 MBytes 2.23 Gbits/sec
[ 5] 9.00-10.00 sec 266 MBytes 2.24 Gbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 2.61 GBytes 2.24 Gbits/sec 2 sender
[ 5] 0.00-10.00 sec 2.60 GBytes 2.23 Gbits/sec receiver

iperf Done.

Performance counter stats for 'iperf3 -c fe80::3a63:bbff:fe2e:1a68%enp99s0 -R':

53,183 swiotlb:swiotlb_bounced

10.008265531 seconds time elapsed

0.025647000 seconds user
0.370087000 seconds sys

修改后:

➜  linux git:(aqc_swiotlb_fix) ✗ sudo perf stat -e swiotlb:swiotlb_bounced iperf3 -c fe80::3a63:bbff:fe2e:1a68%enp99s0 -R
Connecting to host fe80::3a63:bbff:fe2e:1a68%enp99s0, port 5201
Reverse mode, remote host fe80::3a63:bbff:fe2e:1a68%enp99s0 is sending
[ 5] local fe80::265e:beff:fe6a:4da1 port 43772 connected to fe80::3a63:bbff:fe2e:1a68 port 5201
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 269 MBytes 2.25 Gbits/sec
[ 5] 1.00-2.00 sec 268 MBytes 2.25 Gbits/sec
[ 5] 2.00-3.00 sec 268 MBytes 2.25 Gbits/sec
[ 5] 3.00-4.00 sec 269 MBytes 2.25 Gbits/sec
[ 5] 4.00-5.00 sec 268 MBytes 2.25 Gbits/sec
[ 5] 5.00-6.00 sec 269 MBytes 2.25 Gbits/sec
[ 5] 6.00-7.00 sec 268 MBytes 2.25 Gbits/sec
[ 5] 7.00-8.00 sec 268 MBytes 2.25 Gbits/sec
[ 5] 8.00-9.00 sec 268 MBytes 2.25 Gbits/sec
[ 5] 9.00-10.00 sec 268 MBytes 2.25 Gbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 2.62 GBytes 2.25 Gbits/sec 3 sender
[ 5] 0.00-10.00 sec 2.62 GBytes 2.25 Gbits/sec receiver

iperf Done.

Performance counter stats for 'iperf3 -c fe80::3a63:bbff:fe2e:1a68%enp99s0 -R':

8 swiotlb:swiotlb_bounced

10.005899095 seconds time elapsed

0.004480000 seconds user
0.376320000 seconds sys

可以看到,虽然 swiotlb_bounced 事件减少到几乎可以忽略不计了,但是性能还是这个鬼样子。

于是我继续阅读驱动的代码和一些参数的文档,无意间让我发现了一个这样的描述:

# Documentation/networking/device_drivers/ethernet/aquantia/atlantic.rst
For some fine tuning and performance optimizations,
some parameters can be changed in the {source_dir}/aq_cfg.h file.

AQ_CFG_RX_PAGEORDER
-------------------

Default value: 0

RX page order override. That's a power of 2 number of RX pages allocated for
each descriptor. Received descriptor size is still limited by
AQ_CFG_RX_FRAME_MAX.

Increasing pageorder makes page reuse better (actual on iommu enabled systems).

我们继续观测驱动中接收 eth frame 的代码:

➜  linux git:(aqc_swiotlb_fix) ✗ batcat -r 84:110 drivers/net/ethernet/aquantia/atlantic/aq_ring.c
───────┬──────────────────────────────────────────────────────────────────────────────────────────
│ File: drivers/net/ethernet/aquantia/atlantic/aq_ring.c
───────┼──────────────────────────────────────────────────────────────────────────────────────────
84 │ static int aq_get_rxpages(struct aq_ring_s *self, struct aq_ring_buff_s *rxbuf)
85 │ {
86 │ unsigned int order = self->page_order;
87 │ u16 page_offset = self->page_offset;
88 │ u16 frame_max = self->frame_max;
89 │ u16 tail_size = self->tail_size;
90 │ int ret;
91 │
92 │ if (rxbuf->rxdata.page) {
93 │ /* One means ring is the only user and can reuse */
94 │ if (page_ref_count(rxbuf->rxdata.page) > 1) {
95 │ /* Try reuse buffer */
96 │ rxbuf->rxdata.pg_off += frame_max + page_offset +
97 │ tail_size;
98 │ if (rxbuf->rxdata.pg_off + frame_max + tail_size <=
99 │ (PAGE_SIZE << order)) {
100 │ u64_stats_update_begin(&self->stats.rx.syncp);
101 │ self->stats.rx.pg_flips++;
102 │ u64_stats_update_end(&self->stats.rx.syncp);
103 │
104 │ } else {
105 │ /* Buffer exhausted. We have other users and
106 │ * should release this page and realloc
107 │ */
108 │ aq_free_rxpage(&rxbuf->rxdata,
109 │ aq_nic_get_dev(self->aq_nic));
110 │ u64_stats_update_begin(&self->stats.rx.syncp);
───────┴──────────────────────────────────────────────────────────────────────────────────────────
➜ linux git:(aqc_swiotlb_fix) ✗

好家伙,原来这个这个只是下界呀,明明更大的 rx page order 也就可以存储更多的 rx frame 而不需要频繁修改 IOMMU 的映射。

而让我更加意想不到的是,原来网卡驱动可以如此草台班子,一个 descriptor 只开一个页面,MTU 1500 时只能放 2 个 eth frame,超过了就要进行 Overhead 非常之大的 IOMMU 重映射操作。

于是,我尝试把 AQ_CFG_RX_PAGEORDER 改成了 3,也就是原来的8倍大小的 buffer,并且 revert 了对 swiotlb 的修改,结果:

➜  ~ iperf3 -c fe80::3a63:bbff:fe2e:1a68%enp99s0 -R
Connecting to host fe80::3a63:bbff:fe2e:1a68%enp99s0, port 5201
Reverse mode, remote host fe80::3a63:bbff:fe2e:1a68%enp99s0 is sending
[ 5] local fe80::265e:beff:fe6a:4da1 port 36924 connected to fe80::3a63:bbff:fe2e:1a68 port 5201
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 1.08 GBytes 9.27 Gbits/sec
[ 5] 1.00-2.00 sec 1.08 GBytes 9.28 Gbits/sec
[ 5] 2.00-3.00 sec 1.08 GBytes 9.28 Gbits/sec
[ 5] 3.00-4.00 sec 1.08 GBytes 9.28 Gbits/sec
[ 5] 4.00-5.00 sec 1.08 GBytes 9.28 Gbits/sec
[ 5] 5.00-6.00 sec 1.08 GBytes 9.28 Gbits/sec
[ 5] 6.00-7.00 sec 1.08 GBytes 9.28 Gbits/sec
[ 5] 7.00-8.00 sec 1.08 GBytes 9.27 Gbits/sec
[ 5] 8.00-9.00 sec 1.08 GBytes 9.25 Gbits/sec
[ 5] 9.00-10.00 sec 1.08 GBytes 9.25 Gbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 10.8 GBytes 9.28 Gbits/sec 29 sender
[ 5] 0.00-10.00 sec 10.8 GBytes 9.27 Gbits/sec receiver

iperf Done.

轻松跑满 10Gbps !

后续

给 Linux Kernel 交了个 Patch ,让 AQ_CFG_RX_PAGEORDER 可以被直接配置:https://lore.kernel.org/lkml/tencent_E71C2F71D9631843941A5DF87204D1B5B509@qq.com/

Generated by RSStT. The copyright belongs to the original author.

Source

Report Page