第二章 网络设备驱动基础:Linux 网络设备模型、NAPI 机制与中断处理
2.1 网络设备模型:net_device 结构体
做网络驱动,第一个要打交道的结构体就是 net_device。它代表一个网络接口,比如 eth0、eth1。这玩意儿很大,几百个字段。你别慌,大部分字段内核帮你管了,你要关心的是几个核心部分。
我个人习惯把 net_device 拆成三块来看:
- 标识信息:设备名、索引、MAC 地址。
- 操作函数集:
netdev_ops,这相当于设备的行为手册——打开、关闭、发包、设置特征等。 - 控制与状态:标志位、特性、硬件头长度、MTU、队列数。
有一个字段很容易被忽略:features。它告诉内核这个网卡能做什么——比如 Checksum Offload、TSO、GRO。我曾经因为少设了个 NETIF_F_GRO,导致一个大文件传输场景下 CPU 飙升。嗯,这坑踩过才知道疼。
| 字段 | 类型 | 说明 |
|---|---|---|
name | char[] | 接口名,通常由内核自动生成 |
dev_addr | unsigned char[] | MAC 地址缓存 |
netdev_ops | const struct net_device_ops * | 驱动的函数表 |
features | netdev_features_t | 硬件能力位掩码 |
mtu | unsigned int | 最大传输单元 |
irq | int | 中断号(老设备仍然需要) |
napi_list | struct list_head | NAPI 实例链表(内核 5.x+ 该逻辑已调整,但概念仍在) |
struct net_device *dev 后立即用 ether_setup() 初始化以太网相关字段。别自己手填,内核都帮你准备好了。
2.2 设备注册与注销流程
流程其实很简单:先分配、初始化,再注册。注册成功后就能被用户空间看到。注销时反过来。
/* 典型注册流程 */
struct net_device *dev = alloc_etherdev(sizeof(priv));
if (!dev)
return -ENOMEM;
/* 设置 ops、features、MAC 等 */
dev->netdev_ops = &my_netdev_ops;
dev->features |= NETIF_F_IP_CSUM | NETIF_F_GRO;
/* 注册到内核 */
ret = register_netdev(dev);
if (ret) {
free_netdev(dev);
return ret;
}
注销时调用 unregister_netdev(dev) 然后 free_netdev(dev)。这里有个坑:必须在注销前确保所有 NAPI 已经停用,中断已经释放,否则内核会报 BUG。
unregister_netdev 再去关中断,结果触发了一个 scheduling while atomic。解决方案:先 napi_disable → free_irq → unregister_netdev。顺序搞反,内核分分钟教你做人。
2.3 NAPI 机制
NAPI 的核心目的就一个:减少高流量下的中断风暴。你想想看,一个千兆网卡每秒能收 1.5M 个小包。如果每个包都触发中断,CPU 光来回切换就累死了。
它的做法很简单:首次包到达时,中断上半部把设备注册到轮询列表中,然后关掉接收中断。之后驱动通过轮询模式从硬件 FIFO 里批量取包。直到包取完了,再重新开启中断。
关键结构体 napi_struct:
struct napi_struct {
struct list_head poll_list; /* 挂在 NAPI 调度队列上 */
unsigned long state; /* 状态标志 */
int weight; /* 每次 poll 最多处理的包数 */
int (*poll)(struct napi_struct *, int);
...
};
使用时流程:
- 分配并初始化
napi_struct,设置poll函数和weight(一般 64)。 - 在中断上半部调用
napi_schedule(napi)。 - 内核会调度
poll函数在软中断上下文中执行。 poll函数里调用napi_complete(napi)并重新开中断。
2.4 中断处理流程回顾
网络中断处理分两段:上半部(Hard IRQ)和下半部(SoftIRQ)。上半部要快,只做最少的事——读状态寄存器、拿包、通知下半部。
典型的驱动中断处理函数:
static irqreturn_t my_net_isr(int irq, void *dev_id)
{
struct net_device *dev = dev_id;
struct my_priv *priv = netdev_priv(dev);
/* 检查中断原因 */
u32 sts = readl(priv->ioaddr + INT_STS);
if (!(sts & RX_INT_BIT))
return IRQ_NONE;
/* 清除中断标志 */
writel(sts & RX_INT_BIT, priv->ioaddr + INT_CLR);
/* 调度 NAPI */
napi_schedule(&priv->napi);
return IRQ_HANDLED;
}
注意点:
- 中断标志必须在
napi_schedule之前清除,否则同一中断可能反复触发。 - 如果使用老式非 NAPI 驱动,下半部通常用
tasklet或workqueue。但 NAPI 已经是主流,除非你的硬件很老。 - 中断注册用
request_irq(irq, my_net_isr, IRQF_SHARED, dev->name, dev)。如果是 PCIe 设备,用request_threaded_irq把上半部做成不可睡眠的,下半部做成 threaded 也可以。
poll 又在 CPU1 执行,两个同时读写相同的状态寄存器导致数据错乱。解决:spin_lock 或使用 __raw_readl 配合 DMA memory barrier。别想当然,硬件寄存器共享时需要同步。
2.5 三者如何协作
你看上面 SVG 图:net_device 是身份证,register_netdev 是上户口。之后中断来了,上半部快速清理硬件并唤醒 NAPI。NAPI 下半部批量收包,通过 netif_receive_skb 喂给协议栈。数据发出时,内核调用 netdev_ops->ndo_start_xmit。整个生命周期就是这样一个循环。
这里有一个容易忽略的细节:NAPI 的 poll 函数必须在 net_device 状态为 __LINK_STATE_START 时才被允许执行。如果你在设备关闭后还挂起 NAPI,内核会 panic。所以注销前一定要 napi_disable 并等待所有 poll 完成。
cat /proc/net/dev 看丢包,用 perf top 看哪个软中断函数消耗 CPU。如果 net_rx_action占比高,说明 NAPI weight 可能偏大,或者硬件中断亲和性没调好。
好了,这一章就到这里。网络设备模型、NAPI 和中断处理是 phylink 驱动的基础。后面几章我们会把这些知识用在 PHY 和 MAC 的适配上,到时候你会看到它们怎么具体落地的。