3. NAPI初始化与注册
NAPI 的初始化与注册是设备驱动启用 NAPI 模式的第一步。驱动必须在探测(probe)阶段完成 NAPI 实例的创建、与网络设备的绑定以及使能操作,之后内核才能通过 NAPI 调度机制调用驱动的轮询回调。本章详细拆解 napi_init()、netif_napi_add()、napi_enable()/napi_disable() 四个核心接口,并给出完整的驱动初始化流程。
3.1 napi_init():NAPI 实例的底层初始化
napi_init() 是 NAPI 结构体最底层的初始化函数,通常在驱动分配 struct napi_struct 后立即调用。该函数完成以下工作:
- 将
poll_list链表头初始化为空链表 - 将
state字段清零,清除所有标志位 - 初始化
timer(用于低延迟模式下的定时轮询) - 将
napi_id置为 0(后续由netif_napi_add()分配)
函数原型:
void napi_init(struct napi_struct *napi);
参数 napi 指向驱动已分配的 NAPI 实例。该函数不分配内存,只做字段清零和链表初始化。
驱动开发者需要注意:napi_init() 不会将 NAPI 实例与任何网络设备关联,也不会使能 NAPI 调度。它只是一个“干净的起点”,后续必须通过 netif_napi_add() 完成绑定。
3.2 netif_napi_add():NAPI 与网络设备的绑定
netif_napi_add() 是 NAPI 初始化流程中最关键的注册函数。它将 NAPI 实例与 struct net_device 绑定,并注册驱动的轮询回调函数。绑定完成后,内核的 NAPI 调度器才能识别并调度该实例。
函数原型:
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_list 链表中 |
napi |
struct napi_struct * |
已通过 napi_init() 初始化的 NAPI 实例指针 |
poll |
函数指针 | 驱动的轮询回调函数,内核在调度 NAPI 时调用此函数收取数据包 |
weight |
int |
每次轮询允许处理的最大数据包数量,典型值为 64 |
weight 参数选择建议: 对于千兆网卡,weight 通常设为 64;万兆网卡可设为 128 或更高。weight 过小会导致频繁调度增加 CPU 开销,过大则可能造成单次轮询时间过长,影响其他软中断的响应。
netif_napi_add() 内部执行的关键步骤:
- 调用
napi_init()确保 NAPI 实例已初始化(防御性编程) - 将
napi->dev指向dev - 将
napi->poll指向驱动提供的poll函数 - 设置
napi->weight为传入的 weight 值 - 将 NAPI 实例添加到
dev->napi_list链表中 - 分配全局唯一的
napi_id,用于 IRQ 亲和性跟踪
3.3 napi_enable() / napi_disable():NAPI 的使能与禁用
NAPI 实例在绑定设备后默认处于禁用状态,必须调用 napi_enable() 才能参与内核调度。这两个函数控制 NAPI 实例的生命周期开关。
3.3.1 napi_enable()
函数原型:
void napi_enable(struct napi_struct *napi);
napi_enable() 执行的操作:
- 清除
NAPI_STATE_SCHED标志位,允许内核调度该 NAPI 实例 - 清除
NAPI_STATE_NPSVC标志位(非抢占安全上下文标志) - 如果 NAPI 实例配置了低延迟模式(busy poll),还会初始化相关定时器
注意:
napi_enable() 必须在驱动使能硬件中断 之前 调用。如果先开中断再使能 NAPI,中断处理函数中调用 napi_schedule() 时 NAPI 可能尚未就绪,导致调度失败或状态不一致。
3.3.2 napi_disable()
函数原型:
void napi_disable(struct napi_struct *napi);
napi_disable() 执行的操作:
- 设置
NAPI_STATE_DISABLE标志位,阻止新的调度请求 - 等待当前正在执行的
poll回调完成(忙等待,会睡眠) - 清除
NAPI_STATE_SCHED标志位 - 如果启用了低延迟模式,删除相关定时器
| 特性 | napi_enable() | napi_disable() |
|---|---|---|
| 调用时机 | 驱动探测完成、开中断之前 | 驱动卸载、关中断之后 |
| 是否可睡眠 | 否(原子上下文安全) | 是(可能等待 poll 完成) |
| 对调度的影响 | 允许 NAPI 被调度 | 阻止新调度,等待当前调度结束 |
| 典型调用者 | 驱动 probe / 恢复(resume) | 驱动 remove / 挂起(suspend) |
3.4 设备驱动中的 NAPI 初始化完整流程
将上述三个步骤串联起来,形成设备驱动中 NAPI 初始化的标准流程。以下是一个典型网卡驱动的 probe 函数中 NAPI 初始化的代码模板:
static int mynic_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct net_device *netdev;
struct mynic_priv *priv;
int ret;
// 1. 分配 net_device
netdev = alloc_etherdev(sizeof(struct mynic_priv));
if (!netdev)
return -ENOMEM;
priv = netdev_priv(netdev);
priv->pdev = pdev;
priv->netdev = netdev;
// 2. 初始化 PCI 设备、DMA、寄存器映射等硬件资源
ret = mynic_hw_init(pdev);
if (ret)
goto err_free_netdev;
// 3. 初始化 NAPI 实例(底层清零)
napi_init(&priv->napi);
// 4. 注册 NAPI:绑定设备、轮询回调、weight
netif_napi_add(netdev, &priv->napi, mynic_poll, 64);
// 5. 注册网络设备(此时 NAPI 仍处于禁用状态)
ret = register_netdev(netdev);
if (ret)
goto err_del_napi;
// 6. 使能 NAPI(必须在开中断之前)
napi_enable(&priv->napi);
// 7. 使能硬件中断(此时 NAPI 已就绪)
mynic_irq_enable(priv);
return 0;
err_del_napi:
netif_napi_del(&priv->napi);
err_free_netdev:
free_netdev(netdev);
return ret;
}
流程要点:
napi_init()→netif_napi_add()→napi_enable()顺序不可颠倒napi_enable()必须在register_netdev()之后、开中断之前调用- 卸载时顺序相反:关中断 →
napi_disable()→netif_napi_del()→unregister_netdev()
3.5 常见错误与避坑指南
错误 1:napi_enable() 在开中断之后调用
后果:中断处理函数中调用
解决方案:严格按照“NAPI 使能 → 开中断”的顺序执行。
后果:中断处理函数中调用
napi_schedule() 时,NAPI 实例尚未使能,调度请求被忽略,数据包无法被轮询处理。解决方案:严格按照“NAPI 使能 → 开中断”的顺序执行。
错误 2:netif_napi_add() 重复调用
后果:同一个 NAPI 实例被多次添加到
解决方案:每个 NAPI 实例只调用一次
后果:同一个 NAPI 实例被多次添加到
dev->napi_list,导致链表出现循环或重复节点,内核遍历时崩溃。解决方案:每个 NAPI 实例只调用一次
netif_napi_add(),或在调用前检查 napi->dev 是否为空。
错误 3:卸载时未调用 napi_disable() 直接释放内存
后果:如果 NAPI 轮询正在执行,释放 NAPI 实例内存会导致内核访问已释放的内存,引发 use-after-free 错误。
解决方案:卸载顺序必须为
后果:如果 NAPI 轮询正在执行,释放 NAPI 实例内存会导致内核访问已释放的内存,引发 use-after-free 错误。
解决方案:卸载顺序必须为
napi_disable() → netif_napi_del() → 释放内存。
3.6 本章小结
| 函数 | 核心作用 | 调用顺序 |
|---|---|---|
napi_init() |
清零 NAPI 实例,初始化链表和定时器 | 第 1 步 |
netif_napi_add() |
绑定设备、注册轮询回调、设置 weight | 第 2 步 |
napi_enable() |
使能 NAPI 调度,允许内核调用 poll | 第 3 步(开中断前) |
napi_disable() |
禁用 NAPI 调度,等待当前 poll 完成 | 卸载时第 1 步(关中断后) |