赞
踩
Table of Contents
__netif_receive_skb_core 将数据传送到数据包抽头和协议层
这篇博客文章解释了运行Linux内核的计算机如何接收数据包,以及如何在数据包从网络流向用户域程序时监视和调整网络堆栈的每个组件。
更新我们已经发布了与之对应的文章:监视和调整Linux网络堆栈:发送数据。
更新请看英文原文《监控和调整Linux网络堆栈:接收数据的插图指南》或中文译文《监控和调整Linux网络协议栈的图解指南:接收数据》,其中为以下信息添加了一些图表。
如果不阅读内核的源代码并且对正在发生的事情有深刻的了解,就不可能调整或监视Linux网络堆栈。
希望该博客文章可以为希望这样做的任何人提供参考。
更新我们已经发布了与之对应的文章:监视和调整Linux网络堆栈:发送数据。
更新请看《监控和调整Linux网络堆栈:接收数据的插图指南》,其中为以下信息添加了一些图表。
网络协议栈很复杂,没有一种适合所有解决方案的规模。如果网络的性能和健康状况对您或您的业务至关重要,那么您将别无选择,只能投入大量的时间,精力和金钱来了解系统各部分之间的交互方式。
理想情况下,您应该考虑测量网络堆栈每一层的丢包率。这样,您可以确定并缩小需要调整的组件。
我认为,这是许多操作员偏离轨道的地方:假定一组sysctl设置或/proc
值可以简单地批量重用。也许在某些情况下,但事实证明,整个系统是如此细微和纠缠在一起,以至于如果您希望进行有意义的监视或调整,则必须努力理解系统的深层功能。否则,您可以简单地使用默认设置,该默认设置应该足够好,直到需要进一步优化(以及进行这些设置所需的投资)为止。
本博客文章中提供的许多示例设置仅用于说明目的,不建议或反对某些配置或默认设置。在调整任何设置之前,您应该围绕需要监视的内容建立参考框架,以注意到有意义的更改。
通过网络连接到机器时调整网络设置很危险;您可以轻松地将自己锁定在外,或者完全断开网络连接。不要在生产机器上调整这些设置;而是在可能的情况下对新机器进行调整,然后将其轮换投入生产。
作为参考,您可能希望随身携带一份器件数据手册。这篇文章将检查由igb
设备驱动程序控制的Intel I350以太网控制器。您可以在此处找到该数据表(警告:大PDF)以供参考。
数据包从到达到达套接字接收缓冲区的高级路径如下:
ksoftirqd
进程在系统上的每个CPU上运行。它们在引导时注册。该ksoftirqd
过程通过调用NAPI拉断的环形缓冲器的数据包poll
,该设备驱动器初始化期间注册的函数。在以下各节中将详细检查整个流程。
下面检查的协议层是IP和UDP协议层。所提供的许多信息也将用作其他协议层的参考。
更新我们已经发布了与之对应的文章:监视和调整Linux网络堆栈:发送数据。
更新请看《监控和调整Linux网络堆栈:接收数据的插图指南》,其中为以下信息添加了一些图表。
这篇博客文章将研究Linux内核版本3.13.0,并在本文中链接到GitHub上的代码和代码段。
确切地了解如何在Linux内核中接收数据包非常重要。我们需要仔细检查并了解网络驱动程序的工作方式,以便以后网络堆栈的各个部分更加清晰。
这篇博客文章将介绍igb
网络驱动程序。该驱动程序用于相对通用的服务器NIC,即英特尔以太网控制器I350。因此,让我们从了解igb
网络驱动程序的工作原理开始。
驱动程序注册一个初始化函数,该函数在加载驱动程序时由内核调用。通过使用module_init
宏注册此功能。
的igb
初始化函数(igb_init_module
)及其与登记module_init
中可以找到的驱动/净/以太网/英特尔/ IGB / igb_main.c。
两者都很简单:
- /**
- * igb_init_module - Driver Registration Routine
- *
- * igb_init_module is the first routine called when the driver is
- * loaded. All it does is register with the PCI subsystem.
- **/
- static int __init igb_init_module(void)
- {
- int ret;
- pr_info("%s - version %s\n", igb_driver_string, igb_driver_version);
- pr_info("%s\n", igb_copyright);
-
- /* ... */
-
- ret = pci_register_driver(&igb_driver);
- return ret;
- }
-
- module_init(igb_init_module);

初始化设备的大部分工作都是通过调用来完成的pci_register_driver
,我们将在后面看到。
PCI初始化
英特尔I350网卡是PCI Express设备。
PCI设备通过PCI配置空间中的一系列寄存器来标识自己。
编译设备驱动程序后,将使用名为MODULE_DEVICE_TABLE
(from include/module.h
)的宏来导出PCI设备ID表,以标识该设备驱动程序可以控制的设备。该表也被注册为结构的一部分,稍后我们将看到。
内核使用此表来确定要加载哪个设备驱动程序来控制设备。
这样操作系统可以确定哪些设备已连接到系统以及应该使用哪个驱动程序与该设备进行通讯。
igb
可以在drivers/net/ethernet/intel/igb/igb_main.c
和drivers/net/ethernet/intel/igb/e1000_hw.h
中分别找到此表和驱动程序的PCI设备ID :
- static DEFINE_PCI_DEVICE_TABLE(igb_pci_tbl) = {
- { PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_BACKPLANE_1GBPS) },
- { PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_SGMII) },
- { PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_BACKPLANE_2_5GBPS) },
- { PCI_VDEVICE(INTEL, E1000_DEV_ID_I211_COPPER), board_82575 },
- { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_COPPER), board_82575 },
- { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_FIBER), board_82575 },
- { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SERDES), board_82575 },
- { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SGMII), board_82575 },
- { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_COPPER_FLASHLESS), board_82575 },
- { PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SERDES_FLASHLESS), board_82575 },
-
- /* ... */
- };
- MODULE_DEVICE_TABLE(pci, igb_pci_tbl);
如上一节所述,pci_register_driver
在驱动程序的初始化函数中被调用。
该函数注册指针的结构。大多数指针是功能指针,但PCI设备ID表也已注册。内核使用驱动程序注册的功能启动PCI设备。
来自drivers/net/ethernet/intel/igb/igb_main.c
:
- static struct pci_driver igb_driver = {
- .name = igb_driver_name,
- .id_table = igb_pci_tbl,
- .probe = igb_probe,
- .remove = igb_remove,
-
- /* ... */
- };
PCI探针
通过设备的PCI ID识别设备后,内核即可选择合适的驱动程序来控制设备。每个PCI驱动程序在内核中的PCI系统中注册一个探测功能。内核为尚未被设备驱动程序要求保护的设备调用此函数。索取设备版权后,将不会再向其他驱动程序询问该设备。大多数驱动程序都有很多代码,可以运行以使设备准备就绪。实际执行的操作因驱动程序而异。
要执行的一些典型操作包括:
struct net_device_ops
。该结构包含指向打开设备,将数据发送到网络,设置MAC地址等所需的各种功能的功能指针。struct net_device
代表网络设备的高层的创建,初始化和注册。让我们快速看一下igb
该函数中驱动程序中的一些操作igb_probe
。
窥视PCI初始化
该igb_probe
函数的以下代码执行一些基本的PCI配置。从driver / net / ethernet / intel / igb / igb_main.c:
- err = pci_enable_device_mem(pdev);
-
- /* ... */
-
- err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
-
- /* ... */
-
- err = pci_request_selected_regions(pdev, pci_select_bars(pdev,
- IORESOURCE_MEM),
- igb_driver_name);
-
- pci_enable_pcie_error_reporting(pdev);
-
- pci_set_master(pdev);
- pci_save_state(pdev);

