接下来,在上面的循环内部开始另一个循环:
/* clear last DMA location and unmap remaining buffers */ while (tx_desc != eop_desc) { tx_buffer++; tx_desc++; i++; if (unlikely(!i)) { i -= tx_ring->count; tx_buffer = tx_ring->tx_buffer_info; tx_desc = IGB_TX_DESC(tx_ring, 0); } /* unmap any remaining paged data */ if (dma_unmap_len(tx_buffer, len)) { dma_unmap_page(tx_ring->dev, dma_unmap_addr(tx_buffer, dma), dma_unmap_len(tx_buffer, len), DMA_TO_DEVICE); dma_unmap_len_set(tx_buffer, len, 0); } }
该内部循环将在每个传输描述符上循环,直到 tx_desc
到达 eop_desc
。 这段代码取消映射任何附加描述符引用的数据。
外部循环继续:
/* move us one more past the eop_desc for start of next pkt */ tx_buffer++; tx_desc++; i++; if (unlikely(!i)) { i -= tx_ring->count; tx_buffer = tx_ring->tx_buffer_info; tx_desc = IGB_TX_DESC(tx_ring, 0); } /* issue prefetch for next Tx descriptor */ prefetch(tx_desc); /* update budget accounting */ budget--; } while (likely(budget));
外部循环增加迭代器并减少 budget
值。 检查循环不变量以确定循环是否应继续。
netdev_tx_completed_queue(txring_txq(tx_ring), total_packets, total_bytes); i += tx_ring->count; tx_ring->next_to_clean = i; u64_stats_update_begin(&tx_ring->tx_syncp); tx_ring->tx_stats.bytes += total_bytes; tx_ring->tx_stats.packets += total_packets; u64_stats_update_end(&tx_ring->tx_syncp); q_vector->tx.total_bytes += total_bytes; q_vector->tx.total_packets += total_packets;
此代码:
- 调用
netdev_tx_completed_queue
,它是上面解释的 DQL API 的一部分。 如果处理了足够的传输完成,这将潜在地重新启用传输队列。 - 统计数据被添加到适当位置,以便用户可以访问它们,我们将在后面看到。
代码继续执行,首先检查是否设置了 IGBIGB_RING_FLAG_TX_DETECT_HANG
标志。 看门狗定时器在每次运行定时器回调时设置此标志,以强制执行传输队列的定期检查。 如果该标志现在恰好打开,代码将继续并检查传输队列是否挂起:
if (test_bit(IGB_RING_FLAG_TX_DETECT_HANG, &tx_ring->flags)) { struct e1000_hw *hw = &adapter->hw; /* Detect a transmit hang in hardware, this serializes the * check with the clearing of time_stamp and movement of i */ clear_bit(IGB_RING_FLAG_TX_DETECT_HANG, &tx_ring->flags); if (tx_buffer->next_to_watch && time_after(jiffies, tx_buffer->time_stamp + (adapter->tx_timeout_factor * HZ)) && !(rd32(E1000_STATUS) & E1000_STATUS_TXOFF)) { /* detected Tx unit hang */ dev_err(tx_ring->dev, "Detected Tx Unit Hang\n" " Tx Queue <%d>\n" " TDH <%x>\n" " TDT <%x>\n" " next_to_use <%x>\n" " next_to_clean <%x>\n" "buffer_info[next_to_clean]\n" " time_stamp <%lx>\n" " next_to_watch <%p>\n" " jiffies <%lx>\n" " desc.status <%x>\n", tx_ring->queue_index, rd32(E1000_TDH(tx_ring->reg_idx)), readl(tx_ring->tail), tx_ring->next_to_use, tx_ring->next_to_clean, tx_buffer->time_stamp, tx_buffer->next_to_watch, jiffies, tx_buffer->next_to_watch->wb.status); netif_stop_subqueue(tx_ring->netdev, tx_ring->queue_index); /* we are about to reset, no point in enabling stuff */ return true; }
上面的 if
语句检查:
- 设置了
tx_buffer->next_to_watch
,并且 - 当前
jiffies
大于在传输路径上记录到tx_buffer
的time_stamp
,其中添加了超时因子,以及 - 设备的传输状态寄存器未设置为
E1000_STATUS_TXOFF
。
如果这三个测试都为真,则打印一个错误,表明检测到挂起。使用 netif_stop_subqueue
关闭队列,并返回 true
。
让我们继续阅读代码,看看如果没有传输挂起检查,或者如果有,但没有检测到挂起,会发生什么:
#define TX_WAKE_THRESHOLD (DESC_NEEDED * 2) if (unlikely(total_packets && netif_carrier_ok(tx_ring->netdev) && igb_desc_unused(tx_ring) >= TX_WAKE_THRESHOLD)) { /* Make sure that anybody stopping the queue after this * sees the new next_to_clean. */ smp_mb(); if (__netif_subqueue_stopped(tx_ring->netdev, tx_ring->queue_index) && !(test_bit(__IGB_DOWN, &adapter->state))) { netif_wake_subqueue(tx_ring->netdev, tx_ring->queue_index); u64_stats_update_begin(&tx_ring->tx_syncp); tx_ring->tx_stats.restart_queue++; u64_stats_update_end(&tx_ring->tx_syncp); } } return !!budget;
在上面的代码中,驱动程序重新启动传输队列(如果先前已禁用)。它首先检查是否:
- 某些数据包已经处理完成(
total_packets
非零),并且 netif_carrier_ok
以确保设备未被关闭,以及- 传输队列中未使用的描述符数量大于或等于
TX_WAKE_THRESHOLD
。在我的 x86_64 系统上,此阈值似乎为42
。
如果所有条件都满足,则使用写屏障(smp_mb
)。接下来检查另一组条件:
- 如果队列已停止,并且
- 设备未关闭
然后调用 netif_wake_subqueue
唤醒传输队列并向更高层次发出信号,表示它们可以再次排队数据。增加 restart_queue
统计计数器。接下来我们将看到如何读取此值。
最后,返回一个布尔值。如果有任何剩余的未使用预算,则返回 true
,否则返回 false
。在 igb_poll
中检查此值以确定返回给 net_rx_action
的内容。
igb_poll
返回值
igbigb_poll
函数有以下代码来确定返回给 net_rx_action
:
if (q_vector->tx.ring) clean_complete = igb_clean_tx_irq(q_vector); if (q_vector->rx.ring) clean_complete &= igb_clean_rx_irq(q_vector, budget); /* If all work not completed, return budget and keep polling */ if (!clean_complete) return budget;
换句话说,如果:
igb_clean_tx_irq
清除了所有传输完成,而没有耗尽其传输完成预算,以及igb_clean_rx_irq
清除了所有传入数据包,而没有耗尽其数据包处理预算
然后,将返回整个预算数量(对于大多数驱动程序,它被硬编码为 64
,包括 igb
)。 如果传输或传入处理中的任何一个不能完成(因为还有更多的工作要做),则调用 napi_complete
并返回 0
:
/* If not enough Rx work done, exit the polling mode */ napi_complete(napi); igb_ring_irq_enable(q_vector); return 0; }
监控网络设备
有几种不同的方法可以监控网络设备,提供不同级别的粒度和复杂性。 让我们从最细粒度开始,然后转到最细粒度。
使用 ethtool -S
你可以运行以下命令在 Ubuntu 系统上安装 ethtool
:sudo apt-get install ethtool
.
安装后,您可以传递 -S
标志以及需要统计信息的网络设备的名称来访问统计信息。
使用 ethtool -S
监控详细的 NIC 设备统计信息(例如, 传输错误)。
$ sudo ethtool -S eth0 NIC statistics: rx_packets: 597028087 tx_packets: 5924278060 rx_bytes: 112643393747 tx_bytes: 990080156714 rx_broadcast: 96 tx_broadcast: 116 rx_multicast: 20294528 ....
监测这些数据可能很困难。 它很容易获得,但字段值没有标准化。 不同的驱动程序,甚至不同版本的同一 驱动可能会产生具有相同含义的不同字段名称。
你应该在标签中寻找带有“drop”、“buffer”、“miss”、“errors”等的值。接下来,您将不得不阅读驱动程序源代码。您将能够确定哪些值完全在软件中计算(例如,在没有内存时增加)以及哪些值直接通过寄存器从硬件读取获得。对于寄存器值,您应该查阅硬件的数据表以确定计数器的真实含义; ethtool
给出的许多标签可能会产生误导。
使用 sysfs
sysfs 也提供了许多统计值,但它们比直接提供的 NIC 级别统计值略高一些。
您可以使用 cat
在文件上查找丢弃的传入网络数据帧的数量,例如 eth0。
使用 sysfs 监控更高级别的 NIC 统计信息。
$ cat /sys/class/net/eth0/statistics/tx_aborted_errors 2
计数器值将被拆分为 tx_aborted_errors
、tx_carrier_errors
、tx_compressed
、tx_dropped
等文件。
不幸的是,由驱动程序来决定每个字段的含义,以及何时增加它们以及值来自何处。 您可能会注意到,一些驱动程序将某种类型的错误情况视为丢弃,但其他驱动程序可能会将其视为未命中。
如果这些值对您很重要,您需要阅读驱动程序源代码和设备数据表,以准确了解驱动程序认为的每个值的含义。
使用 /proc/net/dev
更高级的文件是 /proc/net/dev
,它为系统上的每个网络适配器提供高级摘要式信息。
读取 /proc/net/dev
来监视高级 NIC 统计信息。
$ cat /proc/net/dev Inter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed eth0: 110346752214 597737500 0 2 0 0 0 20963860 990024805984 6066582604 0 0 0 0 0 0 lo: 428349463836 1579868535 0 0 0 0 0 0 428349463836 1579868535 0 0 0 0 0 0
这个文件显示了您在上面提到的 sysfs 文件中找到的值的子集,但它可能作为一个有用的一般参考。
上面提到的警告也适用于这里:如果这些值对您很重要,您仍然需要阅读驱动程序源代码,以准确了解何时、何地以及为什么它们会增加,以确保您对 error、drop 或 fifo 的理解与驱动程序相同。
监控动态队列限制
您可以读取位于以下位置的文件来监控网络设备的动态队列限制:
/sys/class/net/NIC/queues/tx-QUEUE_NUMBER/byte_queue_limits/
。
替换 NIC
为您的设备名称(eth0
、eth1
等),替换 tx-QUEUE_NUMBER
为传输队列号(tx-0
、tx-1
、tx-2
等)。
其中一些文件是:
hold_time
:初始化为HZ
(单个赫兹)。 如果队列在hold_time
内已满,则减小最大大小。inflight
:它是尚未处理完成的正在传输的数据包的当前数量。该值等于(排队的数据包数量-完成的数据包数量)。limit_max
:硬编码值,设置为DQL_MAX_LIMIT
(在我的 x86_64 系统上为1879048192
)。limit_min
:硬编码值,设置为0
。limit
:一个介于limit_min
和limit_max
之间的值,表示当前可以排队的对象的最大数量。
在修改任何这些值之前,强烈建议阅读这些演示幻灯片,以深入了解算法。
读取 /sys/class/net/eth0/queues/tx-0/byte_queue_limits/inflight
监控在传输过程中的数据包情况。
$ cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/inflight 350
调优网络设备
检查正在使用的传输队列数
如果您的 NIC 和系统上加载的设备驱动程序支持多个传输队列,则通常可以使用 ethtool 调整 TX 队列(也称为 TX 通道)的数量 ethtool
。
使用 ethtool 检查 NIC 传输队列的数量 ethtool
$ sudo ethtool -l eth0 Channel parameters for eth0: Pre-set maximums: RX: 0 TX: 0 Other: 0 Combined: 8 Current hardware settings: RX: 0 TX: 0 Other: 0 Combined: 4
此输出显示预设的最大值(由驱动程序和硬件强制执行)和当前设置。
注意: 并非所有设备驱动程序都支持此操作。
如果您的 NIC 不支持此操作,则会出现错误。
$ sudo ethtool -l eth0 Channel parameters for eth0: Cannot get device channel parameters : Operation not supported
这意味着您的驱动程序尚未实现 ethtool get_channels
操作。 这可能是因为 NIC 不支持调整队列数量,不支持多个传输队列,或者您的驱动程序尚未更新以处理此功能。
调整使用的传输队列数
找到当前和最大队列计数后,可以使用 sudo ethtool -L
调整这些值。
注意: 某些设备及其驱动程序仅支持为发送和接收配对的组合队列,如上一节中的示例所示。
使用 ethtool -L
设置组合 NIC 传输和接收队列为 8
$ sudo ethtool -L eth0 combined 8
如果您的设备和驱动程序支持 RX 和 TX 的单独设置,并且您只想更改 TX 队列计数为 8,则可以运行:
使用 ethtool -L
设置 NIC 传输队列的数量为 8。
$ sudo ethtool -L eth0 tx 8
注意: 对于大多数驱动程序来说,进行这些更改将关闭接口,然后再重新打开; 与该接口的连接将被中断。 不过,这对于一次性的改变来说可能并不重要。
调整传输队列的大小
某些 NIC 及其驱动程序还支持调整 TX 队列的大小。 具体的工作原理是硬件相关的,但幸运的是,ethtool
为用户提供了一种通用的方法来调整大小。 由于使用了 DQL 来防止更高层次的网络代码在某些时候排队更多数据,因此增加发送队列的大小可能不会产生巨大的差异。尽管如此,您可能仍然希望增加发送队列到最大大小,并让 DQL 为您处理其他所有事情:
使用 ethtool -g
检查当前网卡队列大小。
$ sudo ethtool -g eth0 Ring parameters for eth0: Pre-set maximums: RX: 4096 RX Mini: 0 RX Jumbo: 0 TX: 4096 Current hardware settings: RX: 512 RX Mini: 0 RX Jumbo: 0 TX: 512
上面的输出指示硬件支持多达 4096 个接收和发送描述符,但是它当前仅使用 512 个。
使用 ethtool -G
增加每个 TX 队列的大小到 4096
$ sudo ethtool -G eth0 tx 4096
注意: 对于大多数驱动程序来说,进行这些更改将关闭接口,然后再重新打开;与该接口的连接将被中断。 不过,这对于一次性的改变来说可能并不重要。
结束
结束了! 现在你已经知道了 Linux 上数据包传输的工作原理:从用户程序到设备驱动程序再返回。
其他
有一些额外的事情值得一提,值得一提的是,似乎不太正确的其他任何地方。
减少 ARP 流量(MSG_CONFIRM
)
send
、sendto
和 sendmsg
系统调用都采用 flags
参数。 如果您传递 MSG_CONFIRM
标志给应用程序中的这些系统调用,它将导致内核中发送路径上的 dst_neigh_output
函数更新邻居结构的时间戳。 这样做的结果是相邻结构将不会被垃圾收集。 这可以防止产生额外的 ARP 流量,因为邻居缓存条目将保持更热、更长时间。
UDP Corking
我们在整个 UDP 协议栈中广泛地研究了 UDP corking。 如果要在应用程序中使用它,可以调用 setsockopt
启用 UDP corking,设置 level 为 IPPROTO_UDP
,optname 设置为 UDP_CORK
,optval
设置为 1
。
时间戳
正如上面的博客文章中提到的,网络栈可以收集传出数据的时间戳。 请参阅上面的网络栈演练,了解软件中的传输时间戳发生的位置。 一些 NIC 甚至还支持硬件中的时间戳。
如果您想尝试确定内核网络栈在发送数据包时增加了多少延迟,这是一个有用的特性。
关于时间戳的内核文档非常好,甚至还有一个包含的示例程序和 Makefile,你可以查看!
使用 ethtool -T
确定您的驱动程序和设备支持的时间戳模式。
$ sudo ethtool -T eth0 Time stamping parameters for eth0: Capabilities: software-transmit (SOF_TIMESTAMPING_TX_SOFTWARE) software-receive (SOF_TIMESTAMPING_RX_SOFTWARE) software-system-clock (SOF_TIMESTAMPING_SOFTWARE) PTP Hardware Clock: none Hardware Transmit Timestamp Modes: none Hardware Receive Filter Modes: none
不幸的是,这个网卡不支持硬件传输时间戳,但是软件时间戳仍然可以在这个系统上使用,以帮助我确定内核给我的数据包传输路径增加了多少延迟。
结论
Linux 网络栈很复杂。
正如我们上面看到的,即使像 NET_RX
这样简单的东西也不能保证像我们期望的那样工作。 即使RX
在名称中,传输完成仍在此 softIRQ 中处理。
这突出了我认为是问题的核心:除非您仔细阅读并理解网络栈的工作原理,否则无法优化和监控网络栈。您无法监控您不深入了解的代码。
原文:Monitoring and Tuning the Linux Networking Stack: Sending Data
本文作者 : cyningsun
本文地址 : https://www.cyningsun.com/04-25-2023/monitoring-and-tuning-the-linux-networking-stack-sent-cn.html
版权声明 :本博客所有文章除特别声明外,均采用 CC BY-NC-ND 3.0 CN 许可协议。转载请注明出处!