2. 网络设备驱动基础:struct net_device 结构体详解、NAPI 轮询机制、中断与轮询的权衡
网络设备驱动是内核网络协议栈的基石,而 struct net_device 则是驱动与协议栈之间的核心桥梁。本章将深入剖析该结构体的关键字段、NAPI 轮询机制的设计原理,以及在实际工程中如何权衡中断与轮询模式。
2.1 struct net_device 结构体详解
struct net_device 是 Linux 内核中用于描述和管理网络设备的巨型结构体(定义在 include/linux/netdevice.h),它包含了设备的状态、操作函数、统计数据、硬件特征等数百个字段。面试中常考察的核心字段可归纳为以下几类:
| 分类 | 字段名 | 说明 | 面试关注点 |
|---|---|---|---|
| 标识与名称 | char name[IFNAMSIZ] |
设备名称,如 eth0、lo | 命名规则、重命名机制 |
| 状态与标志 | unsigned long state |
设备状态位,如 __LINK_STATE_START | 状态机转换、与用户态 ifconfig 的对应 |
| 操作函数集 | const struct net_device_ops *netdev_ops |
驱动实现的 ndo_open/ndo_stop/ndo_start_xmit 等 | 数据发送路径、回调注册时机 |
| NAPI 相关 | struct napi_struct napi_list |
设备关联的 NAPI 实例链表 | 多队列 NAPI 绑定、轮询权重 |
| 特征与能力 | netdev_features_t features |
硬件 offload 能力位图,如 TSO/GRO/LRO | 特性协商、与协议栈交互 |
| 统计数据 | struct rtnl_link_stats64 stats |
收发包计数、错误计数等 | 原子更新、procfs 导出 |
| 队列管理 | struct netdev_queue *ingress_queue |
入向队列(用于 ifb 等) | 多队列调度、XDP 重定向 |
面试高频点:面试官常问“
net_device_ops 中的 ndo_start_xmit 函数返回 NETDEV_TX_OK 或 NETDEV_TX_BUSY 时,协议栈如何处理?”——返回 BUSY 时,协议栈会将 skb 重新入队并稍后重试,驱动应避免频繁返回 BUSY 导致性能下降。
2.2 NAPI 轮询机制
NAPI(New API)是 Linux 内核为高性能网络设备设计的混合中断-轮询机制,旨在解决传统中断模式在高速网络下的“中断风暴”问题。其核心思想是:在数据流量较低时使用中断唤醒处理,在流量较高时切换到轮询模式批量收包。
2.2.1 NAPI 工作流程
- 中断触发:硬件收到数据包后触发 IRQ,驱动中断处理函数调用
napi_schedule()。 - 关闭中断并加入轮询列表:
napi_schedule()将设备的 NAPI 实例添加到当前 CPU 的softnet_data轮询链表中,并关闭硬件中断。 - 软中断调度:触发 NET_RX_SOFTIRQ 软中断,内核开始遍历轮询链表。
- 轮询回调:调用驱动注册的
poll()函数,驱动从硬件队列中批量读取数据包并送入协议栈。 - 预算控制:
poll()函数返回实际处理的数据包数量,不能超过预设的budget(通常为 64)。 - 退出条件:若处理完所有数据包(返回 < budget),则重新开启硬件中断;否则继续轮询。
关键数据结构:
struct napi_struct 包含 poll() 函数指针、weight(轮询权重)、state(NAPI_STATE_SCHED 等标志位)以及 dev_list 链表节点。
2.2.2 NAPI 驱动注册示例
// 驱动初始化时注册 NAPI
static int my_driver_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct net_device *ndev;
struct my_priv *priv;
ndev = alloc_etherdev(sizeof(struct my_priv));
priv = netdev_priv(ndev);
// 初始化 NAPI 实例,weight 通常设为 64
netif_napi_add(ndev, &priv->napi, my_poll, 64);
// 注册 net_device_ops
ndev->netdev_ops = &my_netdev_ops;
register_netdev(ndev);
return 0;
}
// 中断处理函数
static irqreturn_t my_interrupt(int irq, void *dev_id)
{
struct net_device *ndev = dev_id;
struct my_priv *priv = netdev_priv(ndev);
// 禁止硬件中断(由 NAPI 管理)
// ...
// 调度 NAPI 轮询
if (napi_schedule_prep(&priv->napi)) {
__napi_schedule(&priv->napi);
}
return IRQ_HANDLED;
}
// 轮询回调函数
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;
// 从硬件队列批量读取数据包
while (work_done < budget) {
struct sk_buff *skb = my_read_packet(priv);
if (!skb)
break;
// 送入协议栈
napi_gro_receive(napi, skb);
work_done++;
}
// 如果处理完所有包,退出轮询并重新开启中断
if (work_done < budget) {
napi_complete_done(napi, work_done);
// 重新开启硬件中断
my_enable_irq(priv);
}
return work_done;
}
2.3 中断与轮询的权衡
在高速网络场景下,中断模式和纯轮询模式各有优劣。NAPI 通过动态切换实现了两者的平衡,但工程师仍需根据硬件特性和业务负载进行调优。
| 对比维度 | 传统中断模式 | 纯轮询模式 | NAPI 混合模式 |
|---|---|---|---|
| CPU 开销 | 低负载时低,高负载时因中断风暴急剧升高 | 持续占用 CPU,低负载时浪费 | 低负载时接近中断,高负载时接近轮询 |
| 延迟 | 低(立即响应) | 取决于轮询间隔 | 低负载时低,高负载时略有增加 |
| 吞吐量 | 高负载时因中断开销下降 | 高负载时稳定 | 高负载时保持高吞吐 |
| 实现复杂度 | 简单 | 简单 | 中等(需管理状态切换) |
| 适用场景 | 低速网络(< 1Gbps) | 专用高速设备(如 DPDK) | 通用高速网络(1Gbps ~ 100Gbps) |
工程陷阱:NAPI 的
weight 参数并非越大越好。过大的 weight 会导致单次轮询占用 CPU 时间过长,增加其他网络流量的延迟。内核默认值为 64,对于 10Gbps 网卡,建议根据 MTU 和包大小进行微调。例如,小包场景(64 字节)可适当降低 weight 以平衡延迟,大包场景(1500 字节)可适当提高 weight 以提升吞吐。
2.3.1 自适应中断调节
现代网卡驱动通常支持自适应中断调节(Adaptive Interrupt Coalescing),通过硬件参数(如 ITR/Interrupt Throttling Rate)动态调整中断频率。NAPI 与硬件中断调节协同工作时,需注意以下配置:
- 中断合并(Interrupt Coalescing):硬件在发送中断前等待一定时间或积累一定数量的数据包,减少中断次数。但会增加延迟。
- NAPI 权重与硬件队列深度:确保
weight不小于硬件队列的收包阈值,避免 NAPI 频繁退出轮询导致中断重开。 - 多队列 RSS 与 NAPI 绑定:每个 RX 队列应绑定独立的 NAPI 实例,并亲和到不同 CPU,实现并行收包。
2.4 面试高频问题与实战建议
Q1:NAPI 如何避免中断风暴?
A:NAPI 在第一次中断后关闭硬件中断,通过软中断轮询收包。只要轮询未完成(即仍有数据包待处理),硬件中断保持关闭,从而避免中断风暴。只有当轮询返回数量小于 budget 时,才重新开启中断。
A:NAPI 在第一次中断后关闭硬件中断,通过软中断轮询收包。只要轮询未完成(即仍有数据包待处理),硬件中断保持关闭,从而避免中断风暴。只有当轮询返回数量小于 budget 时,才重新开启中断。
Q2:
A:
napi_schedule() 和 __napi_schedule() 的区别?A:
napi_schedule() 会先检查 NAPI 是否已被调度(通过 napi_schedule_prep()),确保原子性;__napi_schedule() 则假设调用者已确认 NAPI 可调度,直接加入轮询链表。驱动中应优先使用 napi_schedule()。
Q3:多队列网卡如何配置 NAPI 实现并行收包?
A:每个 RX 队列分配独立的
A:每个 RX 队列分配独立的
napi_struct 实例,通过 netif_napi_add() 注册到同一个 net_device。利用 RSS(Receive Side Scaling)将不同队列的中断亲和到不同 CPU,实现并行处理。
实战调优参数:
建议在 10Gbps 以上场景中,将
/sys/class/net/eth0/napi_defer_hard_irqs:延迟硬件中断的触发次数,减少中断频率。/sys/class/net/eth0/gro_flush_timeout:GRO 刷新的超时时间,与 NAPI 轮询配合使用。建议在 10Gbps 以上场景中,将
napi_defer_hard_irqs 设置为 2~5,gro_flush_timeout 设置为 20000~50000 ns。