首先,使用初始化设备pci_enable_device_mem
。如果设备被挂起,这将唤醒设备,启用内存资源等等。
接下来,将设置DMA掩码。该设备可以读写64位内存地址,因此dma_set_mask_and_coherent
称为DMA_BIT_MASK(64)
。
将通过调用保留存储区域pci_request_selected_regions
,启用PCI Express高级错误报告(如果已加载PCI AER驱动程序),通过调用启用DMA,通过调用来pci_set_master
保存PCI配置空间pci_save_state
。
ew
更多Linux PCI驱动程序信息
关于PCI设备如何工作的完整解释不在本文的讨论范围之内,但是Linux内核中的精彩演讲,Wiki和文本文件都是出色的资源。
该igb_probe
功能执行一些重要的网络设备初始化。除了PCI特定的工作之外,它还将执行更多的常规联网和网络设备工作:
struct net_device_ops
注册。ethtool
操作已注册。net_device
功能标志已设置。让我们看一下其中的每一个,因为稍后它们会很有趣。
struct net_device_ops
struct net_device_ops
包含函数指针,这些指针指向网络子系统控制设备所需的许多重要操作。在本文的其余部分中,我们将多次提及此结构。
此net_device_ops
结构附加到struct net_device
in中igb_probe
。从driver / net / ethernet / intel / igb / igb_main.c)
- static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
- {
- /* ... */
-
- netdev->netdev_ops = &igb_netdev_ops;
并且此net_device_ops
结构保存指针的功能在同一文件中设置。从driver / net / ethernet / intel / igb / igb_main.c:
- static const struct net_device_ops igb_netdev_ops = {
- .ndo_open = igb_open,
- .ndo_stop = igb_close,
- .ndo_start_xmit = igb_xmit_frame,
- .ndo_get_stats64 = igb_get_stats64,
- .ndo_set_rx_mode = igb_set_rx_mode,
- .ndo_set_mac_address = igb_set_mac,
- .ndo_change_mtu = igb_change_mtu,
- .ndo_do_ioctl = igb_ioctl,
-
- /* ... */
正如你所看到的,有这几个有趣的领域struct
一样ndo_open
,ndo_stop
,ndo_start_xmit
,和ndo_get_stats64
其持有的由执行函数的地址igb
的驱动程序。
稍后我们将更详细地研究其中一些。
ethtool
注册
ethtool
是一个命令行程序,可用于获取和设置各种驱动程序和硬件选项。您可以通过运行在Ubuntu上安装它apt-get install ethtool
。
的常见用法ethtool
是从网络设备收集详细的统计信息。ethtool
稍后将描述其他感兴趣的设置。
该ethtool
程序通过使用ioctl
系统调用与设备驱动程序对话。设备驱动程序注册了一系列为这些ethtool
操作运行的功能,而内核则提供了粘合剂。
当ioctl
从进行调用时ethtool
,内核会找到ethtool
适当的驱动程序注册的结构并执行注册的功能。驱动程序的ethtool
功能实现可以执行任何操作,从更改驱动程序中的简单软件标志到通过将寄存器值写入设备来调整实际NIC硬件的工作方式。
该igb
驱动程序注册其ethtool
在运营igb_probe
致电igb_set_ethtool_ops
:
- static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
- {
- /* ... */
-
- igb_set_ethtool_ops(netdev);
可以在文件中找到所有igb
驱动程序ethtool
代码drivers/net/ethernet/intel/igb/igb_ethtool.c
以及igb_set_ethtool_ops
函数。
来自drivers/net/ethernet/intel/igb/igb_ethtool.c
:
- void igb_set_ethtool_ops(struct net_device *netdev)
- {
- SET_ETHTOOL_OPS(netdev, &igb_ethtool_ops);
- }
除此之外,您可以找到igb_ethtool_ops
具有驱动程序支持的ethtool
功能的结构,并将其igb
设置为适当的字段。
来自drivers/net/ethernet/intel/igb/igb_ethtool.c
:
- static const struct ethtool_ops igb_ethtool_ops = {
- .get_settings = igb_get_settings,
- .set_settings = igb_set_settings,
- .get_drvinfo = igb_get_drvinfo,
- .get_regs_len = igb_get_regs_len,
- .get_regs = igb_get_regs,
- /* ... */
由各个驱动程序确定哪些ethtool
功能相关以及应该执行哪些功能。ethtool
不幸的是,并非所有驱动程序都能实现所有功能。
一个有趣的ethtool
功能是get_ethtool_stats
,(如果实现),该功能可以生成详细的统计信息计数器,该计数器可以在驱动程序中的软件中或通过设备本身进行跟踪。
下面的监视部分将显示如何使用ethtool
来访问这些详细的统计信息。
问卷
当数据帧通过DMA写入RAM时,NIC如何告知系统其余部分已准备好处理数据?
传统上,NIC将生成一个中断请求(IRQ),指示数据已到达。共有三种常见的IRQ:MSI-X,MSI和旧式IRQ。这些将在短期内涉及。当数据已经通过DMA写入RAM时,生成IRQ的设备非常简单,但是,如果到达大量数据帧,则可能导致生成大量IRQ。生成的IRQ越多,可用于更高级别任务(如用户进程)的CPU时间就越少。
的新的API(NAPI)是作为用于减少由上包到达网络设备生成的IRQ的数目的机制创建的。尽管NAPI减少了IRQ的数量,但它无法完全消除它们。
我们将在后面的部分中确切地说明为什么会这样。
NAPI
NAPI在几个重要方面不同于传统的数据收集方法。NAPI允许设备驱动程序注册poll
NAPI子系统将调用以收集数据帧的功能。
NAPI在网络设备驱动程序中的预期用途如下:
poll
在单独的执行线程中调用驱动程序的注册函数来开始收集数据包。与传统方法相比,这种收集数据帧的方法减少了开销,因为一次可以消耗许多数据帧,而不必一次处理一个IRQ。
设备驱动程序实现一个poll
功能,并通过调用将其注册到NAPI netif_napi_add
。向其注册NAPI poll
函数时netif_napi_add
,驱动程序还将指定weight
。大多数驱动程序将值硬编码为64
。该值及其含义将在下面更详细地描述。
通常,驱动程序poll
在驱动程序初始化期间注册其NAPI 函数。
igb
驱动程序中的NAPI初始化
该igb
驱动器通过一个长调用链做到这一点:
igb_probe
来电igb_sw_init
。igb_sw_init
来电igb_init_interrupt_scheme
。igb_init_interrupt_scheme
来电igb_alloc_q_vectors
。igb_alloc_q_vectors
来电igb_alloc_q_vector
。igb_alloc_q_vector
来电netif_napi_add
。此调用跟踪导致发生一些高级事件:
pci_enable_msix
。igb_alloc_q_vector
将为每个将创建的发送和接收队列调用一次。igb_alloc_q_vector
调用netif_napi_add
该poll
函数的实例struct napi_struct
将poll
在调用以获取数据包时传递给该函数。让我们看一下igb_alloc_q_vector
如何poll
注册回调及其私有数据。
从driver / net / ethernet / intel / igb / igb_main.c:
- static int igb_alloc_q_vector(struct igb_adapter *adapter,
- int v_count, int v_idx,
- int txr_count, int txr_idx,
- int rxr_count, int rxr_idx)
- {
- /* ... */
-
- /* allocate q_vector and rings */
- q_vector = kzalloc(size, GFP_KERNEL);
- if (!q_vector)
- return -ENOMEM;
-
- /* initialize NAPI */
- netif_napi_add(adapter->netdev, &q_vector->napi, igb_poll, 64);
-
- /* ... */

上面的代码是用于接收队列并igb_poll
在NAPI子系统中注册功能的分配内存。它提供了struct napi_struct
与此新创建的RX队列相关联的参考(&q_vector->napi
上方)。igb_poll
当需要从此RX队列中收集数据包时,将由NAPI子系统调用时将其传递给该数据包。
稍后当我们检查来自网络堆栈上的驱动程序的数据流时,这一点将很重要。
回想一下net_device_ops
我们之前看到的结构,该结构注册了一组功能,用于启动网络设备,传输数据包,设置MAC地址等。
当网络设备启动时(例如,使用ifconfig eth0 up
),将调用附加到结构ndo_open
字段的函数net_device_ops
。
该ndo_open
函数通常会执行以下操作:
对于igb
驱动程序,称为结构ndo_open
域的函数net_device_ops
称为igb_open
。
准备从网络接收数据
您今天将发现的大多数NIC将使用DMA将数据直接写入RAM,OS可以在其中检索数据进行处理。大多数NIC为此目的使用的数据结构类似于建立在循环缓冲区(或环形缓冲区)上的队列。
为此,设备驱动程序必须与OS一起使用,以保留NIC硬件可以使用的内存区域。一旦保留了该区域,就将其位置通知硬件,并将传入的数据写入RAM,然后由网络子系统对其进行提取和处理。
这似乎很简单,但是如果数据包速率足够高而单个CPU无法正确处理所有传入数据包,该怎么办?数据结构建立在内存的固定长度区域上,因此传入的数据包将被丢弃。
这就是所谓的接收方缩放(RSS)或多队列可以提供帮助的地方。
某些设备能够同时将传入的数据包写入RAM的多个不同区域。每个区域是一个单独的队列。从硬件级别开始,这允许OS使用多个CPU并行处理传入的数据。并非所有NIC都支持此功能。
英特尔I350 NIC确实支持多个队列。我们可以在igb
驱动程序中看到这一点的证据。igb
驱动程序启动时要做的第一件事就是调用名为的函数igb_setup_all_rx_resources
。此函数igb_setup_rx_resources
为每个RX队列调用一次函数,以安排设备可以在其中写入传入数据的DMA内存。
如果您想知道这是如何工作的,请参阅Linux内核的DMA API HOWTO。
事实证明,可以使用来调整RX队列的数量和大小ethtool
。调整这些值可能会对处理的帧数和丢弃的帧数产生显着影响。
NIC在数据包头字段(例如源,目标,端口等)上使用哈希函数来确定数据应定向到哪个RX队列。
某些NIC可让您调整RX队列的权重,因此您可以向特定队列发送更多流量。
更少的NIC使您可以调整此哈希函数本身。如果可以调整哈希功能,则可以根据需要将某些流发送到特定的RX队列进行处理,甚至可以在硬件级别丢弃数据包。
我们将很快看一下如何调整这些设置。
启用NAPI
当网络设备启动时,驱动程序通常会启用NAPI。
前面我们已经看到了驱动程序如何poll
向NAPI 注册功能,但是通常在设备启动后才启用NAPI。
启用NAPI相对简单。到的调用napi_enable
将在中翻转一点struct napi_struct
以表明它已被启用。如上所述,当启用NAPI时,它将处于关闭位置。
对于igb
驱动程序,q_vector
在加载驱动程序或使用更改队列计数或大小时,将为每个初始化的NAPI启用ethtool
。
从driver / net / ethernet / intel / igb / igb_main.c:
- for (i = 0; i < adapter->num_q_vectors; i++)
- napi_enable(&(adapter->q_vector[i]->napi));
注册一个中断处理程序
启用NAPI后,下一步是注册中断处理程序。设备可以使用不同的方法来发出中断信号:MSI-X,MSI和传统中断。因此,代码因设备而异,具体取决于特定硬件所支持的中断方法是什么。
驱动程序必须确定设备支持哪种方法,并注册将在接收到中断时执行的适当处理程序函数。
某些驱动程序(如igb
驱动程序)将尝试为每个方法注册一个中断处理程序,并在失败时退回到下一个未经测试的方法。
MSI-X中断是首选方法,特别是对于支持多个RX队列的NIC。这是因为每个RX队列可以分配自己的硬件中断,然后可以由特定的CPU处理(通过irqbalance
或通过修改/proc/irq/IRQ_NUMBER/smp_affinity
)。稍后我们将看到,处理中断的CPU将是处理数据包的CPU。这样,到达的数据包可以由单独的CPU从硬件中断级别通过网络堆栈进行处理。
如果MSI-X不可用,则MSI仍然具有优于传统中断的优点,并且如果设备支持,驱动程序将使用它。阅读此有用的Wiki页面,以获取有关MSI和MSI-X的更多信息。
在igb
驱动器,则各功能igb_msix_ring
,igb_intr_msi
,igb_intr
是用于MSI-X,MSI,和传统中断模式的中断处理程序的方法,分别。
您可以在驱动程序/net/ethernet/intel/igb/igb_main.c中找到尝试每种中断方法的驱动程序中的代码:
- static int igb_request_irq(struct igb_adapter *adapter)
- {
- struct net_device *netdev = adapter->netdev;
- struct pci_dev *pdev = adapter->pdev;
- int err = 0;
-
- if (adapter->msix_entries) {
- err = igb_request_msix(adapter);
- if (!err)
- goto request_done;
- /* fall back to MSI */
-
- /* ... */
- }
-
- /* ... */
-
- if (adapter->flags & IGB_FLAG_HAS_MSI) {
- err = request_irq(pdev->irq, igb_intr_msi, 0,
- netdev->name, adapter);
- if (!err)
- goto request_done;
-
- /* fall back to legacy interrupts */
-
- /* ... */
- }
-
- err = request_irq(pdev->irq, igb_intr, IRQF_SHARED,
- netdev->name, adapter);
-
- if (err)
- dev_err(&pdev->dev, "Error %d getting interrupt\n", err);
-
- request_done:
- return err;
- }

如您在上面的缩写代码中所看到的,驱动程序首先尝试使用设置MSI-X中断处理程序igb_request_msix
,并在失败时返回到MSI。接下来,request_irq
用于注册igb_intr_msi
MSI中断处理程序。如果失败,驱动程序将退回传统中断。request_irq
再次用于注册旧式中断处理程序igb_intr
。
这就是igb
驱动程序注册功能的方式,该功能将在NIC发出一个中断信号来指示数据已到达并准备进行处理时执行。
启用中断
至此,几乎所有东西都准备好了。剩下的唯一一件事就是启用来自NIC的中断并等待数据到达。启用中断是特定igb
于硬件的,但是驱动程序__igb_open
通过调用名为的辅助函数来实现igb_irq_enable
。
通过写入寄存器为该设备启用中断:
- static void igb_irq_enable(struct igb_adapter *adapter)
- {
-
- /* ... */
-
- wr32(E1000_IMS, IMS_ENABLE_MASK | E1000_IMS_DRSTA);
- wr32(E1000_IAM, IMS_ENABLE_MASK | E1000_IMS_DRSTA);
-
- /* ... */
- }
网络设备现在已启动
驱动程序可能还会执行其他一些操作,例如启动计时器,工作队列或其他特定于硬件的设置。一旦完成。网络设备已启动并可以使用。
让我们看一下监视和调整网络设备驱动程序的设置。
有几种不同的方法来监视网络设备,以提供不同级别的粒度和复杂性。让我们从最细粒度开始,然后移到最小粒度。
使用 ethtool -S
您可以安装ethtool
运行的Ubuntu系统上:sudo apt-get install ethtool
。
安装后,您可以通过传递-S
标志以及要进行统计的网络设备的名称来访问统计信息。
使用`ethtool -S`监视详细的NIC设备统计信息(例如,数据包丢弃)。
- $ sudo ethtool -S eth0
- NIC统计信息:
- rx_packets:597028087
- tx_packets:5924278060
- rx_bytes:112643393747
- tx_bytes:990080156714
- rx_broadcast:96
- tx_broadcast:116
- rx_multicast:20294528
- ....
监视此数据可能很困难。它很容易获得,但是没有字段值的标准化。不同的驱动程序,甚至同一驱动程序的不同版本,可能会产生具有相同含义的不同字段名称。
您应该在标签中查找带有“ drop”,“ buffer”,“ miss”等的值。接下来,您将必须阅读驱动程序源。您将能够确定哪些值完全由软件计算(例如,当没有内存时增加),以及哪些值直接通过寄存器读取来自硬件。如果是寄存器值,则应查阅硬件的数据手册,以确定计数器的真正含义。通过给出的许多标签ethtool
可能会产生误导。
使用sysfs
sysfs还提供了许多统计信息值,但是它们比提供的直接NIC级别统计信息略高一些。
您可以通过cat
在文件上使用来找到丢失的传入网络数据帧的数量,例如eth0 。
使用sysfs监视更高级别的NIC统计信息。
- $ cat / sys / class / net / eth0 / statistics / rx_dropped
- 2
计数器值将被拆分后的文件一样collisions
,rx_dropped
,rx_errors
,rx_missed_errors
,等。
不幸的是,由驱动程序决定每个字段的含义,从而决定何时递增它们以及值从何而来。您可能会注意到,某些驱动程序将某种错误情况视为掉线,而其他驱动程序可能将其视为未命中。
如果这些值对您很重要,则需要阅读驱动程序源,以准确了解驱动程序对每个值的含义。
使用 /proc/net/dev
甚至更高级别的文件还/proc/net/dev
为系统上的每个网络适配器提供了高级摘要式信息。
通过阅读监视高级NIC统计信息/proc/net/dev
。
- $ cat / proc / net / dev
- 间| 接收| 发送
- 面|字节数据包错误掉落fifo帧压缩多播|字节数据包错误掉落fifo colls载波压缩
- 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文件中找到的值的子集,但它可以用作有用的常规参考。
上面提到的警告同样适用于此:如果这些值对您很重要,您仍然需要阅读驱动程序源,以准确了解它们在何时,何地以及为何递增,以确保您理解错误,掉落或fifo与您的驱动程序相同。
检查正在使用的RX队列的数量
如果网卡和系统上加载的设备驱动程序支持RSS /多队列,通常可以使用来调整RX队列(也称为RX通道)的数量ethtool
。
使用以下命令检查NIC接收队列的数量 ethtool
- $ sudo ethtool -l eth0
- eth0的通道参数:
- 预设最大值:
- 接收:0
- TX:0
- 其他:0
- 合计:8
- 当前的硬件设置:
- 接收:0
- TX:0
- 其他:0
- 合计:4
此输出显示预设的最大值(由驱动程序和硬件强制执行)和当前设置。
注意:并非所有设备驱动程序都支持此操作。
如果您的NIC不支持此操作,则会显示错误。
- $ sudo ethtool -l eth0
- eth0的通道参数:
- 无法获取设备通道参数
- :不支持操作
这意味着您的驱动程序尚未实现ethtool get_channels
操作。这可能是因为NIC不支持调整队列数,不支持RSS /多队列,或者您的驱动程序尚未更新以处理此功能。
调整接收队列的数量
找到当前和最大队列数后,您可以使用来调整值sudo ethtool -L
。
注意:某些设备及其驱动程序仅支持为发送和接收而配对的组合队列,如上节中的示例所示。
将合并的NIC发送和接收队列设置为8 ethtool -L
$ sudo ethtool -L eth0合并8
如果您的设备和驱动程序支持RX和TX的各个设置,并且您只想将RX队列数更改为8,则可以运行:
使用设置NIC接收队列的数量为8 ethtool -L
。
$ sudo ethtool -L eth0 rx 8
注意:对于大多数驱动程序,进行这些更改将关闭接口,然后将其恢复;与此接口的连接将中断。但是,对于一次更改而言,这可能无关紧要。
调整接收队列的大小
某些NIC及其驱动程序还支持调整RX队列的大小。确切的工作方式是特定于硬件的,但是幸运的是ethtool
为用户提供了一种调整大小的通用方法。增加RX队列的大小有助于防止在收到大量数据帧的期间在NIC上网络数据丢失。但是,数据仍可能会丢失到软件中,并且需要进行其他调整才能完全减少或消除丢失。
使用以下命令检查当前的NIC队列大小 ethtool -g
- $ sudo ethtool -g eth0
- eth0的环参数:
- 预设最大值:
- 接收:4096
- 迷你接收:0
- 接收超大:0
- TX:4096
- 当前的硬件设置:
- 接收:512
- 迷你接收:0
- 接收超大:0
- TX:512
以上输出表明该硬件最多支持4096个接收和发送描述符,但当前仅使用512个。
使用以下命令将每个RX队列的大小增加到4096 ethtool -G
$ sudo ethtool -G eth0接收4096
注意:对于大多数驱动程序,进行这些更改将关闭接口,然后将其恢复;与此接口的连接将中断。但是,对于一次更改而言,这可能无关紧要。
调整接收队列的处理权重
某些NIC支持通过设置权重来调整RX队列之间的网络数据分布的功能。
您可以在以下情况下进行配置:
ethtool
功能get_rxfh_indir_size
和get_rxfh_indir
。ethtool
该版本支持命令行选项-x
并-X
分别显示和设置间接表。使用以下命令检查RX流间接表 ethtool -x
- $ sudo ethtool -x eth0
- 具有2个RX环的eth3的RX流哈希间接表:
- 0:0 1 0 1 0 1 0 1
- 8:0 1 0 1 0 1 0 1
- 16:0 1 0 1 0 1 0 1
- 24:0 1 0 1 0 1 0 1
此输出在左侧显示数据包哈希值,列出了接收队列0和1。因此,散列为2的数据包将被传递到接收队列0,而散列为3的数据包将被传递到接收队列1。
示例:在前两个RX队列之间平均分配处理
$ sudo ethtool -X eth0等于2
如果要设置自定义权重来更改到达某些接收队列(从而影响CPU)的数据包数量,也可以在命令行上指定:
设置自定义RX队列权重 ethtool -X
$ sudo ethtool -X eth0重量6 2
上面的命令将rx队列0的权重指定为6,将rx队列1的权重指定为2,将要在队列0上处理的数据更多。
某些NIC也可以让您调整哈希算法中使用的字段,如下所示。
调整网络流量的rx哈希字段
您可以ethtool
用来调整在计算用于RSS的哈希值时将使用的字段。
使用来检查哪些字段用于UDP RX流哈希ethtool -n
。
- $ sudo ethtool -n eth0 rx-flow-hash udp4
- UDP over IPV4流使用以下字段来计算哈希流密钥:
- IP安全联盟
- IP DA
对于eth0,用于在UDP流上计算哈希的字段是IPv4源地址和目标地址。让我们包括源端口和目标端口:
使用设置UDP RX流哈希字段ethtool -N
。
$ sudo ethtool -N eth0 rx-flow-hash udp4 sdfn
该sdfn
字符串有点神秘;检查ethtool
手册页以获取每个字母的解释。
调整字段以进行哈希处理很有用,但是ntuple
过滤对于更精细的控制(哪个流将由哪个RX队列处理)更为有用。
用于引导网络流的元组过滤
某些NIC支持称为“ ntuple过滤”的功能。该功能允许用户指定(通过ethtool
)一组参数,用于过滤硬件中的传入网络数据并将其排队到特定的RX队列中。例如,用户可以指定将发往特定端口的TCP数据包发送到RX队列1。
在Intel NIC上,此功能通常称为Intel Ethernet Flow Director。其他NIC供应商可能为此功能使用其他市场名称。
稍后我们将看到,ntuple过滤是另一种称为加速接收流控制(aRFS)的功能的关键组成部分,如果您的NIC支持,ntuple的使用将变得更加容易。稍后将介绍aRFS。
如果系统的操作要求包括最大化数据局部性,并希望在处理网络数据时提高CPU缓存命中率,则此功能很有用。例如,考虑在端口80上运行的Web服务器的以下配置:
如前所述,可以使用来配置ntuple过滤ethtool
,但是首先,您需要确保在设备上启用了此功能。
检查是否启用了ntuple过滤器 ethtool -k
- $ sudo ethtool -k eth0
- eth0的卸载参数:
- ...
- ntuple-filters:关闭
- 接收哈希:on
如您所见,ntuple-filters
此设备上的设置为关闭。
使用启用ntuple过滤器 ethtool -K
$ sudo ethtool -K eth0 ntuple在
启用ntuple过滤器或验证其已启用后,可以使用以下命令检查现有的ntuple规则ethtool
:
使用以下命令检查现有的ntuple过滤器 ethtool -u
- $ sudo ethtool -u eth0
- 提供40个RX环
- 共有0条规则
如您所见,该设备没有ntuple过滤规则。您可以通过在命令行上指定规则来添加规则ethtool
。让我们添加一条规则,以将目标端口为80的所有TCP通信定向到RX队列2:
添加ntuple过滤器以将具有目标端口80的TCP流发送到RX队列2
$ sudo ethtool -U eth0流类型tcp4 dst-port 80操作2
您还可以使用ntuple过滤在硬件级别丢弃特定流的数据包。这对于减轻来自特定IP地址的大量传入流量很有用。有关配置ntuple过滤器规则的更多信息,请参见ethtool
手册页。
通常,您可以通过检查从中输出的值来获取有关ntuple规则成功(或失败)的统计信息ethtool -S [device name]
。例如,在Intel NIC上,统计fdir_match
并fdir_miss
计算ntuple过滤规则的匹配和未命中次数。请查阅设备驱动程序源和设备数据表,以跟踪统计信息计数器(如果有)。
在检查网络堆栈之前,我们需要走一小段弯路,以检查Linux内核中称为SoftIRQ的内容。
Linux内核中的softirq系统是一种用于在驱动程序中实现的中断处理程序的上下文之外执行代码的机制。该系统很重要,因为在执行中断处理程序的全部或部分过程中可能会禁用硬件中断。禁用的中断时间越长,事件丢失的机会就越大。因此,重要的是将所有长时间运行的操作推迟到中断处理程序之外,以便它可以尽快完成并重新启用设备中断。
还有其他机制可用于延迟内核中的工作,但是出于网络堆栈的目的,我们将研究softirqs。
可以将softirq系统想象为一系列内核线程(每个CPU一个),这些线程运行已为不同softirq事件注册的处理函数。如果您曾经查看过顶部并且ksoftirqd/0
在内核线程列表中看到过,那么您正在查看的是运行在CPU 0上的softirq内核线程。
内核子系统(例如网络)可以通过执行open_softirq
功能来注册softirq处理程序。稍后我们将看到网络系统如何注册其softirq处理程序。现在,让我们进一步了解softirq的工作方式。
ksoftirqd
由于softirq对于推迟设备驱动程序的工作非常重要,因此您可能会想像该ksoftirqd
进程是在内核生命周期的很早就产生的,这是正确的。
查看kernel / softirq.c中找到的代码,可以了解如何ksoftirqd
初始化系统:
- static struct smp_hotplug_thread softirq_threads = {
- .store = &ksoftirqd,
- .thread_should_run = ksoftirqd_should_run,
- .thread_fn = run_ksoftirqd,
- .thread_comm = "ksoftirqd/%u",
- };
-
- static __init int spawn_ksoftirqd(void)
- {
- register_cpu_notifier(&cpu_nfb);
-
- BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
-
- return 0;
- }
- early_initcall(spawn_ksoftirqd);

从struct smp_hotplug_thread
上面的定义可以看到,有两个函数指针正在注册:ksoftirqd_should_run
和run_ksoftirqd
。
这两个函数都从kernel / smpboot.c调用,作为类似于事件循环的一部分。
将执行kernel/smpboot.c
第一次调用中的代码,该代码ksoftirqd_should_run
确定是否有任何待处理的softirq,以及是否有待处理的softirq run_ksoftirqd
。在run_ksoftirqd
调用之前做了一些小的簿记__do_softirq
。
__do_softirq
该__do_softirq
函数做了一些有趣的事情:
open_softirq
)的softirq处理程序。因此,当您查看CPU使用率图并看到softirq
或si
现在知道这正在衡量延迟工作上下文中发生的CPU使用量时。
/proc/softirqs
该softirq
系统的增量统计计数器可以被读取/proc/softirqs
监控这些统计数据可以给你在哪个正在生成各种活动软中断的速度感。
通过阅读检查softIRQ统计信息/proc/softirqs
。
- $ cat / proc / softirqs
- CPU0 CPU1 CPU2 CPU3
- HI:0 0 0 0
- 计时器:2831512516 1337085411 1103326083 1423923272
- NET_TX:15774435 779806 733217 749512
- NET_RX:1671622615 1257853535 2088429526 2674732223
- 区块:1800253852 1466177 1791366 634534
- BLOCK_IOPOLL:0 0 0 0
- 任务:25 0 0 0
- SCHED:2642378225 1711756029 629040543 682215771
- HRTIMER:2547911 2046898 1558136 1521176
- RCU:2056528783 4231862865 3545088730 844379888
该文件可以使您了解当前网络如何在NET_RX
各个CPU上分配()处理。如果分布不均,则某些CPU的计数值会比其他CPU大。这是一个指标,您可能可以从下面描述的接收数据包控制/接收流控制中受益。监视性能时,请仅使用此文件:在网络活动频繁的时期,您会期望速率NET_RX
增加,但这并非一定如此。事实证明,这有点微妙,因为网络堆栈中还有其他调整旋钮,这些旋钮可能会影响NET_RX
softirq触发的速率,我们将很快看到。
但是,您应该意识到这一点,这样,如果您调整其他调音旋钮,便会知道要检查/proc/softirqs
并期望看到变化。
现在,让我们继续到网络堆栈,并跟踪从上到下如何接收网络数据。
现在,我们已经了解了网络驱动程序和softirq的工作方式,让我们看看如何初始化Linux网络设备子系统。然后,我们可以按照数据包到达的路径进行跟踪。
网络设备(netdev)子系统在函数中初始化net_dev_init
。在此初始化函数中发生了很多有趣的事情。
struct softnet_data
结构初始化
net_dev_init
struct softnet_data
为系统上的每个CPU 创建一组结构。这些结构将保存一些指向处理网络数据的重要信息的指针:
在我们逐步研究堆栈时,将更详细地检查每一个。
初始化softirq处理程序
net_dev_init
注册一个发送和接收softirq处理程序,该处理程序将用于处理传入或传出的网络数据。此代码非常简单:
- static int __init net_dev_init(void)
- {
- /* ... */
-
- open_softirq(NET_TX_SOFTIRQ, net_tx_action);
- open_softirq(NET_RX_SOFTIRQ, net_rx_action);
-
- /* ... */
- }
我们将很快看到驱动程序的中断处理程序如何“提高”(或触发)net_rx_action
注册到NET_RX_SOFTIRQ
softirq 的函数。
终于 网络数据到来!
假设RX队列具有足够的可用描述符,则将数据包通过DMA写入RAM。然后,设备引发分配给它的中断(对于MSI-X,该中断与数据包到达的rx队列相关)。
中断处理程序
通常,在引发中断时运行的中断处理程序应尝试延迟尽可能多的处理,以在中断上下文之外进行。这是至关重要的,因为在处理中断时,其他中断可能会被阻止。
让我们看一下MSI-X中断处理程序的源代码。它确实有助于说明中断处理程序所做的工作尽可能少的想法。
从driver / net / ethernet / intel / igb / igb_main.c:
- static irqreturn_t igb_msix_ring(int irq, void *data)
- {
- struct igb_q_vector *q_vector = data;
-
- /* Write the ITR value calculated from the previous interrupt. */
- igb_write_itr(q_vector);
-
- napi_schedule(&q_vector->napi);
-
- return IRQ_HANDLED;
- }
该中断处理程序非常短,在返回之前执行2次非常快速的操作。
首先,此函数调用igb_write_itr
仅更新硬件特定的寄存器。在这种情况下,更新的寄存器是一个用于跟踪硬件中断到达速率的寄存器。
该寄存器与称为“中断限制”(也称为“中断合并”)的硬件功能结合使用,该功能可用于加快将中断传递到CPU的速度。我们很快将看到如何ethtool
提供一种机制来调整IRQ的发射速率。
其次,napi_schedule
调用,如果尚未激活NAPI处理循环,则会将其唤醒。注意,NAPI处理循环在softirq中执行。NAPI处理循环不会从中断处理程序执行。中断处理程序只是使它开始执行(如果尚未执行)。
确切说明其工作原理的实际代码很重要。它将指导我们了解如何在多CPU系统上处理网络数据。
NAPI和 napi_schedule
让我们弄清楚napi_schedule
来自硬件中断处理程序的调用是如何工作的。
请记住,NAPI专门用于收集网络数据,而无需NIC中断就可以发出数据已准备好进行处理的信号。如前所述,poll
通过接收硬件中断来引导NAPI 循环。换句话说:NAPI已启用,但已关闭,直到第一个数据包到达时,NIC会发出IRQ并启动NAPI。我们将很快看到,还有其他几种情况,可以禁用NAPI,并且需要再次引发硬件中断才能重新启动它。
当驱动程序中的中断处理程序调用时,将启动NAPI轮询循环napi_schedule
。napi_schedule
实际上只是一个在头文件中定义的包装函数,它调用__napi_schedule
。
- /**
- * __napi_schedule - schedule for receive
- * @n: entry to schedule
- *
- * The entry's receive function will be scheduled to run
- */
- void __napi_schedule(struct napi_struct *n)
- {
- unsigned long flags;
-
- local_irq_save(flags);
- ____napi_schedule(&__get_cpu_var(softnet_data), n);
- local_irq_restore(flags);
- }
- EXPORT_SYMBOL(__napi_schedule);
此代码__get_cpu_var
用于获取softnet_data
注册到当前CPU 的结构。此softnet_data
结构和struct napi_struct
从驱动程序传递的结构都传递到____napi_schedule
。哇,有很多下划线;)
让我们____napi_schedule
从net / core / dev.c看一下:
- /* Called with irq disabled */
- static inline void ____napi_schedule(struct softnet_data *sd,
- struct napi_struct *napi)
- {
- list_add_tail(&napi->poll_list, &sd->poll_list);
- __raise_softirq_irqoff(NET_RX_SOFTIRQ);
- }
这段代码做了两件事:
struct napi_struct
从设备驱动程序的中断处理程序代码手向上被添加到poll_list
附接至softnet_data
与当前CPU相关联的结构。__raise_softirq_irqoff
用于“提高”(或触发)NET_RX_SOFTIRQ softirq。net_rx_action
如果当前未在网络设备子系统初始化期间进行注册,这将导致该注册被执行。稍后我们将看到,softirq处理程序函数net_rx_action
将调用NAPI poll
函数来收集数据包。
有关CPU和网络数据处理的注释
注意,到目前为止,我们看到的所有将硬件中断处理程序推迟到softirq的代码都在使用与当前CPU相关的结构。
尽管驱动程序的IRQ处理程序本身自身所做的工作很少,但softirq处理程序将与驱动程序的IRQ处理程序在同一CPU上执行。
这就是为什么要为CPU设置特定IRQ的原因,这一点很重要:不仅要使用CPU在驱动程序中执行中断处理程序,而且要通过NAPI在softirq中收集数据包时,也要使用同一CPU。
正如我们稍后将看到的那样,诸如“ 接收数据包导向”之类的功能可以将其中的一些工作分配给网络堆栈上方的其他CPU。
监控网络数据到达
硬件中断请求
注意:监视硬件IRQ不能完整描述数据包处理的运行状况。NAPI运行时,许多驱动程序会关闭硬件IRQ,我们将在后面看到。这是整个监控解决方案的重要组成部分。
通过阅读检查硬件中断统计信息/proc/interrupts
。
- $ cat / proc / interrupts
- CPU0 CPU1 CPU2 CPU3
- 0:46 0 0 0 IR-IO-APIC边沿计时器
- 1:3 0 0 0 IR-IO-APIC边缘i8042
- 30:3361234770 0 0 0 IR-IO-APIC-fasteoi aacraid
- 64:0 0 0 0 DMAR_MSI边缘dmar0
- 65:1 0 0 0 IR-PCI-MSI-edge eth0
- 66:863649703 0 0 0 IR-PCI-MSI-edge eth0-TxRx-0
- 67:986285573 0 0 0 IR-PCI-MSI-edge eth0-TxRx-1
- 68:45 0 0 0 IR-PCI-MSI-edge eth0-TxRx-2
- 69:394 0 0 0 IR-PCI-MSI-edge eth0-TxRx-3
- NMI:9729927 4008190 3068645 3375402不可屏蔽的中断
- LOC:2913290785 1585321306 1495872829 1803524526本地计时器中断
您可以监视其中的统计信息,/proc/interrupts
以查看随着数据包到达而导致的硬件中断的数量和速率如何变化,并确保由适当的CPU处理NIC的每个RX队列。正如我们很快就会看到,这个数字只告诉我们的硬件中断了多少事,但它并不一定是好的度量了解有多少数据已经收到或处理尽可能多的驱动程序将禁用网卡的IRQ作为其合同的一部分NAPI子系统。此外,使用中断合并也将影响从该文件收集的统计信息。监视此文件可以帮助您确定所选的中断合并设置是否确实有效。
为了更全面地了解您的网络处理运行状况,您需要监控/proc/softirqs
(如上所述)和其他文件(/proc
我们将在下面介绍)。
调整网络数据到达
中断合并
中断合并是一种防止设备在特定数量的工作或事件数量挂起之前引发中断的方法。
这可以帮助防止中断风暴,并可以帮助提高吞吐量或延迟,具体取决于所使用的设置。生成的中断更少,从而提高了吞吐量,增加了延迟并降低了CPU使用率。产生更多的中断会产生相反的结果:较低的延迟,较低的吞吐量,但也会增加CPU使用率。
从历史上看,早期版本的igb
,e1000
和其他因素包括一个名为参数的支持InterruptThrottleRate
。在较新的驱动程序中,此参数已由通用ethtool
函数替换。
使用获取当前的IRQ合并设置ethtool -c
。
- $ sudo ethtool -c eth0
- eth0的合并参数:
- 自适应RX:关TX:关
- stats-block-usecs:0
- 采样间隔:0
- pkt-rate-low:0
- pkt-rate-high:0
- ...
ethtool
提供用于设置各种合并设置的通用接口。但是请记住,并非每个设备或驱动程序都支持所有设置。您应该检查驱动程序文档或驱动程序源代码,以确定是否支持什么。根据ethtool文档:“任何未由驱动程序执行的操作都会导致这些值被忽略。”
一些驱动程序支持的一个有趣的选项是“自适应RX / TX IRQ合并”。此选项通常在硬件中实现。驱动程序通常需要做一些工作来通知NIC此功能已启用,并且还需要进行一些记帐(如igb
上面的驱动程序代码所示)。
启用自适应RX / TX IRQ合并的结果是,将调整中断传递,以在数据包速率较低时改善等待时间,并在数据包速率较高时提高吞吐量。
通过以下方式启用自适应RX IRQ合并 ethtool -C
$ sudo ethtool -C eth0自适应-rx
您也可以使用ethtool -C
设置几个选项。一些较常见的设置选项是:
rx-usecs
:数据包到达后有多少个usecs延迟RX中断。rx-frames
:RX中断之前要接收的最大数据帧数。rx-usecs-irq
:主机正在处理中断时,有多少个usec可以延迟RX中断。rx-frames-irq
:系统正在处理中断时,在产生RX中断之前要接收的最大数据帧数。还有很多很多。
请注意,您的硬件和驱动程序可能仅支持上面列出的选项的一部分。您应该查阅驱动程序源代码和硬件数据表,以获取有关支持的合并选项的更多信息。
不幸的是,除了头文件之外,您可以设置的选项在任何地方都没有得到很好的记录。检查include / uapi / linux / ethtool.h的来源,以找到对所支持的每个选项的解释ethtool
(但不一定是您的驱动程序和NIC)。
注意:乍一看,中断合并似乎是一个非常有用的优化,但是尝试进行优化时,其余的网络堆栈内部组件也变得非常重要。在某些情况下,中断合并可能会很有用,但您应确保对网络堆栈的其余部分也进行了适当的调整。仅仅修改您的合并设置可能会为其自身带来最小的收益。
调整IRQ亲和力
如果您的NIC支持RSS /多队列,或者您正在尝试针对数据本地性进行优化,则您可能希望使用一组特定的CPU处理NIC生成的中断。
通过设置特定的CPU,您可以细分哪些CPU将用于处理哪些IRQ。如我们在网络堆栈中所见,这些变化可能会影响高层的操作方式。
如果您决定调整IRQ关联性,则应首先检查是否运行irqbalance
守护程序。该守护程序试图自动使IRQ与CPU保持平衡,并可能覆盖您的设置。如果您正在运行irqbalance
,则应禁用irqbalance
或将其--banirq
与一起使用,IRQBALANCE_BANNED_CPUS
以irqbalance
告知它不应触摸您要为其分配的一组IRQ和CPU。
接下来,应检查文件/proc/interrupts
以获取NIC的每个网络RX队列的IRQ编号列表。
最后,您可以通过修改/proc/irq/IRQ_NUMBER/smp_affinity
每个IRQ编号来调整将处理每个IRQ的CPU 。
您只需向该文件写入一个十六进制的位掩码,以指示内核应使用哪些CPU处理IRQ。
示例:将IRQ 8的IRQ关联性设置为CPU 0
$ sudo bash -c'echo 1> / proc / irq / 8 / smp_affinity'
一旦softirq代码确定softirq待处理,开始处理并执行net_rx_action
,网络数据处理就会开始。
让我们看一下net_rx_action
处理循环的各个部分,以了解其工作原理,可调整的部分以及可监视的部分。
net_rx_action
处理循环
net_rx_action
开始从内存中处理数据包,然后设备将数据包DMA到这些数据包中。
该函数遍历为当前CPU排队的NAPI结构列表,使每个结构出队并对其进行操作。
处理循环限制了注册的NAPI poll
函数可以消耗的工作量和执行时间。它以两种方式执行此操作:
budget
(可以调整),以及- while (!list_empty(&sd->poll_list)) {
- struct napi_struct *n;
- int work, weight;
-
- /* If softirq window is exhausted then punt.
- * Allow this to run for 2 jiffies since which will allow
- * an average latency of 1.5/HZ.
- */
- if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit)))
- goto softnet_break;
这就是内核防止数据包处理消耗整个CPU的方式。在budget
上面是预算总额将每个注册该CPU可用NAPI结构中度过。
这是为什么多队列NIC应该仔细调整IRQ亲和力的另一个原因。回想一下,处理来自设备的IRQ的CPU将是执行softirq处理程序的CPU,因此,也将是运行上述循环和预算计算的CPU。
在将多个NAPI结构注册到同一CPU的情况下,具有多个NIC(每个NIC都具有多个队列)的系统可能会结束。同一CPU上所有NAPI结构的数据处理花费相同budget
。
如果没有足够的CPU来分发NIC的IRQ,则可以考虑增加,net_rx_action
budget
以允许每个CPU处理更多的数据包。增加预算将增加CPU使用率(特别是在程序中sitime
或si
在top
其他程序中),但应减少延迟,因为将更迅速地处理数据。
注意:无论分配的预算如何,CPU仍将受到2个jiffies的时间限制。
NAPI poll
函数和weight
回想一下网络设备驱动程序netif_napi_add
用于注册poll
功能。正如我们在本文前面所看到的,igb
驱动程序具有如下代码:
- /* initialize NAPI */
- netif_napi_add(adapter->netdev, &q_vector->napi, igb_poll, 64);
这将注册一个NAPI结构,其硬编码权重为64。现在我们将了解如何在net_rx_action
处理循环中使用它。
- weight = n->weight;
-
- work = 0;
- if (test_bit(NAPI_STATE_SCHED, &n->state)) {
- work = n->poll(n, weight);
- trace_napi_poll(n);
- }
-
- WARN_ON_ONCE(work > weight);
-
- budget -= work;
此代码获得已注册到NAPI结构(64
在上面的驱动程序代码中)的权重,并将其传递到poll
也已注册到NAPI结构(igb_poll
在上面的代码中)的函数中。
该poll
函数返回已处理的数据帧数。此金额另存为work
,然后从总体中减去budget
。
因此,假设:
64
驱动程序的权重(在Linux 3.13.0中,所有驱动程序都使用该值进行了硬编码),并且budget
设置为默认值300
在以下情况之一下,系统将停止处理数据:
igb_poll
函数最多调用了5次(如果没有数据要处理,将减少调用次数),或者NAPI /网络设备驱动程序合同
关于NAPI子系统和设备驱动程序之间的合同的重要信息之一是尚未关闭的有关关闭NAPI的要求。
合同的这一部分如下:
poll
功能会消耗其整个重量(这是硬编码64
),则必须不修改NAPI状态。该net_rx_action
循环将接管。poll
功能不不消耗它的整个重量,所以必须禁用NAPI。下次收到IRQ并且驱动程序的IRQ处理程序调用时,将重新启用NAPI napi_schedule
。现在,我们将了解如何net_rx_action
处理合同的第一部分。接下来,poll
检查功能,我们将看到如何处理合同的第二部分。
完成net_rx_action
循环
该net_rx_action
处理循环完成了代码的最后一个部分,与合同NAPI的第一部分涉及在上一节中的说明。从net / core / dev.c:
- /* Drivers must not modify the NAPI state if they
- * consume the entire weight. In such cases this code
- * still "owns" the NAPI instance and therefore can
- * move the instance around on the list at-will.
- */
- if (unlikely(work == weight)) {
- if (unlikely(napi_disable_pending(n))) {
- local_irq_enable();
- napi_complete(n);
- local_irq_disable();
- } else {
- if (n->gro_list) {
- /* flush too old packets
- * If HZ < 1000, flush all packets.
- */
- local_irq_enable();
- napi_gro_flush(n, HZ >= 1000);
- local_irq_disable();
- }
- list_move_tail(&n->poll_list, &sd->poll_list);
- }
- }

如果整个工作都用完了,则有两种情况可以net_rx_action
处理:
ifconfig eth0 down
),这就是数据包处理循环如何调用驱动程序的注册poll
函数来处理数据包的方式。稍后我们将看到,该poll
函数将收集网络数据并将其发送到堆栈进行处理。
达到极限时退出循环
net_rx_action
在以下任一情况下,循环将退出:
!list_empty(&sd->poll_list)
),或者这是我们之前再次看到的代码:
- /* If softirq window is exhausted then punt.
- * Allow this to run for 2 jiffies since which will allow
- * an average latency of 1.5/HZ.
- */
- if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit)))
- goto softnet_break;
如果遵循softnet_break
标签,您会偶然发现一些有趣的东西。从net / core / dev.c:
- softnet_break:
- sd->time_squeeze++;
- __raise_softirq_irqoff(NET_RX_SOFTIRQ);
- goto out;
该struct softnet_data
结构的统计量增加了,并且softirq NET_RX_SOFTIRQ
已关闭。该time_squeeze
字段衡量的是net_rx_action
要做更多工作的次数,但预算已用完或在完成之前已达到时间限制。这对于了解网络处理中的瓶颈非常有用。不久我们将看到如何监视此值。该NET_RX_SOFTIRQ
被禁用,以腾出其他任务的处理时间。这是有道理的,因为只有在可以完成更多工作的情况下才执行此小段代码,但我们不想垄断CPU。
然后将执行转移到out
标签。out
如果没有更多要处理的NAPI结构,执行也可以放在标签上,换句话说,预算多于网络活动,并且所有驱动程序都已关闭NAPI,并且无事可做net_rx_action
。
该out
节在返回之前做一件重要的事情net_rx_action
:它调用net_rps_action_and_irq_enable
。如果启用了接收数据包导向,则此功能起着重要的作用。它唤醒远程CPU以开始处理网络数据。
稍后,我们将详细介绍RPS的工作方式。现在,让我们看看如何监视net_rx_action
处理循环的运行状况,并继续进行NAPI poll
函数的内部工作,以便我们可以升级网络堆栈。
NAPI民意调查
回顾前面的部分,设备驱动程序为设备分配了一个内存区域,以便对传入的数据包执行DMA。正如分配这些区域是驱动程序的责任一样,取消映射那些区域,收集数据并将其发送到网络堆栈也是驱动程序的责任。
让我们看一下igb
驾驶员如何执行此操作,以了解其在实际中的工作原理。
igb_poll
最后,我们终于可以检查我们的朋友了igb_poll
。事实证明,的代码igb_poll
看似简单。让我们来看看。从driver / 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;
- }

