3. NAPI 的初始化与注册
NAPI 机制的启用始于驱动初始化阶段。在设备被内核发现并分配 net_device 结构体之后,驱动必须在 probe 回调中完成 NAPI 实例的创建与注册。本章将深入剖析 netif_napi_add() 函数的原型、参数含义、注册时机以及多队列场景下的注册策略。
3.1 netif_napi_add() 函数原型与参数详解
netif_napi_add() 是内核提供给驱动注册 NAPI 实例的核心 API,定义在 <linux/netdevice.h> 中。其函数原型如下:
void netif_napi_add(struct net_device *dev,
struct napi_struct *napi,
int (*poll)(struct napi_struct *, int),
int weight);
四个参数的含义与使用要点如下表所示:
| 参数 | 类型 | 说明 | 注意事项 |
|---|---|---|---|
dev |
struct net_device * |
指向该 NAPI 实例所属的网络设备 | 必须已分配且非 NULL;多队列场景下所有 NAPI 实例共享同一个 dev |
napi |
struct napi_struct * |
指向待初始化的 NAPI 实例结构体 | 通常嵌入在驱动私有数据结构中,由 kzalloc 分配或静态定义 |
poll |
int (*)(struct napi_struct *, int) |
轮询回调函数,负责从硬件接收队列中读取数据并上送协议栈 | 返回值表示本次轮询处理的报文数量;必须小于等于 weight |
weight |
int |
单次轮询预算,即 poll 回调最多可处理的报文数 | 典型值为 64;对于纯 NAPI 驱动,weight 不应超过 NAPI_POLL_WEIGHT (64) |
weight 参数决定了单次软中断上下文中驱动最多能消耗的 CPU 时间片。设置过大会导致其他网络流被饿死,设置过小则降低收包吞吐量。内核社区推荐值为 64,该值在吞吐量与公平性之间取得了良好平衡。
调用 netif_napi_add() 后,内核会完成以下初始化工作:
- 将
napi->dev指向传入的dev; - 将
napi->poll和napi->weight赋值为传入参数; - 初始化 NAPI 实例的链表节点、状态标志位和就绪队列;
- 将 NAPI 实例添加到设备
dev->napi_list链表中,完成注册。
netif_napi_add() 之前,驱动应确保 napi_struct 结构体已被清零(推荐使用 kzalloc 分配),否则未初始化的链表指针可能导致内核崩溃。
3.2 NAPI 实例的注册时机:驱动 probe 阶段
NAPI 实例的注册必须发生在设备初始化流程的 probe 阶段,即在 struct net_device_ops 中的 ndo_open 被调用之前。典型的注册时序如下:
- 分配 net_device:调用
alloc_etherdev()或alloc_netdev()分配网络设备结构体。 - 设置硬件参数:读取 PCI 配置空间、映射 BAR 空间、初始化 DMA 队列等。
- 分配驱动私有数据:在私有结构中为每个 RX 队列分配
napi_struct实例。 - 调用 netif_napi_add():为每个 RX 队列注册 NAPI 实例。
- 注册 net_device:调用
register_netdev()将设备注册到内核网络子系统。
以下是一个典型的驱动 probe 函数片段:
static int mydrv_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct net_device *ndev;
struct mydrv_priv *priv;
int i, ret;
// 1. 分配 net_device
ndev = alloc_etherdev(sizeof(struct mydrv_priv));
if (!ndev)
return -ENOMEM;
pci_set_drvdata(pdev, ndev);
priv = netdev_priv(ndev);
// 2. 初始化硬件(省略具体实现)
ret = mydrv_init_hw(pdev, priv);
if (ret)
goto err_free_netdev;
// 3. 为每个 RX 队列注册 NAPI
for (i = 0; i < priv->num_rx_queues; i++) {
netif_napi_add(ndev, &priv->napi[i], mydrv_poll, 64);
}
// 4. 注册网络设备
ret = register_netdev(ndev);
if (ret)
goto err_free_napi;
return 0;
err_free_napi:
// 清理已注册的 NAPI 实例
for (i = 0; i < priv->num_rx_queues; i++)
netif_napi_del(&priv->napi[i]);
err_free_netdev:
free_netdev(ndev);
return ret;
}
ndo_open 中才调用 netif_napi_add(),这会导致设备注册后、接口 up 前,NAPI 实例尚未就绪。若此时内核触发链路事件或统计查询,可能访问未初始化的 NAPI 链表,引发空指针解引用。务必在 register_netdev() 之前完成 NAPI 注册。
3.3 多队列网卡的 NAPI 注册策略
现代高速网卡普遍支持多 RX 队列(RSS / 多队列),每个队列可以独立触发中断并拥有独立的 NAPI 实例。多队列场景下的 NAPI 注册策略直接影响 CPU 负载均衡和收包性能。
3.3.1 一对一策略:每队列独立 NAPI
这是最常用的策略,每个 RX 硬件队列对应一个独立的 napi_struct 实例。中断亲和性(IRQ affinity)将每个队列的中断绑定到不同的 CPU 核心,实现并行收包。
// 一对一注册示例
for (i = 0; i < priv->num_rx_queues; i++) {
netif_napi_add(ndev, &priv->napi[i], mydrv_poll, 64);
// 后续通过 irq_set_affinity_hint 将队列 i 的中断绑定到 CPU i
}
3.3.2 多对一策略:多队列共享 NAPI
在低端硬件或虚拟化场景中,多个 RX 队列可以共享同一个 NAPI 实例。此时 poll 回调需要遍历所有共享队列,从每个队列中收取报文。该策略减少了 NAPI 实例数量,但牺牲了并行度。
// 多对一注册示例(所有队列共享一个 NAPI)
netif_napi_add(ndev, &priv->napi, mydrv_poll_multi, 64);
// poll 回调中遍历所有队列
static int mydrv_poll_multi(struct napi_struct *napi, int budget)
{
struct mydrv_priv *priv = container_of(napi, struct mydrv_priv, napi);
int work_done = 0;
int i;
for (i = 0; i < priv->num_rx_queues; i++) {
work_done += mydrv_poll_queue(&priv->rx_queue[i], budget - work_done);
if (work_done >= budget)
break;
}
return work_done;
}
3.3.3 策略对比
| 策略 | NAPI 实例数 | 并行度 | 适用场景 | 中断亲和性要求 |
|---|---|---|---|---|
| 一对一(每队列独立 NAPI) | 等于 RX 队列数 | 高,每个队列可运行在不同 CPU | 高性能物理网卡、多核服务器 | 必须为每个队列设置独立 CPU 亲和性 |
| 多对一(共享 NAPI) | 1 | 低,所有队列串行处理 | 嵌入式设备、虚拟网卡、队列数少的场景 | 不需要,所有中断共享同一 CPU |
| 分组共享(N 对 M) | 介于 1 和队列数之间 | 中等 | 需要平衡性能和资源消耗的场景 | 每组绑定一个 CPU |
weight 值设置为 64,配合 gro 和 tso 卸载特性,可获得最佳吞吐表现。
3.4 注册后的清理与反注册
在驱动卸载(remove 回调)或设备热拔时,必须使用 netif_napi_del() 反注册所有 NAPI 实例。该函数将 NAPI 实例从设备的 napi_list 中移除,并确保没有正在执行的 poll 回调。
static void mydrv_remove(struct pci_dev *pdev)
{
struct net_device *ndev = pci_get_drvdata(pdev);
struct mydrv_priv *priv = netdev_priv(ndev);
int i;
unregister_netdev(ndev);
// 反注册所有 NAPI 实例
for (i = 0; i < priv->num_rx_queues; i++)
netif_napi_del(&priv->napi[i]);
free_netdev(ndev);
}
netif_napi_del() 必须在 unregister_netdev() 之后调用,以确保设备已从内核网络子系统中移除,不会有新的中断或软中断触发该 NAPI 实例。同时,调用 netif_napi_del() 前应确保所有中断已被禁用,且 NAPI 实例已处于停止状态(通过 napi_disable() 实现)。