Table of Contents

有关监视和调整Linux网络协议栈的建议

总览

详细外观

网络设备驱动程序

初始化

网络设备初始化

启动网络设备

监控网络设备

调整网络设备

SoftIRQ

什么是softirq?

ksoftirqd

__do_softirq

监控方式

Linux网络设备子系统

网络设备子系统的初始化

数据到达

网络数据处理开始

通用接收卸载(GRO)

napi_gro_receive

napi_skb_finish

接收数据包导向(RPS)

调优:启用RPS

接收流量控制(RFS)

调优:启用RFS

硬件加速的接收流控制(aRFS)

调整:启用加速的RFS(aRFS)

向上移动网络堆栈 netif_receive_skb

调优:RX数据包时间戳记

netif_receive_skb

没有RPS(默认设置)

启用RPS

积压队列NAPI轮询器

process_backlog

__netif_receive_skb_core 将数据传送到数据包抽头和协议层

数据包分流交付

协议层传送

协议层注册

IP协议层

更高级别的协议注册

UDP协议层

将数据排队到套接字

附加功能

时间戳记

低延迟套接字的繁忙轮询

Netpoll:在关键情况下支持联网

SO_INCOMING_CPU

DMA引擎

结论

帮助Linux联网或其他系统

相关文章

特别感谢


这篇博客文章解释了运行Linux内核的计算机如何接收数据包,以及如何在数据包从网络流向用户域程序时监视和调整网络堆栈的每个组件。

更新我们已经发布了与之对应的文章:监视和调整Linux网络堆栈:发送数据。

更新请看英文原文《监控和调整Linux网络堆栈:接收数据的插图指南》或中文译文《监控和调整Linux网络协议栈的图解指南:接收数据》,其中为以下信息添加了一些图表。

如果不阅读内核的源代码并且对正在发生的事情有深刻的了解,就不可能调整或监视Linux网络堆栈。

希望该博客文章可以为希望这样做的任何人提供参考。

有关监视和调整Linux网络协议栈的建议


更新我们已经发布了与之对应的文章:监视和调整Linux网络堆栈:发送数据。

更新请看《监控和调整Linux网络堆栈:接收数据的插图指南》,其中为以下信息添加了一些图表。

网络协议栈很复杂,没有一种适合所有解决方案的规模。如果网络的性能和健康状况对您或您的业务至关重要,那么您将别无选择,只能投入大量的时间,精力和金钱来了解系统各部分之间的交互方式。

理想情况下,您应该考虑测量网络堆栈每一层的丢包率。这样,您可以确定并缩小需要调整的组件。

我认为,这是许多操作员偏离轨道的地方:假定一组sysctl设置或/proc值可以简单地批量重用。也许在某些情况下,但事实证明,整个系统是如此细微和纠缠在一起,以至于如果您希望进行有意义的监视或调整,则必须努力理解系统的深层功能。否则,您可以简单地使用默认设置,该默认设置应该足够好,直到需要进一步优化(以及进行这些设置所需的投资)为止。

本博客文章中提供的许多示例设置仅用于说明目的,不建议或反对某些配置或默认设置。在调整任何设置之前,您应该围绕需要监视的内容建立参考框架,以注意到有意义的更改。

通过网络连接到机器时调整网络设置很危险;您可以轻松地将自己锁定在外,或者完全断开网络连接。不要在生产机器上调整这些设置;而是在可能的情况下对新机器进行调整,然后将其轮换投入生产。

总览


作为参考,您可能希望随身携带一份器件数据手册。这篇文章将检查由igb设备驱动程序控制的Intel I350以太网控制器。您可以在此处找到该数据表(警告:大PDF)以供参考。

数据包从到达到达套接字接收缓冲区的高级路径如下:

  1. 驱动程序已加载并初始化。
  2. 数据包从网络到达NIC。
  3. 数据包被复制(通过DMA)到内核内存中的环形缓冲区。
  4. 产生硬件中断以使系统知道内存中有一个数据包。
  5. 如果尚未运行,驱动程序将调用NAPI以启动轮询循环。
  6. ksoftirqd进程在系统上的每个CPU上运行。它们在引导时注册。该ksoftirqd过程通过调用NAPI拉断的环形缓冲器的数据包poll,该设备驱动器初始化期间注册的函数。
  7. 环形缓冲区中已写入网络数据的内存区域未映射。
  8. DMA存入内存的数据作为“ skb”传递到网络层,以进行更多处理。
  9. 如果启用了数据包控制或NIC具有多个接收队列,则传入的网络数据帧将在多个CPU之间分配。
  10. 网络数据帧从队列传递到协议层。
  11. 协议层处理数据。
  12. 数据被添加以接收协议层附加到套接字的缓冲区。

