最后,我们来看看我们的朋友 dev_hard_start_xmit
因此,我们已经遍历了整个网络栈,直到 dev_hard_start_xmit
。 也许你是经 sendmsg
系统调用直接到达这里的,或者你是经 qdisc 上处理网络数据的 softirq 线程到达这里的。dev_hard_start_xmit
将向下调用设备驱动程序来实际执行传输操作。
dev_hard_start_xmit
函数处理两种主要情况:
- 准备发送的网络数据,或
- 具有需要处理的分段卸载的网络数据。
我们将看到这两种情况是如何处理的,从准备发送的网络数据开始。 让我们一起来看看(如下所示:./net/code/dev.c:
int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev, struct netdev_queue *txq) { const struct net_device_ops *ops = dev->netdev_ops; int rc = NETDEV_TX_OK; unsigned int skb_len; if (likely(!skb->next)) { netdev_features_t features; /* * If device doesn't need skb->dst, release it right now while * its hot in this cpu cache */ if (dev->priv_flags & IFF_XMIT_DST_RELEASE) skb_dst_drop(skb); features = netif_skb_features(skb);
这段代码首先 ops
获取设备驱动程序暴露的操作的引用。当需要驱动程序执行一些工作来传输数据时,将使用它。 代码检查 skb->next
以确保此数据不是数据链的一部分,该数据链已分段准备就绪,并继续执行两件事:
- 首先,它检查设备是否设置了
IFF_XMIT_DST_RELEASE
标志。 此内核中的任何“真正的”以太网设备都不使用此标志。 但是,它被环回设备和其他一些软件设备使用。 如果启用此标志,则可以减少目标缓存条目的引用计数,因为驱动程序不需要它。 - 接下来,
netif_skb_features
从设备获取特性标志,并根据数据的目的协议(dev->protocol
)对它们进行一些修改。 例如,如果协议是设备可以校验和的协议,则标记 skb 为这样的协议。 VLAN 标记(如果已设置)也会导致其他功能标志翻转。
接下来,将检查 VLAN 标记,如果设备无法卸载 VLAN 标记,则将在软件中__vlan_put_tag
来执行此操作:
if (vlan_tx_tag_present(skb) && !vlan_hw_offload_capable(features, skb->vlan_proto)) { skb = __vlan_put_tag(skb, skb->vlan_proto, vlan_tx_tag_get(skb)); if (unlikely(!skb)) goto out; skb->vlan_tci = 0; }
接下来,将检查数据是否是封装卸载请求,例如,可能是 GRE。 在这种情况下,更新功能标志,以包括可用的任何特定于设备的硬件封装功能:
/* If encapsulation offload request, verify we are testing * hardware encapsulation features instead of standard * features for the netdev */ if (skb->encapsulation) features &= dev->hw_enc_features;
接下来,netif_needs_gso
来确定 skb 本身是否需要分段。 如果 skb 需要分段,但设备不支持,则 netif_needs_gso
将返回 true
指示分段应在软件中进行。 在本例中,调用dev_gso_segment
来执行分段,代码将跳转到 gso
来传输数据包。 稍后我们将看到 GSO 路径。
if (netif_needs_gso(skb, features)) { if (unlikely(dev_gso_segment(skb, features))) goto out_kfree_skb; if (skb->next) goto gso; }
如果数据不需要分割,则处理一些其他情况。 第一:数据是否需要线性化? 也就是说,如果数据分布在多个缓冲区中,设备是否可以支持发送网络数据,或者是否需要首先组合所有数据到单个线性缓冲区中? 绝大多数网卡不需要在传输之前对数据进行线性化,因此在几乎所有情况下,这将被计算为 false 并跳过。
else { if (skb_needs_linearize(skb, features) && __skb_linearize(skb)) goto out_kfree_skb;
接下来提供了一个有用的注释,解释了下一个分支。 检查数据包以确定它是否仍需要校验和。 如果设备不支持校验和,则在软件中生成校验和:
/* If packet is not checksummed and device does not * support checksumming for this protocol, complete * checksumming here. */ if (skb->ip_summed == CHECKSUM_PARTIAL) { if (skb->encapsulation) skb_set_inner_transport_header(skb, skb_checksum_start_offset(skb)); else skb_set_transport_header(skb, skb_checksum_start_offset(skb)); if (!(features & NETIF_F_ALL_CSUM) && skb_checksum_help(skb)) goto out_kfree_skb; } }
现在我们继续讨论数据包抓取!回想一下,在 接收端博客文章 中,我们看到了如何传递数据包给数据包抓取(例如 PCAP)。此函数中的下一块代码将即将传输的数据包交给数据包抓取(如果有的话)。
if (!list_empty(&ptype_all)) dev_queue_xmit_nit(skb, dev);
最后,驱动程序的 ops
调用 ndo_start_xmit
向下传递数据到设备:
skb_len = skb->len; rc = ops->ndo_start_xmit(skb, dev); trace_net_dev_xmit(skb, rc, dev, skb_len); if (rc == NETDEV_TX_OK) txq_trans_update(txq); return rc; }
返回 ndo_start_xmit
的返回值,指示数据包是否被传输。 我们看到了这个返回值将如何影响上层:由该函数调用方的 QDisc 重新排队数据,以便它可以稍后再次传输。
让我们来看看 GSO 的案例。 如果 skb 已经由于在此函数中发生的分段,而被分离成一个数据包链,或者先前分段但未能发送并排队等待再次发送的数据包,则此代码将运行。
gso: do { struct sk_buff *nskb = skb->next; skb->next = nskb->next; nskb->next = NULL; if (!list_empty(&ptype_all)) dev_queue_xmit_nit(nskb, dev); skb_len = nskb->len; rc = ops->ndo_start_xmit(nskb, dev); trace_net_dev_xmit(nskb, rc, dev, skb_len); if (unlikely(rc != NETDEV_TX_OK)) { if (rc & ~NETDEV_TX_MASK) goto out_kfree_gso_skb; nskb->next = skb->next; skb->next = nskb; return rc; } txq_trans_update(txq); if (unlikely(netif_xmit_stopped(txq) && skb->next)) return NETDEV_TX_BUSY; } while (skb->next);
您可能已经猜到了,这段代码是一个 while 循环,它遍历在数据分段时生成的 skb 列表。
每个数据包:
- 通过数据包抓取(如果有)。
- 通过
ndo_start_xmit
传递给驱动器进行传输。
传输数据包中的任何错误都会调整需要发送的 skb 列表来处理。 错误将返回堆栈,未发送的 skb 可能会被重新排队,以便稍后再次发送。
此函数的最后一部分处理清理,并可能在出现上述错误时释放数据:
out_kfree_gso_skb: if (likely(skb->next == NULL)) { skb->destructor = DEV_GSO_CB(skb)->destructor; consume_skb(skb); return rc; } out_kfree_skb: kfree_skb(skb); out: return rc; } EXPORT_SYMBOL_GPL(dev_hard_start_xmit);
在继续讨论设备驱动程序之前,让我们看一下可以对我们刚刚浏览的代码进行的一些监控和调优。
监控 qdiscs
使用 tc
命令行工具
使用 tc
监控您的 qdisc 统计数据
$ tc -s qdisc show dev eth1 qdisc mq 0: root Sent 31973946891907 bytes 2298757402 pkt (dropped 0, overlimits 0 requeues 1776429) backlog 0b 0p requeues 1776429
为了监控系统的数据包传输状况,检查连接到网络设备的队列规则的统计信息至关重要。 您可以运行命令行工具 tc
来检查状态。 上面的示例显示了如何检查 eth1
接口的统计信息。
bytes
:下推到驱动程序进行传输的字节数。pkt
:下推到驱动程序进行传输的数据包数量。dropped
:qdisc 丢弃的数据包数。 如果传输队列长度不足以容纳排队的数据,则可能发生这种情况。overlimits
:取决于排队规则,但可以是由于达到限制而无法入队的数据包数量,和/或在出队时触发节流事件的数据包数量。requeues
:调用dev_requeue_skb
重新排队 skb 的次数。 请注意,多次重新排队的 skb 将在每次重新排队时增加此计数器。backlog
:当前在 qdisc 队列中的字节数。 这个数字通常在每次数据包入队时增加。
某些 qdics 可能会导出其他统计信息。 每个 qdisc 是不同的,并且可以在不同的时间增加这些计数器。 您可能需要研究您正在使用的 qdisc 的源代码,以准确了解这些值何时可以在您的系统上增加,从而帮助了解对您的影响。
调优 qdiscs
增加 __qdisc_run
您可以调整前面看到 __qdisc_run
循环的权重(上面看到的quota
变量),这将导致执行更多__netif_schedule
的调用。 结果是当前 qdisc 更多次被添加到当前 CPU 的 output_queue
列表中,这应该会导致对传输数据包的额外处理。
示例:使用 sysctl
增加所有 qdisc 的 __qdisc_run
配额。
$ sudo sysctl -w net.core.dev_weight=600
增加传输队列长度
每个网络设备都有一个可以修改的 txqueuelen
调节旋钮。大多数 qdisc 在对最终应由 qdisc 传输的数据排队时,都会检查设备是否具有足够的 txqueuelen
字节。您可以调整此参数以增加 qdisc 可排队的字节数。
示例:增加 eth0
的 txqueuelen
到 10000
。
$ sudo ifconfig eth0 txqueuelen 10000
以太网设备的默认值为 1000
。 您可以读取 ifconfig
的输出来检查网络设备的 txqueuelen
。
网络设备驱动程序
我们的旅程就要结束了。 关于数据包传输有一个重要的概念需要理解。 大多数设备和驱动程序将数据包传输处理分为两步过程:
- 数据被正确地排列,并且触发设备从 RAM DMA 写入数据到网络
- 传输完成后,设备引发中断,以便驱动程序可以取消缓冲区映射、释放内存或以其他方式清除其状态。
第二阶段通常被称为“传输完成”阶段。 我们将研究这两个阶段,但我们将从第一阶段开始:传输阶段。
我们看到 dev_hard_start_xmit
调用了 ndo_start_xmit
(持有锁)来传输数据,所以让我们从检查驱动程序如何注册 ndo_start_xmit
开始,然后我们将深入研究该函数如何工作。
和 上一篇博文一样, 我们将研究 igb
驱动程序。