3. NAPI 初始化与注册:核心 API 解析
NAPI 机制的启动始于驱动初始化阶段。驱动必须完成两个关键步骤:调用 netif_napi_add() 将 NAPI 实例注册到内核,再通过 napi_enable() 激活轮询能力。本节将深入解析这三个 API 的语义、参数、使用场景及常见陷阱。
3.1 netif_napi_add():NAPI 实例的注册
netif_napi_add() 是 NAPI 初始化的入口函数,负责分配并初始化 struct napi_struct,将其挂入网络设备的 NAPI 链表。该函数通常在驱动 probe() 或 open() 回调中调用。
函数原型与参数
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 与该设备绑定 |
napi |
struct napi_struct * |
驱动预先分配的 NAPI 实例指针(通常嵌入私有结构体) |
poll |
函数指针 | 轮询回调函数,内核在软中断上下文中调用 |
weight |
int |
单次轮询最大处理报文数,典型值 64 |
关键约束:
weight 值不能超过 NAPI_POLL_WEIGHT(内核默认 64)。过大的 weight 会导致软中断独占 CPU,造成其他任务饥饿。对于高速网卡(如 25G/100G),建议保持 64 或根据实测调低。
内部数据结构关系
调用 netif_napi_add() 后,内核完成以下操作:
- 初始化
napi->poll和napi->weight - 将
napi加入dev->napi_list双向链表 - 设置
napi->dev回指设备 - 初始化
napi->poll_list,用于挂入 CPU 的轮询队列 - 清除
NAPI_STATE_SCHED标志位
驱动最佳实践:将
struct napi_struct 嵌入驱动私有结构体(如 struct my_adapter),而非单独分配。这样可保证 NAPI 实例与硬件队列一一对应,便于多队列扩展。
struct my_adapter {
struct net_device *netdev;
struct napi_struct rx_napi; // 嵌入 NAPI 实例
// ... 其他成员
};
static int my_probe(struct pci_dev *pdev, ...)
{
struct my_adapter *adapter;
// ... 分配 adapter 和 netdev
netif_napi_add(adapter->netdev, &adapter->rx_napi,
my_poll, 64);
return 0;
}
3.2 NAPI 注册流程与数据结构
下图展示了 netif_napi_add() 执行后,NAPI 实例与内核子系统之间的静态关系:
3.3 napi_enable() 与 napi_disable():生命周期管理
注册 NAPI 实例后,驱动需要通过 napi_enable() 和 napi_disable() 控制其运行状态。这两个函数管理 NAPI_STATE_SCHED 和 NAPI_STATE_DISABLE 标志位,确保轮询与硬件操作同步。
函数语义对比
| 函数 | 原型 | 核心行为 | 调用时机 |
|---|---|---|---|
napi_enable() |
void napi_enable(struct napi_struct *n) |
清除 NAPI_STATE_DISABLE 标志,允许 napi_schedule() 将实例加入轮询队列 |
probe() 末尾、open() 中、链路恢复后 |
napi_disable() |
void napi_disable(struct napi_struct *n) |
设置 NAPI_STATE_DISABLE,等待当前轮询退出,禁止新调度 |
close()、suspend()、移除设备前 |
重要:
napi_disable() 是 阻塞调用。如果当前 CPU 正在执行该 NAPI 实例的 poll 回调,napi_disable() 会忙等待直到 poll 返回。因此,绝不能在 poll 回调自身或持有自旋锁的上下文中调用 napi_disable(),否则会导致死锁。
典型生命周期序列
- probe():分配资源 →
netif_napi_add()→napi_enable() - open():开启硬件中断 →
napi_enable()(若 probe 时未启用) - 正常运行:ISR 调用
napi_schedule()→ 软中断执行 poll - close():关闭硬件中断 →
napi_disable()→ 释放资源 - remove():
napi_disable()→netif_napi_del()→ 释放内存
驱动代码示例
static int my_open(struct net_device *netdev)
{
struct my_adapter *adapter = netdev_priv(netdev);
// 1. 开启硬件中断
my_irq_enable(adapter);
// 2. 启用 NAPI
napi_enable(&adapter->rx_napi);
// 3. 启动 TX 队列
netif_tx_start_all_queues(netdev);
return 0;
}
static int my_close(struct net_device *netdev)
{
struct my_adapter *adapter = netdev_priv(netdev);
// 1. 停止 TX 队列
netif_tx_stop_all_queues(netdev);
// 2. 禁用 NAPI(阻塞,等待 poll 退出)
napi_disable(&adapter->rx_napi);
// 3. 关闭硬件中断
my_irq_disable(adapter);
return 0;
}
3.4 napi_enable/napi_disable 使用场景对照
| 场景 | 操作 | 原因 |
|---|---|---|
| 设备 probe 完成 | napi_enable() |
使 NAPI 实例可被调度,准备接收数据 |
| 接口 up (open) | napi_enable() |
确保中断开启前 NAPI 已就绪 |
| 接口 down (close) | napi_disable() |
防止中断触发调度,安全停止轮询 |
| 系统 suspend | napi_disable() |
设备进入低功耗,禁止 DMA 和中断 |
| 系统 resume | napi_enable() |
恢复设备后重新激活 NAPI |
| 链路 down (LSC) | napi_disable() |
避免无数据时空转轮询 |
| 链路 up (LSC) | napi_enable() |
恢复数据接收能力 |
| 设备移除 (remove) | napi_disable() + netif_napi_del() |
完整清理 NAPI 实例 |
3.5 常见错误与避坑指南
错误 1:在
后果:调度请求被忽略,数据包丢失。
修复:确保中断处理函数中先检查
napi_disable() 之后调用 napi_schedule()。后果:调度请求被忽略,数据包丢失。
修复:确保中断处理函数中先检查
NAPI_STATE_DISABLE 标志。
错误 2:多次调用
后果:引用计数混乱,可能导致 NAPI 永远无法禁用。
修复:严格遵循 enable/disable 配对调用。
napi_enable() 而不配对 napi_disable()。后果:引用计数混乱,可能导致 NAPI 永远无法禁用。
修复:严格遵循 enable/disable 配对调用。
错误 3:在 poll 回调中调用
后果:死锁——
修复:使用工作队列或 tasklet 延迟执行 disable 操作。
napi_disable()。后果:死锁——
napi_disable() 等待 poll 返回,而 poll 在等待 disable 完成。修复:使用工作队列或 tasklet 延迟执行 disable 操作。
3.6 多队列场景下的 NAPI 初始化
现代网卡支持多个 RX 队列,每个队列应拥有独立的 NAPI 实例。初始化模式如下:
struct my_adapter {
struct net_device *netdev;
struct my_ring *rx_ring;
int num_rx_queues;
struct napi_struct *napi; // 动态数组
};
static int my_probe(struct pci_dev *pdev, ...)
{
struct my_adapter *adapter;
int i;
adapter->napi = kcalloc(adapter->num_rx_queues,
sizeof(struct napi_struct), GFP_KERNEL);
for (i = 0; i < adapter->num_rx_queues; i++) {
netif_napi_add(adapter->netdev, &adapter->napi[i],
my_poll, 64);
napi_enable(&adapter->napi[i]);
}
return 0;
}
每个 NAPI 实例绑定到特定 RX 队列的中断,实现 RSS(接收端缩放)与 NAPI 的协同工作。
3.7 本章小结
netif_napi_add()注册 NAPI 实例,指定 poll 回调和 weight 权重napi_enable()激活 NAPI,允许中断触发调度napi_disable()阻塞禁用 NAPI,确保轮询安全停止- 多队列场景下每个 RX 队列独立 NAPI 实例
- 严格遵循 enable/disable 配对,避免在 poll 中调用 disable