NAPI poll
在前面的章节中,我们提到设备驱动程序会为设备分配一块内存区域,用于对传入数据包进行 DMA。正如驱动程序负责分配这些区域一样,它也负责取消映射这些区域,收集数据并发送其到网络栈。
让我们看看 igb
驱动程序是如何做到这一点的,以便了解这在实践中是如何运作的。
igb_poll
最后,我们终于可以探讨我们的朋友 igb_poll
。 igb_poll
看起来很简单。 我们来看看。 来自 drivers/net/ethernet/intel/igb/igb_main.c:
/** * igb_poll - NAPI Rx polling callback * @napi: napi polling structure * @budget: count of how many packets we should handle **/ static int igb_poll(struct napi_struct *napi, int budget) { struct igb_q_vector *q_vector = container_of(napi, struct igb_q_vector, napi); bool clean_complete = true; #ifdef CONFIG_IGB_DCA if (q_vector->adapter->flags & IGB_FLAG_DCA_ENABLED) igb_update_dca(q_vector); #endif /* ... */ 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; /* If not enough Rx work done, exit the polling mode */ napi_complete(napi); igb_ring_irq_enable(q_vector); return 0; }
这段代码做了一些有趣的事情:
- 如果内核中启用了 直接缓存访问 (DCA) 支持,则会预热 CPU 缓存,以便对接收环的访问能够命中 CPU 缓存。您可以在本博客文章末尾的额外部分中了解更多关于 DCA 的信息。
- 接下来,调用
igb_clean_rx_irq
进行繁重的工作,我们接下来会看到。 - 然后,检查
clean_complete
以确定是否还有更多的工作可以完成。如果是这样,返回budget
(记住,这是硬编码为64
的)。正如我们前面看到的,net_rx_action
会移动此 NAPI 结构到轮询列表的末尾。 - 否则,驱动程序调用
napi_complete
关闭 NAPI,并调用igb_ring_irq_enable
重新启用中断。下一个到达的中断将重新启用 NAPI。
让我们看看 igb_clean_rx_irq
如何发送网络数据到栈。
igb_clean_rx_irq
igb_clean_rx_irq
函数是一个循环,每次处理一个数据包,直到用尽 budget
或没有更多数据需要处理为止。
这个函数中的循环做了一些重要的事情:
- 在清理使用过的缓冲区时,为接收数据分配额外的缓冲区。每次添加
IGB_RX_BUFFER_WRITE
(16)个额外的缓冲区。 - 从接收队列中获取一个缓冲区并存储在
skb
结构中。 - 检查缓冲区是否为“数据包结束”缓冲区。如果是,则继续处理。否则,继续从接收队列中获取额外的缓冲区,并添加到
skb
中。如果接收到的数据帧大于缓冲区大小,则需要这样做。 - 验证数据的布局和头部是否正确。
- 已处理字节数统计计数器增加
skb->len
。 - 设置 skb 的哈希、校验和、时间戳、VLAN id 和协议字段。哈希、校验和、时间戳和 VLAN id 由硬件提供。如果硬件发出校验和错误信号,则增加
csum_error
统计量。如果校验和成功且数据为 UDP 或 TCP 数据,则标记skb
为CHECKSUM_UNNECESSARY
。如果校验和失败,则协议栈负责处理此数据包。协议调用eth_type_trans
计算并存储在skb
结构中。 - 调用
napi_gro_receive
传递构建的skb
到网络栈。 - 增加已处理数据包数量统计计数器。
- 循环继续,直到处理的数据包数量达到预算。
循环结束后,函数为接收数据包和已处理字节数分配统计计数器。
在继续上行网络栈之前,现在是时候先兵分两路了。首先,让我们看看如何监控和调整网络子系统的 softirqs。接下来,让我们谈谈通用接收卸载 (GRO)。之后,当我们进入 napi_gro_receive
时,网络栈的其余部分将更有意义。
监控网数据处理
/proc/net/softnet_stat
如前一节所述,在退出 net_rx_action
循环并且可以完成更多工作,但 softirq 的 budget
或时间限制被触发时,net_rx_action
会增加一个统计量。这个统计量作为与 CPU 关联的 struct softnet_data
的一部分进行跟踪。
这些统计数据输出到 proc 中的一个文件:/proc/net/softnet_stat
,不幸的是,关于这个文件的文档非常少。proc 文件中的字段没有标记,并且可能在内核版本之间发生变化。
在 Linux 3.13.0 中,您可以阅读内核源代码来查找哪些值映射到 /proc/net/softnet_stat
中的哪个字段。从 net/core/net-procfs.c:
seq_printf(seq, "%08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x\n", sd->processed, sd->dropped, sd->time_squeeze, 0, 0, 0, 0, 0, /* was fastroute */ sd->cpu_collision, sd->received_rps, flow_limit_count);
这些统计数据中包含许多令人困惑的名称,并且在您可能未预期的地方增加。在探讨网络栈时,将提供每个统计数据何时以及在哪里增加的解释。由于在 net_rx_action
中看到了 squeeze_time
统计量,我认为现在记录这个文件是有意义的。
读取 /proc/net/softnet_stat
监控网络数据处理统计信息。
$ cat /proc/net/softnet_stat 6dcad223 00000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000 6f0e1565 00000000 00000002 00000000 00000000 00000000 00000000 00000000 00000000 00000000 660774ec 00000000 00000003 00000000 00000000 00000000 00000000 00000000 00000000 00000000 61c99331 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 6794b1b3 00000000 00000005 00000000 00000000 00000000 00000000 00000000 00000000 00000000 6488cb92 00000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000
关于 /proc/net/softnet_stat
的重要细节:
- 每一行
/proc/net/softnet_stat
对应一个struct softnet_data
结构,每个 CPU 都有一个。 - 值之间用一个空格分隔,并以十六进制显示
- 第一个值,
sd->processed
,是处理的网络帧数。如果您使用以太网绑定,这可能会超过接收到的网络帧总数。有些情况下,以太网绑定驱动程序会触发网络数据重新处理,同一数据包将使sd->processed
计数增加不止一次。 - 第二个值,
sd->dropped
,是因处理队列没有空间而丢弃的网络帧数。稍后再谈。 - 第三个值,
sd->time_squeeze
,(如我们所见)是net_rx_action
循环因消耗预算或达到时间限制而终止的次数,但仍然可以完成更多工作。如前所述,增加budget
可以帮助减少这种情况。 - 接下来的 5 个值始终为 0。
- 第九个值,
sd->cpu_collision
,是在发送数据包尝试获取设备锁时发生冲突的次数。本文讨论的是接收,因此下面不会看到这个统计量。 - 第十个值,
sd->received_rps
,是唤醒此 CPU 通过 处理器间中断 处理数据包的次数。 - 最后一个值,
flow_limit_count
,是达到流量限制的次数。流量限制是可选的 Receive Packet Steering 功能,稍后会探讨到该特性。
如果您决定监控此文件并绘制结果图表,则必须非常小心这些字段的顺序是否发生了变化,并且每个字段的含义是否得到了保留。您需要阅读内核源代码来验证这一点。
调整网络数据处理
调整 net_rx_action
预算
您可以调整 net_rx_action
预算,设置名为 net.core.netdev_budget
的 sysctl 值来确定注册到 CPU 的所有 NAPI 结构数据包处理可以消耗多少。
示例:设置总体数据包处理预算为 600。
$ sudo sysctl -w net.core.netdev_budget=600
您可能还希望写入此设置到 /etc/sysctl.conf
文件,以便在重启前后保持更改。
Linux 3.13.0上的默认值是 300。
Generic Receive Offloading (GRO)
Generic Receive Offloading (GRO) 是 Large Receive Offloading (LRO) 硬件优化的软件实现。
这两种方法的主要思想是,将“足够相似”的数据包组合在一起,减少传递到网络栈的数据包数量,从而减少 CPU 使用率。例如,想象一种情况,正在进行大文件传输,大多数数据包都包含文件中的数据块。与其一次发送一个小数据包到栈,不如将传入的数据包组合成一个具有巨大有效负载的数据包。然后传递该数据包到栈。这样可以让协议层处理单个数据包的头部,同时传递更大的数据块给用户程序。
当然,这种优化的问题是信息丢失。如果一个数据包设置了某些重要选项或标志,则如果该数据包与另一个数据包合并,则该选项或标志可能会丢失。这正是为什么大多数人不使用或鼓励使用 LRO 的原因。一般来说,对于合并数据包,LRO 实现的规则非常宽松。
GRO 作为 LRO 的软件实现被引入,但对于哪些数据包可以合并有更严格的规则。
顺便说一句:如果您曾经使用过 tcpdump
并看到过不切实际的大型传入数据包大小,那么很可能是因为您的系统启用了 GRO。正如您很快就会看到的那样,在 GRO 已经发生之后,数据包抓取被插入栈中。
调优:使用 ethtool
调整 GRO 设置
您可以使用 ethtool
检查是否启用了 GRO,也可以调整设置。
使用 ethtool -k
检查您的 GRO 设置。
$ ethtool -k eth0 | grep generic-receive-offload generic-receive-offload: on
如您所见,在这个系统上,我设置 generic-receive-offload
为启用。
使用 ethtool -K
启用(或禁用)GRO。
$ sudo ethtool -K eth0 gro on
注意: 对于大多数驱动程序来说,进行这些更改将使接口关闭,然后再将其重新打开;到该接口的连接将被中断。 不过,这对于一次性的改变来说可能并不重要。
napi_gro_receive
函数 napi_gro_receive
处理 GRO 的网络数据(如果系统启用了GRO),并向上发送数据到协议层。 这个逻辑的大部分是在一个名为 dev_gro_receive
的函数中。
dev_gro_receive
这个函数首先检查是否启用了 GRO,如果是,则准备进行 GRO。在启用 GRO 的情况下,遍历 GRO 卸载过滤器列表,以便高层协议栈对正在考虑进行 GRO 的数据进行操作。这样做是为了使得协议层让网络设备层知道此数据包是否属于当前正在接收卸载的 网络流,并处理应该为 GRO 发生的任何协议相关内容。例如,TCP 协议需要决定是否/何时对正在合并到现有数据包中的数据包进行 ACK。
下面是来自 net/core/dev.c
的代码,它执行此操作:
list_for_each_entry_rcu(ptype, head, list) { if (ptype->type != type || !ptype->callbacks.gro_receive) continue; skb_set_network_header(skb, skb_gro_offset(skb)); skb_reset_mac_len(skb); NAPI_GRO_CB(skb)->same_flow = 0; NAPI_GRO_CB(skb)->flush = 0; NAPI_GRO_CB(skb)->free = 0; pp = ptype->callbacks.gro_receive(&napi->gro_list, skb); break; }
如果协议层指示是时候刷新 GRO 的数据包,则接下来进行处理。 这是调用napi_gro_complete
来实现的,它调用协议层的 gro_complete
回调,然后调用 netif_receive_skb
向上传递数据包到网络栈。
下面是 net/core/dev.c
中的代码,它可以做到这一点:
if (pp) { struct sk_buff *nskb = *pp; *pp = nskb->next; nskb->next = NULL; napi_gro_complete(nskb); napi->gro_count--; }
接下来,如果协议层合并此数据包到现有流中,napi_gro_receive
将简单地返回,因为没有其他事情要做。
如果数据包未合并,并且系统上的 GRO 流少 于MAX_GRO_SKBS
(8),则会向该CPU的NAPI结构上的 gro_list
添加一个新条目。
下面是 net/core/dev.c
中的代码,它可以做到这一点:
if (NAPI_GRO_CB(skb)->flush || napi->gro_count >= MAX_GRO_SKBS) goto normal; napi->gro_count++; NAPI_GRO_CB(skb)->count = 1; NAPI_GRO_CB(skb)->age = jiffies; skb_shinfo(skb)->gso_size = skb_gro_len(skb); skb->next = napi->gro_list; napi->gro_list = skb; ret = GRO_HELD;
这就是 Linux 网络栈中 GRO 系统的工作方式。
napi_skb_finish
一旦 dev_gro_receive
执行完毕,就会调用 napi_skb_finish
,它要么释放不需要的数据结构(因为数据包已经被合并),要么调用 netif_receive_skb
向上传递数据到网络栈(因为已经有 MAX_GRO_SKBS
流被 GRO)。
接下来,是时候让 netif_receive_skb
看看数据是如何传递到协议层的了。 在对此进行探讨之前,我们首先需要了解一下 Receive Packet Steering (RPS)。