2、网络设备驱动:struct net_device 结构体深度解析,NAPI与新式轮询机制,驱动注册与数据收发流程

网络设备驱动,说白了就是内核和物理网卡之间的翻译官。你想想看,数据从网线进来,怎么变成 sk_buff 往上送?这中间全靠驱动在干活。今天我们就把它彻底拆开看。

2.1 struct net_device:网络设备的身份证

每个网络设备在内核里都对应一个 struct net_device。这个结构体有多大?我记得第一次看源码时吓了一跳——足足几百行。但别慌,核心字段其实就几类。

分类 关键字段 作用
标识 name、ifindex、dev_id 设备名字、索引、物理位置
配置 mtu、flags、features MTU、IFF_UP 等标志、硬件 offload 能力
地址 dev_addr、broadcast MAC 地址、广播地址
统计 stats 收发包计数、丢包、错误统计
操作函数 netdev_ops 驱动实现的 open/stop/xmit/ioctl 等
NAPI napi_list、poll NAPI 轮询相关

重点:netdev_ops 是驱动和内核的契约。你实现了 ndo_start_xmit,内核就知道怎么把数据交给你发出去。你实现了 ndo_open,内核就知道怎么激活你的硬件。

我个人习惯在调试驱动时,先看 features 字段。为什么?因为很多诡异的问题都是硬件 offload 没对齐导致的。比如你开了 TSO,但驱动没正确处理分段,就会出现莫名其妙的丢包。

2.2 NAPI:告别老式中断风暴

老式驱动怎么收包?来一个中断收一个。流量小的时候还行,一旦大流量进来,CPU 就忙着响应中断,什么事都干不了。这叫 中断风暴。

NAPI 的思路很简单:中断只通知一次,然后切到轮询模式。内核把中断关了,驱动自己循环去收包,直到收完或者达到预算。

2.2.1 NAPI 的核心流程

  1. 硬件收到包,触发中断。
  2. 中断处理函数调用 napi_schedule(),把 napi_struct 挂到 CPU 的 poll_list 上。
  3. 软中断(NET_RX_SOFTIRQ)触发,调用 napi_struct 的 poll 方法。
  4. poll 方法循环收包,每收一个包调用 napi_gro_receive() 或 netif_receive_skb()。
  5. 收完或达到预算后,重新开启中断。

小技巧:预算值(budget)很关键。设太大,其他协议栈没时间跑;设太小,频繁切中断。我一般从 64 开始调,看 CPU 占用率和吞吐量的平衡点。

2.2.2 NAPI 的注册

// 驱动初始化时调用
netif_napi_add(netdev, &priv->napi, my_poll, 64);

// 驱动卸载时调用
netif_napi_del(&priv->napi);

这里 my_poll 是你自己实现的收包函数。原型长这样:

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) {
        // 从硬件 FIFO 取包
        struct sk_buff *skb = my_hw_receive(priv);
        if (!skb)
            break;

        // 送协议栈
        napi_gro_receive(napi, skb);
        work_done++;
    }

    if (work_done < budget) {
        // 收完了,重新开中断
        napi_complete_done(napi, work_done);
        my_hw_enable_irq(priv);
    }

    return work_done;
}

注意:千万别在 poll 里做耗时操作。我曾经见过有人在 poll 里加了个 printk,结果 10G 网卡直接掉到百兆水平。NAPI 的核心理念就是快进快出。

2.3 驱动注册:从模块到设备

驱动怎么告诉内核「我来了」?靠 module_init 和 platform_driver_register(或者 PCI 的 pci_register_driver)。

2.3.1 典型的驱动初始化流程

  1. 探测设备:probe 函数被调用,分配 net_device。
  2. 分配 net_device:alloc_etherdev(sizeof(struct my_priv))。
  3. 设置操作函数:填充 netdev_ops、ethtool_ops。
  4. 注册 NAPI:netif_napi_add。
  5. 注册设备:register_netdev(netdev)。