在以下各节中将详细检查整个流程。

下面检查的协议层是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.cdrivers/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系统中注册一个探测功能。内核为尚未被设备驱动程序要求保护的设备调用此函数。索取设备版权后,将不会再向其他驱动程序询问该设备。大多数驱动程序都有很多代码,可以运行以使设备准备就绪。实际执行的操作因驱动程序而异。

要执行的一些典型操作包括:

  1. 启用PCI设备。
  2. 请求内存范围和IO端口。
  3. 设置DMA掩码。
  4. 驱动程序支持的ethtool(将在下面进行更多描述)功能已注册。
  5. 需要的所有看门狗任务(例如,e1000e都有一个看门狗任务来检查硬件是否已挂起)。
  6. 其他特定于设备的内容,例如解决方法或处理特定于硬件的怪癖或类似内容。
  7. 结构的创建,初始化和注册struct net_device_ops。该结构包含指向打开设备,将数据发送到网络,设置MAC地址等所需的各种功能的功能指针。
  8. 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特定的工作之外,它还将执行更多的常规联网和网络设备工作:

  1. struct net_device_ops注册。
  2. ethtool 操作已注册。
  3. 从NIC获得默认的MAC地址。
  4. net_device 功能标志已设置。
  5. 还有更多。

让我们看一下其中的每一个,因为稍后它们会很有趣。


struct net_device_ops

struct net_device_ops包含函数指针,这些指针指向网络子系统控制设备所需的许多重要操作。在本文的其余部分中,我们将多次提及此结构。

net_device_ops结构附加到struct net_devicein中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_openndo_stopndo_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允许设备驱动程序注册pollNAPI子系统将调用以收集数据帧的功能。

NAPI在网络设备驱动程序中的预期用途如下:

  1. NAPI由驱动程序启用,但最初处于关闭位置。
  2. 数据包到达并由NIC DMA到内存。
  3. NIC会生成一个IRQ,它会在驱动程序中触发IRQ处理程序。
  4. 驱动程序使用softirq唤醒NAPI子系统(稍后会详细介绍)。这将通过poll在单独的执行线程中调用驱动程序的注册函数来开始收集数据包。
  5. 驱动程序应禁用来自NIC的其他IRQ。这样做是为了允许NAPI子系统在不中断设备的情况下处理数据包。
  6. 一旦没有更多工作要做,NAPI子系统将被禁用,设备的IRQ将被重新启用。
  7. 该过程从步骤2开始。

与传统方法相比,这种收集数据帧的方法减少了开销,因为一次可以消耗许多数据帧,而不必一次处理一个IRQ。

设备驱动程序实现一个poll功能,并通过调用将其注册到NAPI netif_napi_add。向其注册NAPI poll函数时netif_napi_add,驱动程序还将指定weight。大多数驱动程序将值硬编码为64。该值及其含义将在下面更详细地描述。

通常,驱动程序poll在驱动程序初始化期间注册其NAPI 函数。


igb驱动程序中的NAPI初始化

igb驱动器通过一个长调用链做到这一点:

  1. igb_probe来电igb_sw_init
  2. igb_sw_init来电igb_init_interrupt_scheme
  3. igb_init_interrupt_scheme来电igb_alloc_q_vectors
  4. igb_alloc_q_vectors来电igb_alloc_q_vector
  5. igb_alloc_q_vector来电netif_napi_add

此调用跟踪导致发生一些高级事件:

  1. 如果支持MSI-X,将通过调用启用它pci_enable_msix
  2. 计算并初始化各种设置;最值得注意的是,设备和驱动程序将用于发送和接收数据包的发送和接收队列的数量。
  3. igb_alloc_q_vector 将为每个将创建的发送和接收队列调用一次。
  4. 每次调用都会为该队列注册一个函数,igb_alloc_q_vector调用netif_napi_addpoll函数的实例struct napi_structpoll在调用以获取数据包时传递给该函数。

让我们看一下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函数通常会执行以下操作:

  1. 分配RX和TX队列内存
  2. 启用NAPI
  3. 注册一个中断处理程序
  4. 启用硬件中断
  5. 和更多。

对于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_ringigb_intr_msiigb_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_msiMSI中断处理程序。如果失败,驱动程序将退回传统中断。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:597028087tx_packets:5924278060rx_bytes:112643393747tx_bytes:990080156714rx_broadcast:96tx_broadcast:116rx_multicast:20294528....

