4. NAPI 注册与初始化:驱动与内核的握手
NAPI 机制的核心在于将网卡中断处理与轮询收包逻辑绑定。这个绑定过程通过 netif_napi_add 函数完成注册,并通过 napi_enable / napi_disable 控制 NAPI 实例的启用与停用。本章将深入分析这三个关键 API 的实现细节,并展示它们在典型网卡驱动初始化流程中的完整调用链路。
4.1 netif_napi_add:注册 NAPI 实例
netif_napi_add 是驱动向内核网络子系统注册一个 NAPI 实例的入口。该函数将驱动提供的轮询函数(poll function)与一个 napi_struct 结构体绑定,并将其添加到所在网络设备的 NAPI 列表中。
4.1.1 函数原型与参数
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 |
struct napi_struct * |
待初始化的 NAPI 实例指针(通常嵌入在驱动私有结构体中) |
poll |
函数指针 | 驱动实现的轮询回调函数,内核在轮询模式下调用此函数收包 |
weight |
int |
每次轮询可处理的最大包数(典型值为 64) |
4.1.2 核心初始化流程
netif_napi_add 内部执行以下关键步骤:
- 初始化链表节点:将
napi->dev_list链表节点初始化,后续用于挂接到设备的 NAPI 列表。 - 设置轮询函数与权重:将传入的
poll函数指针赋值给napi->poll,将weight赋值给napi->weight。 - 初始化状态标志:将
napi->state初始化为 0,表示 NAPI 实例尚未启用。 - 初始化 GRO 相关字段:清零
napi->gro_list和napi->gro_count,为通用接收卸载(GRO)做准备。 - 挂接到设备:将 NAPI 实例添加到
dev->napi_list链表中。
关键点:
netif_napi_add 只完成注册和初始化,不会使 NAPI 实例立即进入工作状态。驱动必须显式调用 napi_enable 来激活轮询能力。
4.2 napi_enable / napi_disable:启用与停用
这两个函数控制 NAPI 实例的生命周期状态。驱动在设备打开时调用 napi_enable,在设备关闭时调用 napi_disable。
4.2.1 napi_enable 实现分析
void napi_enable(struct napi_struct *napi)
{
BUG_ON(!test_bit(NAPI_STATE_SCHED, &napi->state));
clear_bit(NAPI_STATE_DISABLE, &napi->state);
if (napi->poll)
set_bit(NAPI_STATE_SCHED, &napi->state);
}
关键逻辑:
- 清除禁用标志:将
NAPI_STATE_DISABLE位清零,允许 NAPI 实例被调度。 - 设置调度标志:将
NAPI_STATE_SCHED位置位,表示 NAPI 实例已准备好参与轮询调度。 - 原子操作:使用
test_bit/clear_bit/set_bit保证并发安全。
设计意图:
napi_enable 必须在驱动完成硬件初始化、中断注册之后调用。过早启用可能导致 NAPI 在硬件未就绪时被调度,引发空指针访问。
4.2.2 napi_disable 实现分析
void napi_disable(struct napi_struct *napi)
{
might_sleep();
set_bit(NAPI_STATE_DISABLE, &napi->state);
while (test_and_set_bit(NAPI_STATE_SCHED, &napi->state))
msleep(1);
clear_bit(NAPI_STATE_SCHED, &napi->state);
}
关键逻辑:
- 设置禁用标志:通知内核此 NAPI 实例即将停用。
- 等待轮询结束:通过自旋等待确保当前正在执行的轮询函数完成。
- 清除调度标志:最终将
NAPI_STATE_SCHED清除,彻底停止调度。 - 可睡眠:使用
might_sleep()声明,表明此函数可能阻塞,不能在原子上下文中调用。
| 特性 | napi_enable | napi_disable |
|---|---|---|
| 调用时机 | 设备打开、硬件初始化完成后 | 设备关闭、卸载驱动前 |
| 是否可睡眠 | 否(原子操作) | 是(可能 msleep) |
| 核心操作 | 清除 DISABLE 位,设置 SCHED 位 | 设置 DISABLE 位,等待 SCHED 位释放 |
| 并发安全性 | 位操作保证原子性 | test_and_set_bit + 忙等待 |
4.3 设备驱动中的 NAPI 初始化流程
在真实网卡驱动(如 Intel igb、e1000e、Mellanox mlx5)中,NAPI 的初始化遵循一套标准化的流程。以下以典型 PCIe 网卡驱动为例,展示完整的初始化链路。
4.3.1 完整初始化步骤
- 分配驱动私有结构体:在
probe函数中分配包含napi_struct的私有数据结构。 - 注册 NAPI 实例:调用
netif_napi_add,传入设备指针、NAPI 实例、轮询函数和权重。 - 硬件初始化:配置 DMA 描述符环、分配 RX/TX 缓冲区、设置 MAC 地址等。
- 注册中断处理函数:为网卡分配 MSI-X 中断向量,注册中断处理程序。
- 启用 NAPI:调用
napi_enable,使 NAPI 实例进入可调度状态。 - 开启硬件接收:设置网卡控制寄存器,允许数据包流入。
4.3.2 典型驱动代码片段
// 驱动私有结构体
struct my_priv {
struct napi_struct napi;
struct net_device *netdev;
void __iomem *hw_base;
// ... 其他字段
};
// probe 函数中的 NAPI 初始化
static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct net_device *netdev;
struct my_priv *priv;
// 1. 分配 net_device 和私有结构体
netdev = alloc_etherdev(sizeof(struct my_priv));
priv = netdev_priv(netdev);
// 2. 注册 NAPI,权重设为 64
netif_napi_add(netdev, &priv->napi, my_poll, 64);
// 3. 硬件初始化(DMA 环、缓冲区等)
my_hw_init(priv);
// 4. 注册中断
request_irq(pdev->irq, my_interrupt, 0, "my_driver", priv);
// 5. 启用 NAPI
napi_enable(&priv->napi);
// 6. 开启硬件接收
my_hw_rx_enable(priv);
return 0;
}
// 轮询函数实现
static int my_poll(struct napi_struct *napi, int budget)
{
struct my_priv *priv = container_of(napi, struct my_priv, napi);
int work_done = 0;
// 从 RX 描述符环收包,最多处理 budget 个
work_done = my_rx_process(priv, budget);
if (work_done < budget) {
// 收包完毕,关闭轮询,重新开启中断
napi_complete_done(napi, work_done);
my_enable_interrupts(priv);
}
return work_done;
}
4.3.3 初始化顺序的重要性
常见错误:部分驱动开发者为了简化代码,在硬件初始化完成前就调用
napi_enable。这会导致以下问题:
- 中断触发后,NAPI 轮询函数可能访问未初始化的 DMA 描述符环,引发内核崩溃。
- 在
napi_enable之后、硬件接收开启之前,如果有遗留中断到达,轮询函数将空转,浪费 CPU 周期。
4.4 多队列网卡的 NAPI 初始化
现代多队列网卡(如 Intel XL710、Mellanox ConnectX-5)为每个 RX 队列分配独立的 NAPI 实例,实现队列级别的并行收包。
4.4.1 多 NAPI 实例注册
// 为每个 RX 队列注册独立的 NAPI
for (int i = 0; i < num_queues; i++) {
struct my_queue *q = &priv->queues[i];
netif_napi_add(netdev, &q->napi, queue_poll, 64);
napi_enable(&q->napi);
}
| 特性 | 单队列 NAPI | 多队列 NAPI |
|---|---|---|
| NAPI 实例数 | 1 | 等于 RX 队列数 |
| 中断亲和性 | 单个 CPU | 可绑定不同 CPU |
| 并行度 | 低 | 高(多 CPU 并行收包) |
| 轮询函数 | 处理所有队列 | 每个队列独立处理 |
| 适用场景 | 低端嵌入式设备 | 高性能服务器 |
4.4.2 队列与 NAPI 的绑定关系
4.5 本章小结
- netif_napi_add 完成 NAPI 实例的注册与初始化,将驱动轮询函数与内核调度框架绑定。
- napi_enable 激活 NAPI 实例,使其可被内核调度;napi_disable 安全停用 NAPI,等待当前轮询结束。
- 驱动初始化必须遵循严格的顺序:硬件初始化 → 中断注册 → NAPI 启用 → 硬件接收开启。
- 多队列网卡为每个 RX 队列注册独立 NAPI 实例,实现 CPU 间的并行收包,提升吞吐量。