第二章 网络设备驱动基础: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 飙升。嗯,这坑踩过才知道疼。

字段类型说明
namechar[]接口名,通常由内核自动生成
dev_addrunsigned char[]MAC 地址缓存
netdev_opsconst struct net_device_ops *驱动的函数表
featuresnetdev_features_t硬件能力位掩码
mtuunsigned int最大传输单元
irqint中断号(老设备仍然需要)
napi_liststruct list_headNAPI 实例链表(内核 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。顺序搞反,内核分分钟教你做人。
Linux 网络设备驱动核心流程 alloc_etherdev 初始化 ops/features register_netdev NAPI + 中断处理 数据包收发循环 unregister_netdev NAPI 在此介入 中断上半部→napi_schedule tx / rx 驱动回调

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);
    ...
};

使用时流程:

  1. 分配并初始化 napi_struct,设置 poll 函数和 weight(一般 64)。
  2. 在中断上半部调用 napi_schedule(napi)。
  3. 内核会调度 poll 函数在软中断上下文中执行。
  4. poll 函数里调用 napi_complete(napi) 并重新开中断。
我在项目中遇到过:有个同事把 weight 设成 4096,结果 poll 函数占用了 2ms 以上,导致音频数据丢帧。weight 不是越大越好,要和你的硬件 FIFO 深度、CPU 负载一起调。建议从 64 起步。

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 也可以。
我曾经……在 SMP 环境下忘记给寄存器访问加锁。中断可能在 CPU0 触发,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 的适配上,到时候你会看到它们怎么具体落地的。