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

Table of Contents

背景

给家里的 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 相关了。但是作为一个有安全素养的人,即使我们我们不连接奇怪的外设,但怎么能相信外设不会有 vulnerability,怎么能关着 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/

One Response

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to Top