这段代码做了一些有趣的事情:
igb_clean_rx_irq
称为,它会进行繁重的工作,接下来我们将看到。clean_complete
检查以确定是否还有更多可以完成的工作。如果是这样,则返回budget
(请记住,这已被硬编码为64
)。如前所述,net_rx_action
将这个NAPI结构移动到轮询列表的末尾。napi_complete
并通过调用重新启用中断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
。csum_error
统计量会增加。如果校验和成功,并且数据是UDP或TCP数据,则将skb
标记为CHECKSUM_UNNECESSARY
。如果校验和失败,则协议栈留给该数据包处理。协议是通过调用eth_type_trans
并存储在skb
结构中来计算的。skb
的程序通过调用移交给网络堆栈napi_gro_receive
。循环终止后,该函数将为rx数据包和已处理的字节分配统计信息计数器。
现在是时候绕开网络栈进行两次绕道了。首先,让我们看看如何监视和调整网络子系统的softirq。接下来,让我们谈谈通用接收卸载(GRO)。之后,进入网络时,其余的网络堆栈将变得更有意义napi_gro_receive
。
监控网络数据处理
/proc/net/softnet_stat
如上一节所述,net_rx_action
退出net_rx_action
循环时和可以完成其他工作时增加一个统计量,但是budget
达到了softirq的时间限制。该统计信息将作为struct softnet_data
与CPU相关联的一部分进行跟踪。
这些统计信息在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);
其中许多统计信息的名称令人困惑,并且在您可能不希望看到的地方递增。在检查网络堆栈时,将提供有关何时增加何处以及何时增加这些位置的说明。由于从中可以squeeze_time
看到该统计信息net_rx_action
,所以我认为现在对该文件进行记录是有道理的。
通过读取监视网络数据处理的统计信息/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 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 00000000
- 6488cb92 00000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000
有关的重要详细信息/proc/net/softnet_stat
:
/proc/net/softnet_stat
对应一个struct softnet_data
结构,每个CPU有1 个结构。sd->processed
是处理的网络帧数。如果您使用以太网绑定,则该数量可能超过接收到的网络帧总数。在某些情况下,以太网绑定驱动程序将触发网络数据进行重新处理,这将使sd->processed
同一数据包的计数增加一次以上。sd->dropped
是由于处理队列上没有空间而丢弃的网络帧数。稍后再详细介绍。sd->time_squeeze
(如我们所见)是net_rx_action
由于消耗了预算或达到了时间限制而导致循环终止的次数,但本来可以做更多的工作。budget
如前所述增加可以减少这种情况。sd->cpu_collision
是传输数据包时尝试获取设备锁定时发生冲突的次数的计数。本文是关于接收的,因此下面将看不到此统计信息。sd->received_rps
是通过处理器间中断唤醒该CPU来处理数据包的次数的计数flow_limit_count
是达到流量限制的次数的计数。流量限制是一项可选的接收数据包控制功能,将很快进行检查。如果决定监视此文件并以图形方式显示结果,则必须非常小心,以确保这些字段的顺序没有更改,并且每个字段的含义均已保留。您将需要阅读内核源代码以进行验证。
调整网络数据处理
调整net_rx_action
预算
您可以调整net_rx_action
预算,该预算通过设置名为的sysctl值来确定可以在注册到CPU的所有NAPI结构中花费多少数据包处理net.core.netdev_budget
。
示例:将总体数据包处理预算设置为600。
$ sudo sysctl -w net.core.netdev_budget = 600
您可能还希望将此设置写入/etc/sysctl.conf
文件中,以便更改在重新引导之间保持不变。
Linux 3.13.0的默认值为300。
通用接收卸载(GRO)是一种硬件优化的软件实现,称为大型接收卸载(LRO)。
两种方法背后的主要思想是,通过将“足够相似”的数据包组合在一起来减少通过网络堆栈的数据包数量,可以减少CPU使用率。例如,假设发生了大型文件传输并且大多数数据包在文件中包含大块数据的情况。不需要一次将小的数据包一次发送到堆栈中,而是可以将传入的数据包合并为一个具有巨大有效负载的数据包。然后可以将该数据包向上传递到堆栈中。这允许协议层处理单个数据包的标头,同时将更大的数据块传递给用户程序。
这种优化的问题当然是信息丢失。如果一个数据包设置了一些重要的选项或标志,则如果该数据包合并到另一个中,则该选项或标志可能会丢失。这就是为什么大多数人不使用或不鼓励使用LRO的原因。一般来说,LRO实现对于合并数据包的规则非常宽松。
GRO是作为LRO在软件中的实现而引入的,但是围绕可合并数据包的规则更为严格。
顺便说一句:如果您曾经使用tcpdump
并看到过大的传入数据包大小,则很可能是因为您的系统启用了GRO。就像您将很快看到的那样,在GRO发生之后,数据包捕获分路器会插入到堆栈的更深处。
调整:使用 ethtool
您可以ethtool
用来检查是否启用了GRO,还可以调整设置。
使用ethtool -k
检查您的GRO设置。
- $ ethtool -k eth0 | grep泛型接收卸载
- 通用接收卸载:
如您所见,在此系统上,我已generic-receive-offload
设置为on。
使用ethtool -K
启用(或禁用)GRO。
$ sudo ethtool -K eth0继续
注意:对于大多数驱动程序,进行这些更改将关闭接口,然后将其恢复;与此接口的连接将中断。但是,对于一次更改而言,这可能无关紧要。
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
仅返回即可,因为没有其他事情可做。
如果数据包没有合并,并且MAX_GRO_SKBS
系统上的GRO流少于(8),则将新条目添加到gro_list
该CPU的NAPI结构上。
这是执行此操作的代码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
看看如何将数据传递到协议层了。在进行检查之前,我们需要首先了解接收数据包导向(RPS)。
回顾前面的内容,我们讨论了网络设备驱动程序如何注册NAPI poll
函数。每个NAPI
轮询器实例是在每个CPU都有一个softirq的上下文中执行的。进一步回想一下,驱动程序的IRQ处理程序在其上运行的CPU将唤醒其softirq处理循环以处理数据包。
换句话说:单个CPU处理硬件中断并轮询数据包以处理传入的数据。
某些NIC(如Intel I350)在硬件级别支持多个队列。这意味着传入的数据包可以通过DMA发送到每个队列的单独的内存区域,并具有单独的NAPI结构来管理对该区域的轮询。因此,多个CPU将处理来自设备的中断并处理数据包。
此功能通常称为接收方缩放(RSS)。
接收数据包导向(RPS)是RSS的软件实现。由于它是通过软件实现的,因此这意味着可以为任何NIC启用它,甚至只有一个RX队列的NIC也可以启用它。但是,由于它是软件形式的,因此这意味着RPS只能在从DMA存储器区域中收集到数据包后才能进入流。
这意味着您不会注意到花费在处理IRQ或NAPI poll
循环上的CPU时间会减少,但是您可以在收集到数据包后分配负载以处理该数据包,并从此减少网络堆栈的CPU时间。
RPS的工作原理是为传入的数据生成哈希,以确定哪个CPU应该处理该数据。然后将数据排队到每个CPU接收网络积压待处理。一个处理器间中断(IPI)被输送到CPU拥有的积压。如果当前未处理积压中的数据,这将有助于启动积压处理。的/proc/net/softnet_stat
包含倍每个数量的计数softnet_data
结构已收到IPI(该received_rps
字段)。
因此,netif_receive_skb
将继续在网络堆栈上发送网络数据,或将其移交给RPS以便在其他CPU上进行处理。
为了使RPS正常工作,必须在内核配置中启用它(在Ubuntu 3.13.0上为Ubuntu),并使用一个位掩码来描述哪些CPU应该处理给定接口和RX队列的数据包。
简而言之,可以在以下位置找到要修改的位掩码:
/sys/class/net/DEVICE_NAME/queues/QUEUE/rps_cpus
因此,对于eth0
接收队列0,您将修改文件:/sys/class/net/eth0/queues/rx-0/rps_cpus
用一个十六进制数字指示哪个CPU应该处理来自eth0
接收队列0的数据包。如文档所指出,在某些配置中RPS可能是不必要的。
注意:启用RPS将数据包处理分配给以前未处理数据包的CPU,将导致该CPU的“ NET_RX”软irq数量以及CPU使用情况图中的“ si”或“ sitime”增加。您可以比较softirq和CPU使用率图的前后,以确认RPS是否已根据您的喜好进行了正确配置。
接收流控制(RFS)与RPS结合使用。RPS尝试在多个CPU之间分配传入的数据包负载,但并未考虑任何数据局部性问题来最大化CPU缓存命中率。您可以使用RFS通过将相同流的数据包定向到同一CPU进行处理来帮助提高缓存命中率。
为了使RFS正常工作,您必须启用并配置RPS。
RFS跟踪所有流的全局哈希表,并且可以通过设置net.core.rps_sock_flow_entries
sysctl 来调整此哈希表的大小。
通过设置来增加RFS套接字流哈希的大小sysctl
。
$ sudo sysctl -w net.core.rps_sock_flow_entries = 32768
接下来,您还可以通过将该值写入rps_flow_cnt
为每个RX队列命名的sysfs文件中来设置每个RX队列的流数。
示例:将eth0上的RX队列0的流数量增加到2048。
$ sudo bash -c'echo 2048> / sys / class / net / eth0 / queues / rx-0 / rps_flow_cnt'
借助硬件加速可以加快RFS;NIC和内核可以一起工作,以确定应该在哪些CPU上处理哪些流。要使用此功能,NIC和您的驱动程序必须支持它。
请查阅NIC的数据表,以确定是否支持此功能。如果您的NIC驱动程序公开了一个名为的函数ndo_rx_flow_steer
,则该驱动程序支持加速的RFS。
假设您的NIC和驱动程序支持它,则可以通过启用和配置以下各项来启用加速的RFS:
CONFIG_RFS_ACCEL
在编译时启用。Ubuntu内核3.13.0可以。ethtool
用来验证是否已为设备启用ntuple支持。完成上述配置后,加速的RFS将用于自动将数据移动到与正在处理该流的数据的CPU内核绑定的RX队列,并且您无需为每个流手动指定ntuple过滤规则。
netif_receive_skb
netif_receive_skb
从几个地方调用,然后从我们停下来的地方接机。最常见的两种(以及我们已经讨论过的两种):
napi_skb_finish
如果数据包不打算合并到现有的GRO流中,或者napi_gro_complete
如果协议层指示是时候冲洗流了,或者提醒: netif_receive_skb
及其后代均在softirq处理循环的上下文中运行,您将看到在此处花费的时间与sitime
或si
使用诸如之类的工具有关top
。
netif_receive_skb
首先,首先检查一个sysctl
值以确定用户是否在数据包到达积压队列之前或之后请求接收时间戳。如果启用此设置,则在达到RPS(和CPU的相关积压队列)之前,现在先给数据加上时间戳。如果禁用此设置,它将在打入队列后加上时间戳。如果启用了RPS,则可用于在多个CPU之间分配时间戳的负载,但结果会带来一些延迟。
您可以通过调整名为的sysctl来调整在接收到数据包后何时给它们加上时间戳net.core.netdev_tstamp_prequeue
:
通过调整a来禁用RX数据包的时间戳 sysctl
$ sudo sysctl -w net.core.netdev_tstamp_prequeue = 0
默认值为1。有关此设置的确切含义,请参见上一部分。
netif_receive_skb
处理时间戳之后,netif_receive_skb
根据是否启用RPS进行不同的操作。让我们从更简单的路径开始:禁用RPS。
如果未启用RPS,__netif_receive_skb
将调用进行簿记,然后调用__netif_receive_skb_core
将数据移到协议栈附近。
我们将精确地了解其__netif_receive_skb_core
工作原理,但首先让我们了解启用RPS的代码路径如何工作,因为该代码也会调用__netif_receive_skb_core
。
如果启用了RPS,则在处理了上述时间戳选项之后,netif_receive_skb
将执行一些计算以确定应使用哪个CPU的待办事项队列。这是通过使用函数来完成的get_rps_cpu
。从net / core / dev.c:
- cpu = get_rps_cpu(skb->dev, skb, &rflow);
-
- if (cpu >= 0) {
- ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);
- rcu_read_unlock();
- return ret;
- }
get_rps_cpu
将考虑如上所述的RFS和aRFS设置,以确保通过调用来将数据排队到所需CPU的待办事项列表中enqueue_to_backlog
。
enqueue_to_backlog
此函数首先获取指向远程CPU softnet_data
结构的指针,该结构包含指向的指针input_pkt_queue
。接下来,input_pkt_queue
检查远程CPU 的队列长度。从net / core / dev.c:
- qlen = skb_queue_len(&sd->input_pkt_queue);
- if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {
的长度input_pkt_queue
首先与相比较netdev_max_backlog
。如果队列长于此值,则丢弃数据。同样,检查流量限制,如果超过流量限制,则丢弃数据。在这两种情况下,softnet_data
结构上的下降计数都会增加。请注意,这是softnet_data
将要排队的数据的CPU 的结构。阅读上面的部分,/proc/net/softnet_stat
以了解如何获取丢弃计数以进行监视。
enqueue_to_backlog
在很多地方都不叫。启用RPS的数据包处理也需要调用它netif_rx
。大多数司机应该不被使用netif_rx
,而应使用netif_receive_skb
。如果您未使用RPS且驱动程序未使用netif_rx
,则增加积压不会对您的系统产生任何明显的影响,因为未使用它。
注意:您需要检查所使用的驱动程序。如果它调用netif_receive_skb
并且您没有使用RPS,则增加netdev_max_backlog
不会带来任何性能改进,因为没有数据可以到达input_pkt_queue
。
假设input_pkt_queue
足够小并且没有达到(或禁用)流量限制(可以禁用),则可以将数据排队。这里的逻辑有点有趣,但可以总结为:
____napi_schedule
。继续对数据进行排队。该代码使用会有些棘手goto
,因此请仔细阅读。从net / core / dev.c:
- if (skb_queue_len(&sd->input_pkt_queue)) {
- enqueue:
- __skb_queue_tail(&sd->input_pkt_queue, skb);
- input_queue_tail_incr_save(sd, qtail);
- rps_unlock(sd);
- local_irq_restore(flags);
- return NET_RX_SUCCESS;
- }
-
- /* Schedule NAPI for backlog device
- * We can use non atomic operation since we own the queue lock
- */
- if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {
- if (!rps_ipi_queued(sd))
- ____napi_schedule(sd, &sd->backlog);
- }
- goto enqueue;

流量限制
RPS在多个CPU之间分配数据包处理负载,但是单个大流量可以独占CPU处理时间,并使较小的流量匮乏。流限制是一项功能,可用于将每个流的排队到待办事项列表的数据包数量限制为一定数量。这可以帮助确保即使更大的流量将数据包推入,也可以处理较小的流量。
net / core / dev.c中的上述if语句通过调用以下命令检查流量限制skb_flow_limit
:
if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {
此代码检查队列中是否仍有空间,并且尚未达到流量限制。默认情况下,流量限制处于禁用状态。为了启用流量限制,您必须指定一个位图(类似于RPS的位图)。
监视:监视由于满input_pkt_queue
或流量限制而导致的跌落
请参阅上面有关监视的部分/proc/net/softnet_stat
。该dropped
字段是一个计数器,该计数器在每次删除数据时都会递增,而不是排队到CPU的队列中input_pkt_queue
。
调音
调整:调整netdev_max_backlog
以防止跌落
在调整此调整值之前,请参阅上一节中的注释。
如果使用RPS或驱动程序调用,可以enqueue_to_backlog
通过增加来帮助防止netdev_max_backlog
掉线netif_rx
。
示例:使用将未完成订单增加到3000 sysctl
。
$ sudo sysctl -w net.core.netdev_max_backlog = 3000
默认值为1000。
调整:调整积压poll
循环的NAPI权重
您可以通过设置net.core.dev_weight
sysctl 来调整积压NAPI轮询器的权重。调整此值将确定积压poll
循环可以消耗多少总预算(请参阅上面有关调整的部分net.core.netdev_budget
):
示例:使用增加NAPI poll
积压处理循环sysctl
。
$ sudo sysctl -w net.core.dev_weight = 600
默认值为64。
请记住,积压处理在softirq上下文中运行,类似于设备驱动程序的注册poll
功能,并且受总体budget
和时间限制,如前几节所述。
调整:启用流量限制和流量限制哈希表大小
用设置流量限制表的大小sysctl
。
$ sudo sysctl -w net.core.flow_limit_table_len = 8192
默认值为4096。
此更改仅影响新分配的流哈希表。因此,如果您想增加表的大小,则应在启用流量限制之前执行此操作。
要启用流量限制,您应指定/proc/sys/net/core/flow_limit_cpu_bitmap
类似于RPS位掩码的位掩码,该位掩码指示哪些CPU启用了流量限制。
每个CPU的待办事项队列以与设备驱动程序相同的方式插入NAPI。甲poll
设置功能,用于处理数据包从软中断上下文。weight
也提供了A ,就像设备驱动程序一样。
在网络系统初始化期间会提供此NAPI结构。从net_dev_init
在net/core/dev.c
:
- sd->backlog.poll = process_backlog;
- sd->backlog.weight = weight_p;
- sd->backlog.gro_list = NULL;
- sd->backlog.gro_count = 0;
积压的NAPI结构与设备驱动程序NAPI结构的不同之处在于该weight
参数是可调整的,因为驱动程序将其NAPI权重硬编码为64。我们将在下面的调整部分中看到如何使用来调整权重sysctl
。
process_backlog
该process_backlog
函数是一个循环,一直运行到消耗其权重(如上一节中所述)或积压后不再有任何数据为止。
待办事项队列上的每个数据都从待办事项队列中删除,并传递给__netif_receive_skb
。数据命中后的代码路径__netif_receive_skb
与上述针对RPS禁用情况的解释相同。即,__netif_receive_skb
在调用之前将某些簿记做为__netif_receive_skb_core
将网络数据传递到协议层。
process_backlog
与设备驱动程序遵循与NAPI相同的合同,即:如果不使用总重量,则NAPI被禁用。如上所述,通过对____napi_schedule
from 的调用重新启动轮询器enqueue_to_backlog
。
该函数返回已完成的工作量,该工作量net_rx_action
(如上所述)将从预算中减去(如上所述,通过进行调整net.core.netdev_budget
)。
__netif_receive_skb_core
将数据传送到数据包抽头和协议层__netif_receive_skb_core
执行繁重的工作,将数据传递到协议栈。在执行此操作之前,它将检查是否已安装任何捕获所有传入数据包的数据包分接头。AF_PACKET
地址族就是一个做到这一点的例子,通常通过libpcap库使用。
如果存在这种抽头,则首先将数据传输到那里,然后再传输到协议层。
如果安装了数据包分接头(通常通过libpcap),则使用net / core / dev.c中的以下代码将数据包发送到那里:
- list_for_each_entry_rcu(ptype, &ptype_all, list) {
- if (!ptype->dev || ptype->dev == skb->dev) {
- if (pt_prev)
- ret = deliver_skb(skb, pt_prev, orig_dev);
- pt_prev = ptype;
- }
- }
如果您对数据通过pcap的路径感到好奇,请阅读net / packet / af_packet.c。
一旦满足抽头,__netif_receive_skb_core
就将数据传送到协议层。它通过从数据中获取协议字段并遍历为该协议类型注册的传递功能列表来做到这一点。
可以__netif_receive_skb_core
在net / core / dev.c中看到:
- type = skb->protocol;
- list_for_each_entry_rcu(ptype,
- &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
- if (ptype->type == type &&
- (ptype->dev == null_or_dev || ptype->dev == skb->dev ||
- ptype->dev == orig_dev)) {
- if (pt_prev)
- ret = deliver_skb(skb, pt_prev, orig_dev);
- pt_prev = ptype;
- }
- }
ptype_base
上面的标识符定义为net / core / dev.c中列表的哈希表:
struct list_head ptype_base[PTYPE_HASH_SIZE] __read_mostly;
每个协议层都会在哈希表中给定插槽的列表中添加一个过滤器,该过滤器通过一个名为ptype_head
以下函数的帮助器函数进行计算:
- static inline struct list_head *ptype_head(const struct packet_type *pt)
- {
- if (pt->type == htons(ETH_P_ALL))
- return &ptype_all;
- else
- return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
- }
通过调用来将过滤器添加到列表中dev_add_pack
。这就是协议层如何为其协议类型注册自身以进行网络数据传递的方法。
现在您知道了网络数据如何从NIC到达协议层。
现在我们知道如何将数据从网络设备子系统传递到协议栈,让我们看看协议层如何注册自己。
这篇博客文章将研究IP协议栈,因为它是一种常用协议,并且与大多数读者相关。
IP协议层将自身插ptype_base
入哈希表,以便将数据从前面各节中描述的网络设备层传递到哈希表。
这发生在功能inet_init
从网/的IPv4 / af_inet.c:
dev_add_pack(&ip_packet_type);
这将注册在net / ipv4 / af_inet.c中定义的IP数据包类型结构:
- static struct packet_type ip_packet_type __read_mostly = {
- .type = cpu_to_be16(ETH_P_IP),
- .func = ip_rcv,
- };
__netif_receive_skb_core
调用deliver_skb
(如上一节所述),该调用func
(在本例中为ip_rcv
)。
ip_rcv
该ip_rcv
功能在高层次上非常简单。有几种完整性检查,以确保数据有效。统计数据计数器也遭到了破坏。
ip_rcv
通过将数据包ip_rcv_finish
通过netfilter传递到网络结束。这样做是为了使应该在IP协议层匹配的所有iptables规则都可以在继续之前查看该数据包。
我们可以ip_rcv
在net / ipv4 / ip_input.c中看到将数据移交给netfilter的代码:
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);
netfilter和iptables
为了简洁起见(和我的RSI),我决定跳过对Netfilter,iptables和conntrack的深入研究。
简短的版本是NF_HOOK_THRESH
将检查是否安装了任何过滤器,并尝试将执行返回到IP协议层,以避免更深入地进入netfilter以及钩在iptables和conntrack之内的任何内容。
请记住:如果您有大量或非常复杂的netfilter或iptables规则,这些规则将在softirq上下文中执行,并且可能导致网络堆栈中的延迟。但是,如果需要安装一组特定的规则,这可能是不可避免的。
ip_rcv_finish
一旦网络过滤器有机会查看数据并决定如何处理它,ip_rcv_finish
就被称为。当然,只有在数据没有被netfilter丢弃的情况下才会发生这种情况。
ip_rcv_finish
从优化开始。为了将数据包传送到正确的位置,dst_entry
需要在路由系统中放置一个。为了获得一个,代码最初尝试early_demux
从该数据预定用于的更高层协议中调用该函数。
该early_demux
例程是一种优化,它dst_entry
通过检查a dst_entry
是否在套接字结构上缓存来尝试找到传递数据包所需的资源。
这是net / ipv4 / ip_input.c中的内容:
- if (sysctl_ip_early_demux && !skb_dst(skb) && skb->sk == NULL) {
- const struct net_protocol *ipprot;
- int protocol = iph->protocol;
-
- ipprot = rcu_dereference(inet_protos[protocol]);
- if (ipprot && ipprot->early_demux) {
- ipprot->early_demux(skb);
- /* must reload iph, skb->head might have changed */
- iph = ip_hdr(skb);
- }
- }
如您在上面看到的,此代码由sysctl保护sysctl_ip_early_demux
。默认情况下early_demux
启用。下一部分包括有关如何禁用它以及为什么要禁用它的信息。
如果启用了优化并且没有缓存的条目(因为这是第一个到达的数据包),则该数据包将被移交给内核中的路由系统,在其中dst_entry
计算并分配。
路由层完成后,统计信息计数器将更新,并且该函数将通过调用结束,该调用dst_input(skb)
又调用dst_entry
路由系统所附加的数据包结构上的输入函数指针。
如果数据包的最终目的地是本地系统,则路由系统会将功能附加ip_local_deliver
到dst_entry
数据包结构中的输入功能指针上。
调整:调整IP协议早期多路分配
early_demux
通过设置禁用优化sysctl
。
$ sudo sysctl -w net.ipv4.ip_early_demux = 0
预设值为1;early_demux
已启用。
添加此系统是因为某些用户在某些情况下通过优化发现吞吐量降低了5%early_demux
。
ip_local_deliver
回想一下我们如何在IP协议层中看到以下模式:
ip_rcv
一些初始簿记。ip_rcv_finish
是完成处理并继续努力将数据包推入网络堆栈的回调。ip_local_deliver
具有相同的模式。从net / ipv4 / ip_input.c:
- /*
- * Deliver IP Packets to the higher protocol layers.
- */
- int ip_local_deliver(struct sk_buff *skb)
- {
- /*
- * Reassemble IP fragments.
- */
-
- if (ip_is_fragment(ip_hdr(skb))) {
- if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
- return 0;
- }
-
- return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
- ip_local_deliver_finish);
- }