static int my_probe(struct platform_device *pdev)
{
    struct net_device *ndev;
    struct my_priv *priv;

    // 1. 分配 net_device + 私有数据
    ndev = alloc_etherdev(sizeof(struct my_priv));
    if (!ndev)
        return -ENOMEM;

    priv = netdev_priv(ndev);
    priv->ndev = ndev;

    // 2. 设置操作函数
    ndev->netdev_ops = &my_netdev_ops;
    ndev->ethtool_ops = &my_ethtool_ops;

    // 3. 注册 NAPI
    netif_napi_add(ndev, &priv->napi, my_poll, 64);

    // 4. 注册到内核
    register_netdev(ndev);

    return 0;
}

关键点:alloc_etherdev 分配的是 struct net_device + 私有数据区。私有数据区用 netdev_priv() 访问。这个设计很巧妙——设备本身和驱动私有数据在同一个内存块里,缓存友好。

2.4 数据收发流程:从硬件到协议栈

好了,驱动注册完了,设备起来了。数据怎么走?

2.4.1 发送流程

  1. 上层调用 dev_queue_xmit(skb)。
  2. 内核经过流量控制(TC),最终调用 ndo_start_xmit。
  3. 驱动把 skb 的数据拷贝到硬件 DMA 缓冲区。
  4. 硬件自动发出去。
  5. 发送完成中断触发,驱动释放 skb。
static netdev_tx_t my_start_xmit(struct sk_buff *skb,
                                  struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);

    // 把数据写到硬件描述符
    my_hw_write_tx_desc(priv, skb->data, skb->len);

    // 通知硬件发送
    my_hw_kick(priv);

    // 统计
    ndev->stats.tx_packets++;
    ndev->stats.tx_bytes += skb->len;

    // 释放 skb(或者等中断再释放)
    dev_kfree_skb(skb);

    return NETDEV_TX_OK;
}

经验之谈:发送时别急着 dev_kfree_skb。如果硬件支持 DMA 回读,可以等发送完成中断再释放。这样能减少内存拷贝,但要注意 skb 不能被上层复用。

2.4.2 接收流程

  1. 硬件收到包,写入 DMA 缓冲区。
  2. 硬件触发中断(或 NAPI 轮询)。
  3. 驱动从 DMA 缓冲区拷贝数据到 skb。
  4. 调用 napi_gro_receive 或 netif_receive_skb。
  5. 协议栈处理,最终送到 socket。

接收路径上有个关键优化——GRO(Generic Receive Offload)。它把多个小包合并成大包再送协议栈,减少协议栈处理次数。我调过一个问题:某款网卡 GRO 没开,UDP 小包场景下 CPU 直接跑满。开了 GRO 后,CPU 降了 40%。

2.5 整体架构图

下面这张图把驱动、NAPI、协议栈的关系画清楚了:

网络设备驱动数据收发架构 物理网卡硬件 DMA 引擎 / FIFO / 中断控制器 网络设备驱动 ndo_start_xmit / my_poll / 中断处理 net_device / netdev_ops / NAPI NAPI 轮询机制 napi_schedule / poll / napi_complete 网络协议栈 GRO / IP 层 / TCP/UDP / Socket 发送路径:上层 → 驱动 → 硬件;接收路径:硬件 → 驱动 → NAPI → 协议栈

2.6 避坑指南

做驱动开发这些年,踩过的坑不少。挑几个典型的说说:

  • 中断上下文不能睡眠:我曾经在中断处理里调了 kmalloc(GFP_KERNEL),结果系统直接死锁。记住,中断里只能用 GFP_ATOMIC。
  • NAPI 预算别设太大:设 128 以上,其他软中断可能饿死。我一般 64 起步,压测时再调。
  • net_device 的 features 要对齐硬件能力:比如硬件不支持 TSO,你却在 features 里开了,内核会帮你做分段,但性能会下降。不如关掉让协议栈处理。
  • DMA 缓冲区要 cache 对齐:不对齐的话,在某些架构上会导致 cache 一致性错误,数据错乱。我吃过一次亏,排查了两天才发现是 DMA 地址没对齐。

重要提醒:注册 net_device 之前,一定要确保所有字段都初始化好了。尤其是 netdev_ops,如果某个函数指针是 NULL,内核调用时直接 oops。我习惯在 probe 最后加一个 BUG_ON(!ndev->netdev_ops) 做防御。

驱动开发说难也难,说简单也简单。核心就是理解 net_device 这个结构体,把 NAPI 的流程跑通,数据收发路径理清楚。剩下的就是和硬件寄存器打交道了——那又是另一门手艺活。