监视此数据可能很困难。它很容易获得,但是没有字段值的标准化。不同的驱动程序,甚至同一驱动程序的不同版本,可能会产生具有相同含义的不同字段名称。

您应该在标签中查找带有“ drop”,“ buffer”,“ miss”等的值。接下来,您将必须阅读驱动程序源。您将能够确定哪些值完全由软件计算(例如,当没有内存时增加),以及哪些值直接通过寄存器读取来自硬件。如果是寄存器值,则应查阅硬件的数据手册,以确定计数器的真正含义。通过给出的许多标签ethtool可能会产生误导。


使用sysfs

sysfs还提供了许多统计信息值,但是它们比提供的直接NIC级别统计信息略高一些。

您可以通过cat在文件上使用来找到丢失的传入网络数据帧的数量,例如eth0 。

使用sysfs监视更高级别的NIC统计信息。

$ cat / sys / class / net / eth0 / statistics / rx_dropped
2

计数器值将被拆分后的文件一样collisionsrx_droppedrx_errorsrx_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 0lo: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队列之间的网络数据分布的功能。

您可以在以下情况下进行配置:

  • 您的NIC支持流间接访问。
  • 你的驱动程序实现的ethtool功能get_rxfh_indir_sizeget_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服务器的以下配置:

  • 在端口80上运行的Web服务器被固定为在CPU 2上运行。
  • RX队列的IRQ分配给CPU 2处理。
  • 目的地为端口80的TCP流量通过ntuple被“过滤”到CPU 2。
  • 然后,从端口2到数据到达CPU时,CPU 2将处理所有进入端口80的传入通信。
  • 为了确定有效性,需要对系统进行仔细的监视,包括高速缓存命中率和网络堆栈延迟。

如前所述,可以使用来配置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_matchfdir_miss计算ntuple过滤规则的匹配和未命中次数。请查阅设备驱动程序源和设备数据表,以跟踪统计信息计数器(如果有)。

SoftIRQ


在检查网络堆栈之前,我们需要走一小段弯路,以检查Linux内核中称为SoftIRQ的内容。

什么是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_runrun_ksoftirqd

这两个函数都从kernel / smpboot.c调用,作为类似于事件循环的一部分。

将执行kernel/smpboot.c第一次调用中的代码,该代码ksoftirqd_should_run确定是否有任何待处理的softirq,以及是否有待处理的softirq run_ksoftirqd。在run_ksoftirqd调用之前做了一些小的簿记__do_softirq

__do_softirq

__do_softirq函数做了一些有趣的事情:

  • 确定哪个softirq待处理
  • softirq时间用于统计目的
  • softirq执行统计信息增加
  • 执行待处理的softirq(已通过调用注册open_softirq)的softirq处理程序。

因此,当您查看CPU使用率图并看到softirqsi现在知道这正在衡量延迟工作上下文中发生的CPU使用量时。

监控方式


/proc/softirqs

softirq系统的增量统计计数器可以被读取/proc/softirqs监控这些统计数据可以给你在哪个正在生成各种活动软中断的速度感。

通过阅读检查softIRQ统计信息/proc/softirqs

$ cat / proc / softirqsCPU0 CPU1 CPU2 CPU3HI:0 0 0 0计时器:2831512516 1337085411 1103326083 1423923272NET_TX:15774435 779806 733217 749512NET_RX:1671622615 1257853535 2088429526 2674732223区块:1800253852 1466177 1791366 634534
BLOCK_IOPOLL:0 0 0 0任务:25 0 0 0SCHED:2642378225 1711756029 629040543 682215771HRTIMER:2547911 2046898 1558136 1521176RCU:2056528783 4231862865 3545088730 844379888

该文件可以使您了解当前网络如何在NET_RX各个CPU上分配()处理。如果分布不均,则某些CPU的计数值会比其他CPU大。这是一个指标,您可能可以从下面描述的接收数据包控制/接收流控制中受益。监视性能时,请仅使用此文件:在网络活动频繁的时期,您会期望速率NET_RX增加,但这并非一定如此。事实证明,这有点微妙,因为网络堆栈中还有其他调整旋钮,这些旋钮可能会影响NET_RXsoftirq触发的速率,我们将很快看到。

但是,您应该意识到这一点,这样,如果您调整其他调音旋钮,便会知道要检查/proc/softirqs并期望看到变化。

现在,让我们继续到网络堆栈,并跟踪从上到下如何接收网络数据。

Linux网络设备子系统


现在,我们已经了解了网络驱动程序和softirq的工作方式,让我们看看如何初始化Linux网络设备子系统。然后,我们可以按照数据包到达的路径进行跟踪。

网络设备子系统的初始化

