更多 Linux PCI 驱动程序信息
全面解释 PCI 设备如何工作超出了本文的范围,但这个精彩的演讲,这个 wiki和这个来自 Linux 内核的文件都是很好的资源。
网络设备初始化
igb_probe
函数执行一些重要的网络设备初始化。除了 PCI 特定的工作外,它还执行更多通用的网络和网络设备工作:
- 注册
struct net_device_ops
。 - 注册
ethtool
操作。 - 从 NIC 获取默认 MAC 地址。
- 设置
net_device
特性标志。 - 还有更多。
让我们逐个来看看,它们很有趣。
struct net_device_ops
struct net_device_ops
包含指向许多重要操作的函数指针,网络子系统需要这些操作来控制设备。在本文的其余部分,我们将多次提到这个结构。
net_device_ops
结构被关联到 igb_probe
中的 struct net_device
上。来自 drivers/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
结构保存的指针指向的函数也在同一个文件中设置。 来自 drivers/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
操作的函数,内核负责粘合。
当从 ethtool
发出 ioctl
调用时,内核找到驱动程序注册的 ethtool
结构,并执行已注册的函数。驱动程序的 ethtool
函数实现可以做任何事情,从更改驱动程序中的简单软件标志到向设备写入寄存器值来调整实际 NIC 硬件的工作方式。
igb
驱动程序调用 igb_set_ethtool_ops
在 igb_probe
中注册其 ethtool
操作:
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
结构,其中 igb
驱动程序支持的 ethtool
函数设置为适当的字段。
来自 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 和legacy IRQ。这些将在稍后提及。当数据通过 DMA 写入 RAM 时,设备生成 IRQ 是很简单的,但如果大量数据帧到达,则会生成大量 IRQ。生成的 IRQ 越多,更高级任务(如用户进程)的 CPU 时间就越少。
新 API (NAPI) 被创建为一种减少网络设备在数据包到达时生成的 IRQ 数量的机制。虽然 NAPI 减少了 IRQ 的数量,但不能完全消除它们。
我们将在后面的部分看到为什么会这样。
NAPI
NAPI 与传统的收集数据方法在几个重要方面有所不同。NAPI 允许设备驱动程序注册一个 poll
函数,NAPI 子系统将调用该函数来收集数据帧。
在网络设备驱动程序中,NAPI 的预期用法如下:
- 驱动程序启用 NAPI,但最初处于关闭状态。
- 数据包到达,并由 NIC 通过 DMA 写入内存。
- NIC 生成 IRQ,触发驱动程序中的 IRQ 处理程序。
- 驱动程序使用 softirq(稍后将详细介绍)唤醒 NAPI 子系统。开始在单独的执行线程中调用驱动程序注册的
poll
函数来收集数据包。 - 驱动程序应禁用来自 NIC 的进一步 IRQ。这样做是为了让 NAPI 子系统在没有设备中断的情况下处理数据包。
- 一旦没有更多工作要做,NAPI 子系统被禁用,设备的 IRQ 被重新启用。
- 过程从第 2 步重新开始。
与传统方法相比,这种收集数据帧的方法减少了开销,因为可以一次处理多个数据帧,而无需处理每个数据帧一次 IRQ。
设备驱动程序实现一个 poll
函数并调用 netif_napi_add
将其注册到 NAPI。当使用 netif_napi_add
注册 NAPI poll
函数时,驱动程序还将指定 weight
。大多数驱动程序硬编码一个为 64
的值。这个值及其含义将在下面更详细地描述。
通常,驱动程序在驱动程序初始化期间注册它们的 NAPI poll
函数。
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
。
该调用跟踪发生了一些高级的事情:
- 如果支持 MSI-X,则调用
pci_enable_msix
启用它。 - 计算并初始化各种设置;最值得注意的是设备和驱动程序发送和接收数据包的传输和接收队列的数量。
- 为每个创建的传输和接收队列调用一次
igb_alloc_q_vector
。 - 每次调用
igb_alloc_q_vector
都会调用netif_napi_add
为该队列注册一个poll
函数,当调用以收集数据包时,将传递一个struct napi_struct
实例给poll
。
让我们看一下 igb_alloc_q_vector
,看看如何注册 poll
回调及其私有数据。
来自 drivers/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
的引用(上面的 &q_vector->napi
)。当 NAPI 子系统调用它来从此接收队列收集数据包时,将传递给 igb_poll
。
当我们探讨数据从驱动程序到网络栈的流动时,这一点很重要。
启动网络设备
我们之前看到的 net_device_ops
结构体注册了一组函数,用于启动网络设备、传输数据包、设置 MAC 地址等。
当网络设备启动时(例如,使用 ifconfig eth0 up
),net_device_ops
结构体中的 ndo_open
字段所关联的函数会被调用。
ndo_open
函数通常会执行以下操作:
- 分配接收队列 和传输队列内存
- 启用 NAPI
- 注册中断处理程序
- 启用硬件中断
- 等等。
在 igb
驱动程序的情况下,net_device_ops
结构体中 ndo_open
字段所关联的函数被称为 igb_open
。
准备从网络接收数据
现在大多数网卡都使用 DMA 直接将数据写入 RAM,操作系统可以从中获取数据进行处理。大多数网卡用于此目的的数据结构类似于基于循环缓冲区(或环形缓冲区)构建的队列。
为了做到这一点,设备驱动程序必须与操作系统协作,保留一块网卡硬件可以使用的内存区域。一旦保留了这个区域,就会告知硬件其位置,传入的数据将被写入 RAM,在 RAM 中稍后将被网络子系统拾取并处理。
这看起来很简单,但如果数据包速率足够高,以至于单个 CPU 无法正确处理所有传入的数据包呢?数据结构建立在固定长度的内存区域上,因此传入的数据包将被丢弃。
这就是 接收端扩展 (RSS) 或多队列能够改善的点。
有些设备能够同时将传入的数据包写入几个不同的 RAM 区域;每个区域都是一个单独的队列。这允许操作系统从硬件层面开始,使用多个 CPU 并行处理传入的数据。并非所有网卡都支持此功能。
Intel I350 网卡支持多队列。我们可以在 igb
驱动程序中看到这一点。当 igb
驱动程序启动时,它首先调用一个名为 igb_setup_all_rx_resources
的函数。这个函数为每个 接收队列调用另一个函数 igb_setup_rx_resources
,以安排设备将传入数据写入 DMA支持内存。
如果您想了解这是如何工作的,请参阅 Linux 内核的 DMA API HOWTO。
事实证明,可以使用 ethtool
调整接收队列的数量和大小。调整这些值会对处理的帧数与丢弃的帧数产生明显影响。
网卡使用数据包头字段(如源、目的地、端口等)上的哈希函数来确定数据应该发送到哪个接收队列。
有些网卡允许您调整接收队列的权重,因此您可以向特定队列发送更多流量。
少部分网卡允许您调整哈希函数本身。如果您可以调整哈希函数,您可以发送某些流量到特定的接收队列进行处理,甚至在硬件层面丢弃数据包(如果需要)。
我们稍后将看看如何调整这些设置。
启用NAPI
当网络设备启动时,驱动程序通常会启用 NAPI。
我们之前看到驱动程序如何向 NAPI 注册 poll
函数,但 NAPI 通常不会在设备启动之前启用。
启用 NAPI 相对简单。调用 napi_enable
翻转 struct napi_struct
的一个位,以指示它现在已启用。如上所述,虽然 NAPI 被启用,但它将处于关闭状态。
在 igb
驱动程序的情况下,当驱动程序加载或使用 ethtool
更改队列计数或大小时,会为每个已初始化的 q_vector
启用 NAPI。
来自 drivers/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 和 legacy interrupt。因此,代码因设备而异,具体取决于特定硬件支持的中断方法。
驱动程序必须确定设备支持哪种方法,并注册适当的处理程序函数,在接收到中断时执行。
有些驱动程序,如 igb
驱动程序,会尝试使用每种方法注册中断处理程序,在失败时回退到下一个未测试的方法。
MSI-X 中断是首选方法,特别是对于支持多个接收队列的网卡。这是因为每个接收队列都可以分配自己的硬件中断,然后由特定的 CPU 处理(使用 irqbalance
或修改 /proc/irq/IRQ_NUMBER/smp_affinity
)。我们稍后将看到,处理中断的 CPU 将是处理数据包的 CPU。通过这种方式,从硬件中断层次到网络栈,到达的数据包可以由单独的 CPU 处理。
如果 MSI-X 不可用,MSI 仍然比 legacy interrupt 具有优势。如果设备支持它,驱动程序将使用它。阅读 这个有用的维基页面 了解更多关于 MSI 和 MSI-X 的信息。
在 igb
驱动程序中,函数 igb_msix_ring
、igb_intr_msi
、igb_intr
分别是 MSI-X、MSI 和 legacy interrupt 模式的中断处理程序方法。
您可以在驱动程序中找到尝试每种中断方法的代码drivers/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; }
如上面的简略代码所示,驱动程序首先尝试使用 igb_request_msix
设置 MSI-X 中断处理程序,在失败时回退到 MSI。接下来,使用 request_irq
注册 igb_intr_msi
,即 MSI 中断处理程序。如果这失败了,驱动程序将回退到传统中断。再次使用 request_irq
注册 legacy interrupt 处理程序 igb_intr
。
这就是 igb
驱动程序如何注册一个函数,在网卡引发中断信号表明数据已到达并准备好处理时执行。