TL;DR
本文解释了 Linux 内核的计算机如何接收数据包,以及当数据包从网络流向用户程序时,如何监视和调优网络栈的每个组件。
更新 我们已经发布了本文的姊妹篇:监控和调优 Linux 网络栈:发送数据。
更新 查看 监控和调优 Linux 网络栈图解指南:接收数据,它为下面的内容添加了一些图表。
如果不阅读内核的源代码,不深入了解到底发生了什么,就不可能调优或监控 Linux 网络栈。
希望本文能给想做这方面工作的人提供参考。
特别感谢
特别感谢 Private Internet Access 的工作人员雇用我们,结合其他网络研究进行进一步研究,并慷慨地允许以研究为基础发布这些信息。
本文基于为 Private Internet Access 所做的工作,最初以 5 部分的系列文章的形式发表。
监控和调优 Linux 网络栈的一般建议
Linux 网络栈是复杂的,没有一刀切的监控或调优解决方案。 如果您真的想调优网络栈,您别无选择,只能投入大量的时间、精力和金钱来了解网络系统的各个部分是如何交互的。
理想情况下,您应该考虑在网络栈的每一层测量数据包丢弃。 这样您就可以确定并缩小需要调优的组件的范围。
这就是我认为许多运营商偏离轨道的地方:假设一组 sysctl 设置或 /proc
值可以简单地被大规模重用。在某些情况下,也许可以,但事实证明,整个系统是如此微妙和交织在一起,如果您希望有意义的监控或调优,您必须努力深入了解系统如何运作。否则,您可以直接使用默认设置,在必要的进一步优化(以及推导这些设置所需的投资)之前,已经足够好。
本文中提供的许多示例设置仅用于说明目的,并不是对某个配置或默认设置的推荐或反对。 在调整任何设置之前,您应该围绕您需要监控的内容制定一个参考框架,以注意到有意义的变化。
通过网络连接到计算机时调整网络设置是危险的;你很容易地把自己锁在外面,或者完全关闭你的网络。 不要在生产机器上调整这些设置;相反,如果可能的话,在新机器上进行调整,再投入生产中。
概览
作为参考,您可能需要手边有一份设备数据手册。 这篇文章将研究由 igb
设备驱动程序控制的 Intel I350 以太网控制器。 您可以找到该数据手册(警告:大型 PDF)供您参考。
数据包从到达到套接字接收缓冲区的流程概览:
- 驱动程序已加载并初始化。
- 数据包从网络到达 NIC。
- 数据包被复制(通过 DMA)到内核内存中的环形缓冲区。
- 产生硬件中断通知系统知道数据包到达内存。
- 驱动程序调用 NAPI 启动轮询循环(如果尚未运行轮询循环)。
ksoftirqd
进程运行在系统的每个 CPU 上。 它们在启动时注册。ksoftirqd
进程调用设备驱动程序在初始化期间注册的 NAPIpoll
函数,从环形缓冲区收取数据包。- 环形缓冲区中已写入网络数据的内存区域被取消映射。
- DMA 到内存的数据以 “skb” 向上传递到网络层,以进行更多处理。
- 如果 packet steering 启用或 NIC 具有多个接收队列,则传入的网络数据帧将分布在多个CPU 中。
- 网络数据帧从队列传递到协议层。
- 协议层处理数据。
- 协议层添加数据到套接字关联的接收缓冲区。
整个流程将在以下各节中详细介绍。
下面检查的协议层是IP和UDP协议层。 本文提供的许多信息也将作为其他协议层的参考。
详细探讨
本文将探讨 Linux 3.13.0 版本内核,贯穿全文提供了 GitHub 代码链接和代码片段。
准确理解 Linux 内核如何接收数据包是非常复杂的。 我们需要仔细检查和理解网络驱动程序是如何工作的,以便更加清晰理解后面的网络栈部分。
本文将介绍 igb
网络驱动程序。 此驱动程序用于相对常见的服务器 NIC,即 Intel Ethernet Controller I350。 那么,让我们从理解 igb
网络驱动程序的工作原理开始。
网络设备驱动程序
初始化
驱动程序注册一个初始化函数,当驱动程序被加载时,内核会调用该函数。 此函数使用module_init
宏注册。
igb
初始化函数(igb_init_module
)及其与 module_init
的注册可以在 drivers/net/ethernet/intel/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
的宏(来自 include/module.h
)来导出一个 PCI 设备 ID 表,标识设备驱动程序可以控制的设备。该表注册为一个结构的一部分,我们稍后将看到。
内核使用此表来确定要加载哪个设备驱动程序来控制设备。
这就是操作系统如何确定哪些设备连接到系统,以及应该使用哪个驱动程序与设备通信。
此表和 igb
驱动程序的 PCI 设备 ID 位于 drivers/net/ethernet/intel/igb/igb_main.c
和 drivers/net/ethernet/intel/igb/e1000_hw.h
:
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 系统中注册了一个探测函数。内核为尚未被设备驱动程序认领的设备调用此函数。一旦设备被认领,不会再就该设备询问其他驱动程序。大多数驱动程序都有大量的代码运行,以使设备做好使用准备。所做的确切事情因驱动程序而异。
要执行的一些典型操作包括:
- 启用 PCI 设备。
- 请求内存范围和 IO 端口。
- 设置 DMA 掩码。
- 注册驱动程序支持的 ethtool 函数(下面将详细描述)。
- 启动看门狗任务(例如,e1000e 有一个看门狗任务来检查硬件是否挂起)。
- 其他设备相关的内容,如替代方法或处理硬件特定的状况之类。
- 创建、初始化和注册
struct net_device_ops
结构。此结构包含指向打开设备、发送数据到网络、设置 MAC 地址等各种函数的函数指针。 - 创建、初始化和注册抽象
struct net_device
,表示网络设备。
让我们快速看一下 igb
驱动程序中 igb_probe
函数的一些操作。
PCI 初始化一瞥
下面的 igb_probe
函数代码执行一些基本的 PCI 配置。 来自drivers/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_BIT_MASK(64)
调用 dma_set_mask_and_coherent
。
调用 pci_request_selected_regions
保留内存区域,启用 PCI Express 高级错误报告(如果加载了 PCI AER 驱动程序),调用 pci_set_master
启用 DMA,并调用 pci_save_state
保存 PCI 配置空间。