网络设备(netdev)子系统在函数中初始化net_dev_init。在此初始化函数中发生了很多有趣的事情。

struct softnet_data结构初始化

net_dev_initstruct softnet_data为系统上的每个CPU 创建一组结构。这些结构将保存一些指向处理网络数据的重要信息的指针:

  • 要向该CPU注册的NAPI结构的列表。
  • 积压的数据处理。
  • 的加工weight
  • 在收到卸载结构清单。
  • 接收数据包控制设置。
  • 和更多。

在我们逐步研究堆栈时,将更详细地检查每一个。

初始化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_SOFTIRQsoftirq 的函数。

数据到达

终于 网络数据到来!

假设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_schedulenapi_schedule实际上只是一个在头文件中定义的包装函数,它调用__napi_schedule

从net / core / dev.c:

/*** __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);
}

这段代码做了两件事:

  1. 所述struct napi_struct从设备驱动程序的中断处理程序代码手向上被添加到poll_list附接至softnet_data与当前CPU相关联的结构。
  2. __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 / interruptsCPU0 CPU1 CPU2 CPU30:46 0 0 0 IR-IO-APIC边沿计时器1:3 0 0 0 IR-IO-APIC边缘i804230:3361234770 0 0 0 IR-IO-APIC-fasteoi aacraid64:0 0 0 0 DMAR_MSI边缘dmar065:1 0 0 0 IR-PCI-MSI-edge eth066:863649703 0 0 0 IR-PCI-MSI-edge eth0-TxRx-067:986285573 0 0 0 IR-PCI-MSI-edge eth0-TxRx-168:45 0 0 0 IR-PCI-MSI-edge eth0-TxRx-269:394 0 0 0 IR-PCI-MSI-edge eth0-TxRx-3NMI:9729927 4008190 3068645 3375402不可屏蔽的中断LOC:2913290785 1585321306 1495872829 1803524526本地计时器中断

您可以监视其中的统计信息,/proc/interrupts以查看随着数据包到达而导致的硬件中断的数量和速率如何变化,并确保由适当的CPU处理NIC的每个RX队列。正如我们很快就会看到,这个数字只告诉我们的硬件中断了多少事,但它并不一定是好的度量了解有多少数据已经收到或处理尽可能多的驱动程序将禁用网卡的IRQ作为其合同的一部分NAPI子系统。此外,使用中断合并也将影响从该文件收集的统计信息。监视此文件可以帮助您确定所选的中断合并设置是否确实有效。

为了更全面地了解您的网络处理运行状况,您需要监控/proc/softirqs(如上所述)和其他文件(/proc我们将在下面介绍)。


调整网络数据到达

中断合并

中断合并是一种防止设备在特定数量的工作或事件数量挂起之前引发中断的方法。

这可以帮助防止中断风暴,并可以帮助提高吞吐量或延迟,具体取决于所使用的设置。生成的中断更少,从而提高了吞吐量,增加了延迟并降低了CPU使用率。产生更多的中断会产生相反的结果:较低的延迟,较低的吞吐量,但也会增加CPU使用率。

从历史上看,早期版本的igbe1000和其他因素包括一个名为参数的支持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_CPUSirqbalance告知它不应触摸您要为其分配的一组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函数可以消耗的工作量和执行时间。它以两种方式执行此操作:

  1. 通过跟踪工作budget(可以调整),以及
  2. 检查经过时间

从net / core / dev.c:

  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使用率(特别是在程序中sitimesitop其他程序中),但应减少延迟,因为将更迅速地处理数据。

注意:无论分配的预算如何,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处理循环中使用它。

从net / core / dev.c:

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

因此,假设:

  1. 您使用64驱动程序的权重(在Linux 3.13.0中,所有驱动程序都使用该值进行了硬编码),并且
  2. 您已将其budget设置为默认值300

在以下情况之一下,系统将停止处理数据:

  1. igb_poll函数最多调用了5次(如果没有数据要处理,将减少调用次数),或者
  2. 至少已经过了2分钟的时间。

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处理:

  1. 网络设备应关闭(例如,因为用户已运行ifconfig eth0 down),
  2. 如果设备没有关闭,请检查是否有通用接收卸载(GRO)列表。如果计时器滴答速率大于等于1000,则将刷新所有最近更新的GRO网络流。稍后我们将详细探讨GRO。将NAPI结构移到该CPU的列表末尾,以便循环的下一次迭代将注册下一个NAPI结构。

这就是数据包处理循环如何调用驱动程序的注册poll函数来处理数据包的方式。稍后我们将看到,该poll函数将收集网络数据并将其发送到堆栈进行处理。

达到极限时退出循环

net_rx_action在以下任一情况下,循环将退出:

  • 为此CPU注册的轮询列表不再具有NAPI结构(!list_empty(&sd->poll_list)),或者
  • 剩余预算为<= 0,或
  • 已达到2次抖动的时间限制

这是我们之前再次看到的代码:

/* 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;
}

这段代码做了一些有趣的事情:

  • 如果在内核中启用了直接缓存访问(DCA)支持,则会预热CPU缓存,以便对RX环的访问将达到CPU缓存。您可以在此博文末尾的Extras部分中了解有关DCA的更多信息。
  • 接下来,igb_clean_rx_irq称为,它会进行繁重的工作,接下来我们将看到。
  • 接下来,clean_complete检查以确定是否还有更多可以完成的工作。如果是这样,则返回budget(请记住,这已被硬编码为64)。如前所述,net_rx_action将这个NAPI结构移动到轮询列表的末尾。
  • 否则,驱动程序将通过调用关闭NAPI,napi_complete并通过调用重新启用中断igb_ring_irq_enable。下一个到达的中断将重新启用NAPI。

让我们看看如何igb_clean_rx_irq将网络数据发送到堆栈。

igb_clean_rx_irq

igb_clean_rx_irq功能是一个循环,一次处理一个数据包,直到budget达到该数据包或没有其他数据可处理为止。

此函数中的循环执行一些重要的操作:

  1. 当清除使用过的缓冲区时,分配其他缓冲区以接收数据。IGB_RX_BUFFER_WRITE一次添加(16)附加缓冲区。
  2. 从RX队列中获取缓冲区并将其存储在skb结构中。
  3. 检查缓冲区是否为“数据包结尾”缓冲区。如果是这样,请继续处理。否则,请继续从RX队列中提取其他缓冲区,并将其添加到中skb。如果接收到的数据帧大于缓冲区大小,则这是必需的。
  4. 验证数据的布局和标题正确。
  5. 统计计数器处理的字节数增加skb->len
  6. 设置skb的哈希,校验和,时间戳,VLAN ID和协议字段。哈希,校验和,时间戳和VLAN ID由硬件提供。如果硬件发信号通知校验和错误,则csum_error统计量会增加。如果校验和成功,并且数据是UDP或TCP数据,则将skb标记为CHECKSUM_UNNECESSARY。如果校验和失败,则协议栈留给该数据包处理。协议是通过调用eth_type_trans并存储在skb结构中来计算的。
  7. 构造skb的程序通过调用移交给网络堆栈napi_gro_receive
  8. 统计的计数器处理的数据包数量增加。
  9. 循环继续进行,直到处理的数据包数量达到预算为止。

循环终止后,该函数将为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如前所述增加可以减少这种情况。
  • 接下来的5个值始终为0。
  • 第九个值,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)


通用接收卸载(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)。

接收数据包导向(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

为了使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)


接收流控制(RFS)与RPS结合使用。RPS尝试在多个CPU之间分配传入的数据包负载,但并未考虑任何数据局部性问题来最大化CPU缓存命中率。您可以使用RFS通过将相同流的数据包定向到同一CPU进行处理来帮助提高缓存命中率。

调优:启用RFS

为了使RFS正常工作,您必须启用并配置RPS。

RFS跟踪所有流的全局哈希表,并且可以通过设置net.core.rps_sock_flow_entriessysctl 来调整此哈希表的大小。

通过设置来增加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'

硬件加速的接收流控制(aRFS)


借助硬件加速可以加快RFS;NIC和内核可以一起工作,以确定应该在哪些CPU上处理哪些流。要使用此功能,NIC和您的驱动程序必须支持它。

请查阅NIC的数据表,以确定是否支持此功能。如果您的NIC驱动程序公开了一个名为的函数ndo_rx_flow_steer,则该驱动程序支持加速的RFS。

调整:启用加速的RFS(aRFS)

假设您的NIC和驱动程序支持它,则可以通过启用和配置以下各项来启用加速的RFS:

  1. 已启用和配置RPS。
  2. 启用并配置RFS。
  3. 您的内核已CONFIG_RFS_ACCEL在编译时启用。Ubuntu内核3.13.0可以。
  4. 如前所述,已为设备启用ntuple支持。您可以ethtool用来验证是否已为设备启用ntuple支持。
  5. 配置您的IRQ设置,以确保每个RX队列由所需的网络处理CPU之一处理。

完成上述配置后,加速的RFS将用于自动将数据移动到与正在处理该流的数据的CPU内核绑定的RX队列,并且您无需为每个流手动指定ntuple过滤规则。

向上移动网络堆栈 netif_receive_skb

netif_receive_skb从几个地方调用,然后从我们停下来的地方接机。最常见的两种(以及我们已经讨论过的两种):

  • napi_skb_finish 如果数据包不打算合并到现有的GRO流中,或者
  • napi_gro_complete 如果协议层指示是时候冲洗流了,或者

提醒: netif_receive_skb及其后代均在softirq处理循环的上下文中运行,您将看到在此处花费的时间与sitimesi使用诸如之类的工具有关top

netif_receive_skb首先,首先检查一个sysctl值以确定用户是否在数据包到达积压队列之前或之后请求接收时间戳。如果启用此设置,则在达到RPS(和CPU的相关积压队列)之前,现在先给数据加上时间戳。如果禁用此设置,它将在打入队列后加上时间戳。如果启用了RPS,则可用于在多个CPU之间分配时间戳的负载,但结果会带来一些延迟。

调优:RX数据包时间戳记

您可以通过调整名为的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(默认设置)

如果未启用RPS,__netif_receive_skb将调用进行簿记,然后调用__netif_receive_skb_core将数据移到协议栈附近。

我们将精确地了解其__netif_receive_skb_core工作原理,但首先让我们了解启用RPS的代码路径如何工作,因为该代码也会调用__netif_receive_skb_core

启用RPS

如果启用了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足够小并且没有达到(或禁用)流量限制(可以禁用),则可以将数据排队。这里的逻辑有点有趣,但可以总结为:

  • 如果队列为空:检查是否在远程CPU上启动了NAPI。如果不是,请检查是否已排队发送IPI。如果不是,请排队一个并通过调用启动NAPI处理循环____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_weightsysctl 来调整积压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启用了流量限制。

积压队列NAPI轮询器

每个CPU的待办事项队列以与设备驱动程序相同的方式插入NAPI。甲poll设置功能,用于处理数据包从软中断上下文。weight也提供了A ,就像设备驱动程序一样。

在网络系统初始化期间会提供此NAPI结构。从net_dev_initnet/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_schedulefrom 的调用重新启动轮询器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;elsereturn &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}

通过调用来将过滤器添加到列表中dev_add_pack。这就是协议层如何为其协议类型注册自身以进行网络数据传递的方法。

现在您知道了网络数据如何从NIC到达协议层。

协议层注册

现在我们知道如何将数据从网络设备子系统传递到协议栈,让我们看看协议层如何注册自己。

这篇博客文章将研究IP协议栈,因为它是一种常用协议,并且与大多数读者相关。

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_deliverdst_entry数据包结构中的输入功能指针上。

调整:调整IP协议早期多路分配

early_demux通过设置禁用优化sysctl

$ sudo sysctl -w net.ipv4.ip_early_demux = 0

预设值为1;early_demux已启用。

添加此系统是因为某些用户在某些情况下通过优化发现吞吐量降低了5%early_demux

ip_local_deliver

回想一下我们如何在IP协议层中看到以下模式:

  1. 要求进行ip_rcv一些初始簿记。
  2. 数据包将传递给netfilter进行处理,并带有指向处理完成时要执行的回调的指针。
  3. 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为该协议注册的结构,并调用handlernet_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

一些有趣的统计数据:

  • InReceivesip_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协议层。如上所示,handlerUDP 的功能称为udp_rcv

这是IP层处理数据的UDP层的入口点。让我们继续那里的旅程。

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

此功能的初始部分如下:

  1. 确定与数据报关联的套接字是否是封装套接字。如果是这样,请在继续之前将数据包传递到该层的处理程序功能。
  2. 确定数据报是否为UDP-Lite数据报,并进行一些完整性检查。
  3. 验证数据报的UDP校验和,如果校验和失败,则将其丢弃。

最后,我们到达接收队列逻辑,首先检查套接字的接收队列是否已满。来自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_rcvbufsk->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_rcvbufsk_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。您可以设置的最大值setsockoptnet.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如果无法将数据报添加到套接字的接收队列中,则增加统计计数器。

从net / ipv4 / udp.c:

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协议的详细统计信息非常相似,您将需要阅读协议层源来确定何时以及在何处增加这些值。

  • InDatagramsrecvmsg由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。因此,InErrorsInCsumErros应该在接收端产生与内存相关的错误计数。

/proc/net/udp

通过阅读监视UDP套接字统计信息 /proc/net/udp

$ cat / proc / net / udpsl 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 0558:00000000:0371 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7408 2 0000000000000000 0588:0100007F:038F 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7511 2 0000000000000000 0769:00000000:0044 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7673 2 0000000000000000 0812:00000000:006F 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 7407 2 0000000000000000 0

第一行描述了以下各行中的每个字段:

  • sl:套接字的内核哈希槽
  • local_address:套接字的十六进制本地地址和端口号,以分隔:
  • rem_address:套接字的十六进制远程地址和端口号,以分隔:
  • st:套接字的状态。奇怪的是,UDP协议层似乎使用了某些TCP套接字状态。在上面的示例中,7TCP_CLOSE
  • tx_queue:在内核中为传出UDP数据报分配的内存量。
  • rx_queue:内核中为传入的UDP数据报分配的内存量。
  • trtm->whenretrnsmt:这些字段是通过UDP协议层使用。
  • uid:创建此套接字的用户的有效用户ID。
  • timeout:UDP协议层未使用。
  • inode:与此套接字对应的索引节点号。您可以使用它来帮助您确定哪个用户进程打开了此套接字。检查/proc/[pid]/fd,其中将包含指向的符号链接socket[:inode]
  • ref:套接字的当前引用计数。
  • pointer:内核中的内存地址struct sock
  • drops:与此套接字关联的数据报丢弃数。请注意,这不包括与发送数据报有关的任何丢弃(在已塞好的UDP套接字上或以其他方式);从本博客文章所检查的内核版本开始,该值仅在接收路径中递增。

可以在中找到net/ipv4/udp.c输出此代码的代码。

将数据排队到套接字


网络数据通过调用排队到套接字中sock_queue_rcv。在将数据报添加到队列之前,此函数会做一些事情:

  1. 检查套接字分配的内存,以确定它是否已超过接收缓冲区的大小。如果是这样,套接字的丢弃计数将增加。
  2. Next sk_filter用于处理已应用到套接字的所有Berkeley Packet Filter过滤器。
  3. sk_rmem_schedule 运行以确保存在足够的接收缓冲区空间来接受此数据报。
  4. 接下来,通过调用来向套接字收取数据报的大小skb_set_owner_r。这个增加sk->sk_rmem_alloc
  5. 通过调用将数据添加到队列中__skb_queue_tail
  6. 最终,任何等待数据到达套接字的进程都将通过调用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为一个时间值(以微秒为单位),该时间值还包括忙于轮询pollselect等待新数据到达的繁忙轮询的时间。

此选项可以减少延迟,但会增加CPU使用率和功耗。

Netpoll:在关键情况下支持联网

当内核崩溃时,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才被添加,但是它足够有用,应该将其包含在此博客文章中。

您可以使用getsockoptSO_INCOMING_CPU选项来确定哪个CPU正在处理特定套接字的网络数据包。然后,您的应用程序可以使用此信息将套接字移交给在所需CPU上运行的线程,以帮助增加数据局部性和CPU缓存命中率。

该邮件列表的消息中引入SO_INCOMING_CPU提供了一个简短的示例体系结构,其中此选项是有用的。

DMA引擎

甲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

监视memcpyDMA通道的卸载操作总数。

$ 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值的示例,该值应复制并粘贴到您的计算机上。这可能不是优化网络堆栈的最佳方法。

监视网络堆栈需要仔细考虑每一层的网络数据。从驱动程序开始,然后继续。这样,您可以确定确切的位置和发生错误的位置,然后调整设置以确定如何减少所看到的错误。

不幸的是,没有轻松的出路。

帮助Linux联网或其他系统


在浏览网络堆栈时需要其他帮助吗?对本文中的任何内容或相关内容有疑问吗?给我们发送电子邮件,让我们知道我们将如何提供帮助。

相关文章


如果您喜欢此职位,则可以享受我们其他一些低级别的技术职位:

  • 监视和调整Linux网络堆栈:发送数据
  • Linux系统调用权威指南
  • strace工作如何?
  • ltrace工作如何?
  • APT哈希总和不匹配
  • HOWTO:GPG签署并验证Deb软件包和APT存储库
  • HOWTO:GPG签署并验证RPM软件包和yum存储库

特别感谢


特别感谢Private Internet Access的员工,他们雇用我们与其他网络研究一起研究此信息,并亲切地允许我们以研究为基础并发布此信息。

这里提供的信息是建立在对所做的工作专用互联网接入,为5部分组成的系列开始,其最初发表在这里。


监视和调整Linux网络协议栈:接收数据:https://blog.packagecloud.io/eng/2016/06/22/monitoring-tuning-linux-networking-stack-receiving-data/#linux-network-device-subsystem

图标:https://blog.packagecloud.io/eng/2016/10/11/monitoring-tuning-linux-networking-stack-receiving-data-illustrated/

监视和调整Linux网络协议栈:发送数据:https://blog.packagecloud.io/eng/2017/02/06/monitoring-tuning-linux-networking-stack-sending-data/

监视和调整Linux网络协议栈:接收数据相关推荐

  1. 监视和调整Linux网络协议栈:发送数据

    目录 有关监视和调整Linux网络堆栈的一般建议 总览 详细外观 协议族注册 通过套接字发送网络数据 sock_sendmsg,__sock_sendmsg和__sock_sendmsg_nosec ...

  2. 监控和调整Linux网络协议栈的图解指南:接收数据

    Table of Contents 入门 最初设定 数据到达 网络数据处理开始 网络数据处理继续 协议栈和用户态套接字 结论 监视和调整Linux网络协议栈:接收数据(图解):https://rtoa ...

  3. linux内核协议栈接收数据流程(一)

    linux网络接收数据第一站--网卡驱动 linux网络接收数据流程的第一站为网卡驱动,网卡接收包流程大致为: 网卡硬件接收到包,会将数据包通过DMA映射到预先分配好的ringbuffer内存环形缓存 ...

  4. linux网络协议栈之数据包处理过程,Linux网络协议栈之数据包处理过程

    这篇文档是基于 x86 体系结构和转发 IP 分组的. 数据包在 Linux 内核链路层路径 接收分组 1 接收中断 如果网卡收到一个和自己 MAC 地址匹配或链路层广播的以太网帧,它就会产生一个中断 ...

  5. Linux网络协议栈:关闭一个还有没发送数据完的TCP连接

    <监视和调整Linux网络协议栈:接收数据> <监控和调整Linux网络协议栈的图解指南:接收数据> <Linux网络 - 数据包的接收过程> <Linux网 ...

  6. Linux网络协议栈:网络包接收过程

    目录 一 Linux网络收包总览 二 Linux启动 2.1 创建ksoftirqd内核线程 2.2 网络子系统初始化 2.3 协议栈注册 2.4 网卡驱动初始化 2.5 启动网卡 三 迎接数据的到来 ...

  7. Linux网络协议栈:中断下半部处理

    <Linux中断处理:上半部和下半部> <Linux网络协议栈:中断下半部处理> 目录 数据包上送 网络中断下半部处理 总结 推荐阅读 在<Linux网络协议栈:网络包接 ...

  8. Linux网络包接收过程的监控与调优

    Linux内核对网络包的接收过程大致可以分为接收到RingBuffer.硬中断处理.ksoftirqd软中断处理几个过程.其中在ksoftirqd软中断处理中,把数据包从RingBuffer中摘下来, ...

  9. Linux网络协议栈:一个TCP链接的耗时

    <一次系统调用开销到底有多大?strace.time.perf命令> 目录 一 正常TCP连接建立过程 二 TCP连接建立时的异常情况 1)客户端connect系统调用耗时失控 2)半/全 ...

最新文章

  1. postman登录鉴权之接口测试
  2. jupyter % 符号用法
  3. 波士顿动力机器狗再进化:已学会自主规划路线
  4. 【最新】三位深度学习创始人共同获得了2019年公布的图灵奖
  5. 14.并发与异步 - 2.任务Task -《果壳中的c#》
  6. 如何不用BPM配置时间
  7. 接口测试——jemter生成HTML测试报告
  8. UIView Methods
  9. Linux给驱动模块传参数(module_param()用法)
  10. 织梦响应式酒店民宿住宿类网站织梦模板(自适应手机端)
  11. 计算机四级网络工程师笔记
  12. 万物皆可秒——淘宝秒杀Python脚本,扫货618,备战双11!
  13. Controller中使用swagger注解的正确姿势
  14. coverity代码检测工具介绍_Coverity功能介绍
  15. Oracle 日期相减获取年龄
  16. 科学家研发真实版的《星际迷航》牵引光束
  17. springboot+Vue+Elementui医院网上预约挂号系统java项目
  18. Google浏览器密码框自动弹出账号密码的解决方法
  19. AIS(自动识别系统)介绍
  20. CLR的主要作用有哪些

热门文章

  1. android studio中的适配器,如何在Android Studio中测试回收器视图适配器
  2. 【正交幅度调制 QAM】
  3. scrap连接django
  4. PHP Socket编程(转)
  5. jQuery使用示例详解
  6. VMware虚拟机下实现Linux与window文件夹共享
  7. INPUT只能输入数字
  8. Ruby中的字符串与符号
  9. 为Windows Phone SDK 模拟器安装应用
  10. iphone 调试技巧