3. Linux网络设备驱动框架:net_device结构体、网络设备驱动注册与注销、NAPI与中断机制

好,咱们今天聊聊网卡驱动的核心骨架。说实话,Linux网络设备驱动这块,刚入门时容易觉得乱——结构体多、回调函数多、中断处理还分好几套。但你别怕,抓住三个关键点就行:net_device 是网卡的“身份证”,注册与注销是驱动的“生老病死”,NAPI与中断是收发包的“交通规则”。

我个人习惯把这三块串起来理解:先有设备对象,再把它挂到内核里,最后用高效的方式处理数据。咱们一个一个拆开看。

3.1 net_device结构体:网卡的“户口本”

在Linux内核里,每个网络设备都对应一个 struct net_device 结构体。你可以把它想象成网卡的“户口本”——里面记着设备的名字、MAC地址、MTU、操作函数集等等。我当年第一次看这个结构体时,差点被它几百行的成员吓到。其实常用的就那几十个。

核心成员我列一下:

成员 类型 说明
name char[] 设备名,比如 eth0、wlan0
dev_addr unsigned char* MAC地址指针
mtu unsigned int 最大传输单元,默认1500
flags unsigned short 设备标志,比如 IFF_UP、IFF_RUNNING
netdev_ops const struct net_device_ops* 操作函数集,最重要的成员之一
irq int 中断号
features netdev_features_t 硬件特性标志,比如支持校验和卸载

这里我想特别提一下 netdev_ops。它定义了网卡怎么“干活”——打开、关闭、启动传输、设置MAC地址、修改MTU等等。你在驱动里主要就是填充这个结构体。举个例子:

static const struct net_device_ops my_netdev_ops = {
    .ndo_open       = my_netdev_open,
    .ndo_stop       = my_netdev_stop,
    .ndo_start_xmit = my_netdev_start_xmit,
    .ndo_set_mac_address = my_netdev_set_mac,
    .ndo_change_mtu = my_netdev_change_mtu,
};

嗯,这里要注意:ndo_start_xmit 是发送数据的入口。内核协议栈把要发的skb交给你,你负责把它扔到硬件里。我在项目中遇到过一个问题——发送函数里忘了释放skb,结果内存泄漏,跑几天系统就挂了。所以记得:发完数据要 dev_kfree_skb()

3.2 网络设备驱动注册与注销

驱动怎么告诉内核“嘿,我有个网卡要管”?靠两个函数:register_netdev()unregister_netdev()

注册流程大概是这样的:

  1. alloc_etherdev()alloc_netdev() 分配一个 net_device 结构体
  2. 填充必要的成员:name、netdev_ops、ethtool_ops 等
  3. 设置硬件相关资源:IO地址、中断号、DMA缓冲区
  4. 调用 register_netdev() 注册到内核

我贴一段典型的初始化代码:

static int my_netdev_probe(struct platform_device *pdev)
{
    struct net_device *ndev;
    struct my_priv *priv;
    int ret;

    // 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. 设置MAC地址(从设备树或EEPROM读取)
    // ...

    // 4. 注册
    ret = register_netdev(ndev);
    if (ret) {
        free_netdev(ndev);
        return ret;
    }

    platform_set_drvdata(pdev, ndev);
    return 0;
}

注销时反过来:先 unregister_netdev(),再 free_netdev()。我曾经犯过一个错——先释放了私有数据再注销,结果注销函数里还访问了私有数据,直接Oops。顺序很重要,切记。

警告: 注册成功后,net_device 的生命周期由内核管理。不要在注册后直接 kfree 它,要用 free_netdev()。另外,注销前要确保没有正在进行的收发操作,否则可能造成野指针。

3.3 NAPI与中断机制

收包是网卡驱动的核心。早期驱动用纯中断方式:来一个包,触发一次中断,CPU 进中断处理函数收包。问题很明显——高负载下中断风暴,CPU 被活活打死。

NAPI(New API)就是来解决这个问题的。它的核心思想是:中断 + 轮询混合。中断来了先关中断,然后切到轮询模式,一口气收完所有包,再开中断。这样高负载时减少中断次数,低负载时响应也快。

NAPI 的关键数据结构是 struct napi_struct,里面有个 poll 回调函数。你想想看,驱动要做的事就是:

  1. 初始化时调用 netif_napi_add() 注册 NAPI
  2. 中断处理函数里调用 napi_schedule() 触发轮询
  3. 在 poll 函数里收包,返回处理了多少个包
  4. 收完后调用 napi_complete() 并重新开中断

我画个流程图帮你理解:

NAPI 收包流程 硬件中断到达 中断处理函数 napi_schedule() poll() 收包 硬件检测到数据包 触发中断信号 读取中断状态寄存器 关闭本中断 将napi加入轮询列表 触发软中断 循环收包直到预算用完 或没有包了

代码层面,poll 函数长这样:

static int my_netdev_poll(struct napi_struct *napi, int budget)
{
    struct my_priv *priv = container_of(napi, struct my_priv, napi);
    struct net_device *ndev = priv->ndev;
    int work_done = 0;

    // 循环从硬件收包
    while (work_done < budget) {
        struct sk_buff *skb = my_hw_receive_packet(priv);
        if (!skb)
            break;

        skb->protocol = eth_type_trans(skb, ndev);
        netif_receive_skb(skb);
        work_done++;
    }

    // 如果收满了,继续轮询;否则退出轮询模式
    if (work_done < budget) {
        napi_complete_done(napi, work_done);
        // 重新开中断
        my_hw_enable_interrupt(priv);
    }

    return work_done;
}
小技巧: budget 参数是内核给你的“预算”——这次最多收多少个包。一般用 NAPI_POLL_WEIGHT(64)。别贪心一次收太多,否则其他网络设备会饿死。

中断处理函数里,你只需要做最轻量的事:

static irqreturn_t my_netdev_interrupt(int irq, void *dev_id)
{
    struct net_device *ndev = dev_id;
    struct my_priv *priv = netdev_priv(ndev);

    // 关中断
    my_hw_disable_interrupt(priv);

    // 触发NAPI轮询
    napi_schedule(&priv->napi);

    return IRQ_HANDLED;
}

你看,中断处理函数里几乎不做实际收包工作,只是关中断 + 调度NAPI。实际收包在软中断上下文里完成。这样设计的好处是:中断处理时间极短,系统响应快。

核心要点:

  • net_device 是网卡驱动的“身份证”,必须正确填充 netdev_ops
  • 注册用 register_netdev(),注销用 unregister_netdev(),顺序不能乱
  • NAPI 用中断唤醒、轮询收包,高负载下性能远优于纯中断
  • 中断处理函数要轻量,只做关中断和调度NAPI

嗯,说到NAPI,我记得有一次调试一个千兆网卡驱动,发现收包性能上不去。查了半天,原来是 poll 函数里忘了调用 napi_complete(),导致中断一直关着,后续包只能靠轮询收。低负载时延迟飙升。所以啊,细节决定成败。

好了,这一章的内容就到这。你把这些搞懂了,网卡驱动的基本框架就搭起来了。下一块咱们聊聊更具体的——sk_buff 和 DMA 传输,那才是真正搬砖的地方。


专注资料整理