一旦netfilter有机会查看了数据,ip_local_deliver_finish
将假定没有首先由netfilter删除数据的情况下调用该数据。
ip_local_deliver_finish
ip_local_deliver_finish
从数据包中获取协议,查找net_protocol
为该协议注册的结构,并调用handler
该net_protocol
结构中指向的函数。
这将数据包交给更高级别的协议层。
监视:IP协议层统计
通过阅读监视详细的IP协议统计信息/proc/net/snmp
。
- $ cat / proc / net / snmp
- Ip:转发默认TTL InReceives InHdrErrors InAddrErrors ForwDatagrams InUnknownProtos InDiscards InDelivers OutRequests OutDiscards OutNoRoutes ReasmTimeout ReasmReqds ReasmOKs ReasmFails FragOKs FragFails FragCreates
- IP:1 64 25922988125 0 0 15771700 0 0 25898327616 22789396404 12987882 51 1 10129840 2196520 1 0 0 0
- ...
该文件包含几个协议层的统计信息。IP协议层首先出现。第一行包含下一行中每个对应值的空格分隔名称。
在IP协议层中,您会发现统计信息计数器被颠簸。这些计数器由C枚举引用。所有有效的枚举值及其对应的字段名称/proc/net/snmp
都可以在include / uapi / linux / snmp.h中找到:
- enum
- {
- IPSTATS_MIB_NUM = 0,
- /* frequently written fields in fast path, kept in same cache line */
- IPSTATS_MIB_INPKTS, /* InReceives */
- IPSTATS_MIB_INOCTETS, /* InOctets */
- IPSTATS_MIB_INDELIVERS, /* InDelivers */
- IPSTATS_MIB_OUTFORWDATAGRAMS, /* OutForwDatagrams */
- IPSTATS_MIB_OUTPKTS, /* OutRequests */
- IPSTATS_MIB_OUTOCTETS, /* OutOctets */
-
- /* ... */
通过阅读监视扩展的IP协议统计信息/proc/net/netstat
。
- $ cat / proc / net / netstat | grep IpExt
- IpExt:InNoRoutes InTruncatedPkts InMcastPkts OutMcastPkts InBcastPkts OutBcastPkts InOctets OutOctets InMcastOctets OutMcastOctets InBcastOctets OutBcastOctets InCsumErrors InNoECTPkts InECT0Pktsu InCEPkts
- ipExt:0 0 0 0 277959 0 14568040307695 32991309088496 0 0 58649349 0 0 0 0 0
格式类似于/proc/net/snmp
,除了这两行以开头IpExt
。
一些有趣的统计数据:
InReceives
:ip_rcv
在进行任何数据完整性检查之前到达的IP数据包总数。InHdrErrors
:报头损坏的IP报文总数。标头太短,太长,不存在,IP协议版本号错误等。InAddrErrors
:主机不可达的IP报文总数。ForwDatagrams
:已转发的IP数据包总数。InUnknownProtos
:标头中指定的协议未知或不受支持的IP数据包总数。InDiscards
:修剪数据包时由于内存分配失败或校验和失败而丢弃的IP数据包总数。InDelivers
:成功传送到更高协议层的IP数据包总数。请记住,即使IP层没有,这些协议层也可能会丢弃数据。InCsumErrors
:带有校验和错误的IP数据包总数。请注意,在IP层中的特定位置,每个位置都会增加。代码会不时地四处移动,并且可能会出现重复计数错误或其他会计错误。如果这些统计信息对您很重要,则强烈建议您阅读IP协议层源代码以获取对您重要的指标,因此您了解何时增加(和不增加)。
该博客文章将检查UDP,但是TCP协议处理程序的注册方式和时间与UDP协议处理程序相同。
在中net/ipv4/af_inet.c
,可以找到包含用于将UDP,TCP和ICMP协议连接到IP协议层的处理函数的结构定义。从net / ipv4 / af_inet.c:
- static const struct net_protocol tcp_protocol = {
- .early_demux = tcp_v4_early_demux,
- .handler = tcp_v4_rcv,
- .err_handler = tcp_v4_err,
- .no_policy = 1,
- .netns_ok = 1,
- };
-
- static const struct net_protocol udp_protocol = {
- .early_demux = udp_v4_early_demux,
- .handler = udp_rcv,
- .err_handler = udp_err,
- .no_policy = 1,
- .netns_ok = 1,
- };
-
- static const struct net_protocol icmp_protocol = {
- .handler = icmp_rcv,
- .err_handler = icmp_err,
- .no_policy = 1,
- .netns_ok = 1,
- };

这些结构被注册在inet地址系列的初始化代码中。从net / ipv4 / af_inet.c:
- /*
- * Add all the base protocols.
- */
-
- if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
- pr_crit("%s: Cannot add ICMP protocol\n", __func__);
- if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
- pr_crit("%s: Cannot add UDP protocol\n", __func__);
- if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
- pr_crit("%s: Cannot add TCP protocol\n", __func__);
我们将研究UDP协议层。如上所示,handler
UDP 的功能称为udp_rcv
。
这是IP层处理数据的UDP层的入口点。让我们继续那里的旅程。
UDP协议层的代码可以在net / ipv4 / udp.c中找到。
udp_rcv
该udp_rcv
函数的代码仅是一行,直接调用__udp4_lib_rcv
以处理接收数据报。
__udp4_lib_rcv
该__udp4_lib_rcv
功能将检查以确保数据包有效,并获取UDP标头,UDP数据报长度,源地址和目标地址。接下来,是一些其他的完整性检查和校验和验证。
回想一下,在IP协议层的前面,我们看到在将dst_entry
包传递到上层协议(在本例中为UDP)之前进行了优化以将a附加到包上。
如果dst_entry
找到套接字和对应的套接字,__udp4_lib_rcv
则将数据包排队到套接字:
- sk = skb_steal_sock(skb);
- if (sk) {
- struct dst_entry *dst = skb_dst(skb);
- int ret;
-
- if (unlikely(sk->sk_rx_dst != dst))
- udp_sk_rx_dst_set(sk, dst);
-
- ret = udp_queue_rcv_skb(sk, skb);
- sock_put(sk);
- /* a return value > 0 means to resubmit the input, but
- * it wants the return to be -protocol, or 0
- */
- if (ret > 0)
- return -ret;
- return 0;
- } else {

如果early_demux操作未连接任何套接字,则现在将通过调用来查找接收套接字__udp4_lib_lookup_skb
。
在上述两种情况下,数据报都将排队到套接字:
- ret = udp_queue_rcv_skb(sk, skb);
- sock_put(sk);
如果未找到套接字,则数据报将被丢弃:
- /* No socket. Drop packet silently, if checksum is wrong */
- if (udp_lib_checksum_complete(skb))
- goto csum_error;
-
- UDP_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
- icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
-
- /*
- * Hmm. We got an UDP packet to a port to which we
- * don't wanna listen. Ignore it.
- */
- kfree_skb(skb);
- return 0;
udp_queue_rcv_skb
此功能的初始部分如下:
最后,我们到达接收队列逻辑,首先检查套接字的接收队列是否已满。来自net/ipv4/udp.c
:
- if (sk_rcvqueues_full(sk, skb, sk->sk_rcvbuf))
- goto drop;
sk_rcvqueues_full
该sk_rcvqueues_full
函数检查套接字的待办事项长度和套接字的积压长度,sk_rmem_alloc
以确定总和是否大于套接字的总和sk_rcvbuf
(sk->sk_rcvbuf
在上面的代码片段中):
- /*
- * Take into account size of receive queue and backlog queue
- * Do not take into account this skb truesize,
- * to allow even a single big packet to come.
- */
- static inline bool sk_rcvqueues_full(const struct sock *sk, const struct sk_buff *skb,
- unsigned int limit)
- {
- unsigned int qsize = sk->sk_backlog.len + atomic_read(&sk->sk_rmem_alloc);
-
- return qsize > limit;
- }
调整这些值有些棘手,因为可以调整很多东西。
调优:套接字接收队列内存
可以将sk->sk_rcvbuf
(sk_rcvqueues_full
上述限制)值增加到sysctl net.core.rmem_max
设置为的值。
通过设置来增加最大接收缓冲区大小sysctl
。
$ sudo sysctl -w net.core.rmem_max = 8388608
sk->sk_rcvbuf
从该net.core.rmem_default
值开始,也可以通过设置sysctl进行调整,如下所示:
通过设置来调整默认的初始接收缓冲区大小sysctl
。
$ sudo sysctl -w net.core.rmem_default = 8388608
您还可以sk->sk_rcvbuf
通过setsockopt
从应用程序调用并传递来设置大小SO_RCVBUF
。您可以设置的最大值setsockopt
为net.core.rmem_max
。
但是,您可以net.core.rmem_max
通过调用setsockopt
和传递来覆盖限制SO_RCVBUFFORCE
,但是运行应用程序的用户将需要该CAP_NET_ADMIN
功能。
该sk->sk_rmem_alloc
值通过skb_set_owner_r
设置数据报所有者套接字的调用增加。稍后我们将在UDP层中看到此调用。
在sk->sk_backlog.len
被调用增加sk_add_backlog
,我们将在下面看到。
udp_queue_rcv_skb
一旦确认队列未满,就可以继续对数据报进行排队。从net / ipv4 / udp.c:
- bh_lock_sock(sk);
- if (!sock_owned_by_user(sk))
- rc = __udp_queue_rcv_skb(sk, skb);
- else if (sk_add_backlog(sk, skb, sk->sk_rcvbuf)) {
- bh_unlock_sock(sk);
- goto drop;
- }
- bh_unlock_sock(sk);
-
- return rc;
第一步是确定套接字当前是否有来自userland程序的系统调用。如果没有,则可以通过调用将数据报添加到接收队列__udp_queue_rcv_skb
。如果是这样,数据报将通过调用排队到待办事项列表中sk_add_backlog
。
当套接字系统调用release_sock
通过内核中的调用释放套接字时,积压的数据报将添加到接收队列中。
__udp_queue_rcv_skb
该__udp_queue_rcv_skb
函数通过调用将数据报添加到接收队列中,sock_queue_rcv_skb
如果无法将数据报添加到套接字的接收队列中,则增加统计计数器。
- rc = sock_queue_rcv_skb(sk, skb);
- if (rc < 0) {
- int is_udplite = IS_UDPLITE(sk);
-
- /* Note that an ENOMEM error is charged twice */
- if (rc == -ENOMEM)
- UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_RCVBUFERRORS,is_udplite);
-
- UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_INERRORS, is_udplite);
- kfree_skb(skb);
- trace_udp_fail_queue_rcv_skb(rc, sk);
- return -1;
- }
监视:UDP协议层统计信息
用于获取UDP协议统计信息的两个非常有用的文件是:
/proc/net/snmp
/proc/net/udp
/proc/net/snmp
通过阅读监视详细的UDP协议统计信息/proc/net/snmp
。
- $ cat / proc / net / snmp | grep Udp \:
- Udp:InDatagrams NoPorts InErrors OutDatagrams RcvbufErrors SndbufErrors
- Udp:16314 0 0 17161 0 0
与该文件中有关IP协议的详细统计信息非常相似,您将需要阅读协议层源来确定何时以及在何处增加这些值。
InDatagrams
:recvmsg
由userland程序用来读取数据报的时间增加。当UDP数据包被封装并发回进行处理时,该值也会增加。NoPorts
:当UDP数据包到达没有程序正在侦听的端口时增加。InErrors
:在以下几种情况下增加:接收队列中没有内存,看到错误的校验和,以及sk_add_backlog
添加数据报失败。OutDatagrams
:当将UDP数据包无误传递到要发送的IP协议层时增加。RcvbufErrors
:在sock_queue_rcv_skb
报告没有可用内存时增加;如果sk->sk_rmem_alloc
大于或等于,则会发生这种情况sk->sk_rcvbuf
。SndbufErrors
:如果IP协议层在尝试发送数据包时报告错误,并且未设置错误队列,则增加。如果没有发送队列空间或内核内存可用,也将增加。InCsumErrors
:当检测到UDP校验和失败时增加。请注意,在所有情况下,我都InCsumErrors
与一起被补偿InErrors
。因此,InErrors
- InCsumErros
应该在接收端产生与内存相关的错误计数。/proc/net/udp
通过阅读监视UDP套接字统计信息 /proc/net/udp
- $ cat / proc / net / udp
- sl local_address rem_address st tx_queue rx_queue tr tm->当retrnsmt uid超时inode ref指针掉落时
- 515:00000000:B346 00000000:0000 07 00000000:00000000 00:00000000 00000000 104 0 7518 2 0000000000000000 0
- 558:00000000:0371 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7408 2 0000000000000000 0
- 588:0100007F:038F 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7511 2 0000000000000000 0
- 769:00000000:0044 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7673 2 0000000000000000 0
- 812:00000000:006F 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7407 2 0000000000000000 0
第一行描述了以下各行中的每个字段:
sl
:套接字的内核哈希槽local_address
:套接字的十六进制本地地址和端口号,以分隔:
。rem_address
:套接字的十六进制远程地址和端口号,以分隔:
。st
:套接字的状态。奇怪的是,UDP协议层似乎使用了某些TCP套接字状态。在上面的示例中,7
是TCP_CLOSE
。tx_queue
:在内核中为传出UDP数据报分配的内存量。rx_queue
:内核中为传入的UDP数据报分配的内存量。tr
,tm->when
,retrnsmt
:这些字段是通过UDP协议层使用。uid
:创建此套接字的用户的有效用户ID。timeout
:UDP协议层未使用。inode
:与此套接字对应的索引节点号。您可以使用它来帮助您确定哪个用户进程打开了此套接字。检查/proc/[pid]/fd
,其中将包含指向的符号链接socket[:inode]
。ref
:套接字的当前引用计数。pointer
:内核中的内存地址struct sock
。drops
:与此套接字关联的数据报丢弃数。请注意,这不包括与发送数据报有关的任何丢弃(在已塞好的UDP套接字上或以其他方式);从本博客文章所检查的内核版本开始,该值仅在接收路径中递增。可以在中找到net/ipv4/udp.c
输出此代码的代码。
网络数据通过调用排队到套接字中sock_queue_rcv
。在将数据报添加到队列之前,此函数会做一些事情:
sk_filter
用于处理已应用到套接字的所有Berkeley Packet Filter过滤器。sk_rmem_schedule
运行以确保存在足够的接收缓冲区空间来接受此数据报。skb_set_owner_r
。这个增加sk->sk_rmem_alloc
。__skb_queue_tail
。sk_data_ready
通知处理程序函数来进行通知。这就是数据到达系统并遍历网络堆栈直到到达套接字并准备好由用户程序读取的方式。
还有一些值得一提的其他值得一提的东西,这些其他地方似乎都不是很正确。
如以上博客文章所述,网络堆栈可以收集传入数据的时间戳。当与RPS结合使用时,有sysctl值控制何时/如何收集时间戳。有关RPS,时间戳以及在网络堆栈中接收时间戳的确切位置,请参见以上文章。某些NIC甚至还支持在硬件上添加时间戳。
如果您想确定内核网络堆栈为接收数据包增加的延迟时间,则此功能很有用。
关于时间戳的内核文档非常出色,甚至包括一个附带的示例程序和Makefile,您都可以签出!。
确定您的驱动程序和设备支持的时间戳模式ethtool -T
。
- $ sudo ethtool -T eth0
- eth0的时间戳参数:
- 能力:
- 软件传输(SOF_TIMESTAMPING_TX_SOFTWARE)
- 软件接收(SOF_TIMESTAMPING_RX_SOFTWARE)
- 软件系统时钟(SOF_TIMESTAMPING_SOFTWARE)
- PTP硬件时钟:无
- 硬件发送时间戳模式:无
- 硬件接收过滤器模式:无
不幸的是,该NIC不支持硬件接收时间戳,但是仍可以在该系统上使用软件时间戳,以帮助我确定内核添加到数据包接收路径的延迟时间。
可以使用一个名为的套接字选项SO_BUSY_POLL
,当阻塞接收完成并且没有数据时,它将导致内核忙于轮询新数据。
重要说明:为了使此选项起作用,您的设备驱动程序必须支持它。Linux内核3.13.0的igb
驱动程序不支持此选项。该ixgbe
驱动程序,但是,确实。如果您的驱动程序ndo_busy_poll
在其struct net_device_ops
结构字段中设置了功能(在上述博客文章中提到),则它支持SO_BUSY_POLL
。
英特尔提供了一篇很好的文章解释它的工作原理和使用方法。
当将此套接字选项用于单个套接字时,您应该传递以微秒为单位的时间值,作为设备驱动程序的接收队列中新数据忙轮询的时间。设置此值后,如果对此套接字发出阻塞读取,则内核将忙于轮询新数据。
您还可以将sysctl值设置net.core.busy_poll
为一个时间值(以微秒为单位),该时间值还包括忙于轮询poll
或select
等待新数据到达的繁忙轮询的时间。
此选项可以减少延迟,但会增加CPU使用率和功耗。
当内核崩溃时,Linux内核为设备驱动程序提供了一种用于在NIC上发送和接收数据的方法。用于此目的的API称为Netpoll,它有一些用途,但最值得注意的是:kgdb,netconsole。
大多数驱动程序都支持Netpoll。您的驱动程序需要实现该ndo_poll_controller
功能并将其附加到struct net_device_ops
探测期间注册的功能(如上所示)。
当网络设备子系统对传入或传出的数据执行操作时,将首先检查netpoll系统以确定数据包是否以netpoll为目的地。
例如,我们可以看到下面的代码__netif_receive_skb_core
来自net/dev/core.c
:
- static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
- {
-
- /* ... */
-
- /* if we've gotten here through NAPI, check netpoll */
- if (netpoll_receive_skb(skb))
- goto out;
-
- /* ... */
- }
Netpoll检查发生在处理传输或接收网络数据的大多数Linux网络设备子系统代码中。
Netpoll API的使用者可以struct netpoll
通过调用来注册结构netpoll_setup
。该struct netpoll
结构具有用于附加接收挂钩的函数指针,并且API导出了用于发送数据的函数。
如果您对使用Netpoll API感兴趣,则应该看一下netconsole
驱动程序,Netpoll API头文件'include / linux / netpoll.h`以及这个精彩的演讲。
SO_INCOMING_CPU
该SO_INCOMING_CPU
标志直到Linux 3.19才被添加,但是它足够有用,应该将其包含在此博客文章中。
您可以使用getsockopt
该SO_INCOMING_CPU
选项来确定哪个CPU正在处理特定套接字的网络数据包。然后,您的应用程序可以使用此信息将套接字移交给在所需CPU上运行的线程,以帮助增加数据局部性和CPU缓存命中率。
该邮件列表的消息中引入SO_INCOMING_CPU
提供了一个简短的示例体系结构,其中此选项是有用的。
甲DMA引擎是一个硬件,它允许CPU卸载大的复制操作。这可以使CPU腾出时间来执行其他任务,同时使用硬件完成内存复制。启用DMA引擎的使用和运行利用它的代码,应该会减少CPU使用率。
Linux内核具有DMA引擎驱动程序作者可以插入的通用DMA引擎接口。您可以在内核源代码文档中阅读有关Linux DMA引擎接口的更多信息。
尽管内核支持一些DMA引擎,但我们将特别讨论一个非常常见的引擎:Intel IOAT DMA引擎。
英特尔的I / O加速技术(IOAT)
许多服务器都包含Intel I / O AT捆绑包,该捆绑包包含一系列性能更改。
这些更改之一是包含了硬件DMA引擎。您可以检查dmesg
输出ioatdma
以确定是否正在加载模块以及是否找到了受支持的硬件。
DMA卸载引擎在一些地方使用,特别是在TCP堆栈中。
Linux 2.6.18中包含对Intel IOAT DMA引擎的支持,但由于一些不幸的数据损坏错误,后来在3.13.11.10中将其禁用。
ioatdma
默认情况下,使用3.13.11.10之前的内核的用户可能会在其服务器上使用该模块。也许这将在将来的内核版本中修复。
直接缓存访问(DCA)
英特尔I / O AT捆绑包随附的另一个有趣功能是直接缓存访问(DCA)。
此功能允许网络设备(通过其驱动程序)将网络数据直接放置在CPU缓存中。确切地说,这是如何工作的取决于驱动程序。对于igb
驱动程序,您可以检查该功能igb_update_dca
的代码以及的代码igb_update_rx_dca
。该igb
驱动器通过写寄存器值网卡使用DCA。
要使用DCA,您需要确保在BIOS中启用了DCA,dca
已加载模块,并且您的网卡和驱动程序都支持DCA。
监视IOAT DMA引擎
ioatdma
尽管有上述提到的数据损坏风险,但仍在使用该模块,则可以通过检查中的某些条目来对其进行监视sysfs
。
监视memcpy
DMA通道的卸载操作总数。
- $ cat / sys / class / dma / dma0chan0 / memcpy_count
- 123205655
同样,要获取此DMA通道卸载的字节数,可以运行以下命令:
监视为DMA通道传输的字节总数。
- $ cat / sys / class / dma / dma0chan0 / bytes_transferred
- 131791916307
调整IOAT DMA引擎
IOAT DMA引擎仅在数据包大小超过特定阈值时使用。该阈值称为copybreak
。之所以执行此检查,是因为对于小副本,不值得进行加速传输来设置和使用DMA引擎的开销。
用调整DMA引擎复制中断sysctl
。
$ sudo sysctl -w net.ipv4.tcp_dma_copybreak = 2048
默认值为4096。
Linux网络堆栈很复杂。
没有深入了解到底发生了什么,就不可能监视或调整它(或任何其他复杂的软件)。通常,在Internet范围内,您可能会偶然发现一个sysctl.conf
包含一组sysctl值的示例,该值应复制并粘贴到您的计算机上。这可能不是优化网络堆栈的最佳方法。
监视网络堆栈需要仔细考虑每一层的网络数据。从驱动程序开始,然后继续。这样,您可以确定确切的位置和发生错误的位置,然后调整设置以确定如何减少所看到的错误。
不幸的是,没有轻松的出路。
在浏览网络堆栈时需要其他帮助吗?对本文中的任何内容或相关内容有疑问吗?给我们发送电子邮件,让我们知道我们将如何提供帮助。
如果您喜欢此职位,则可以享受我们其他一些低级别的技术职位:
strace
工作如何?ltrace
工作如何?
特别感谢Private Internet Access的员工,他们雇用我们与其他网络研究一起研究此信息,并亲切地允许我们以研究为基础并发布此信息。
这里提供的信息是建立在对所做的工作专用互联网接入,为5部分组成的系列开始,其最初发表在这里。
监视和调整Linux网络协议栈:接收数据:https://blog.packagecloud.io/eng/2016/06/22/monitoring-tuning-linux-networking-stack-receiving-data/#linux-network-device-subsystem
监视和调整Linux网络协议栈:发送数据:https://blog.packagecloud.io/eng/2017/02/06/monitoring-tuning-linux-networking-stack-sending-data/
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。