2. Linux 网络设备驱动基础:核心结构与生命周期管理

网络设备驱动是 Linux 内核网络子系统的基石。本章聚焦于驱动开发者必须掌握的三个核心环节:net_device 结构体的组成与配置、网络设备的注册与注销流程、以及设备的打开与关闭操作。理解这些基础是后续实现中断处理与 DMA 传输协同的前提。

net_device 核心结构关系 net_device 核心结构关系 struct net_device 设备标识与配置 name, dev_id, ifindex mtu, type, addr_len features, priv_flags 操作函数集 netdev_ops (ndo_open/stop/start_xmit) ethtool_ops 数据路径与中断 irq, base_addr rx/tx_queue NAPI 结构 说明:net_device 是网络设备驱动的“身份证”,驱动通过它向内核注册设备能力。 所有中断处理、DMA 传输最终都围绕此结构体展开。

2.1 net_device 结构体深度解析

struct net_device 是 Linux 内核中描述网络设备的巨型结构体,定义在 include/linux/netdevice.h 中。它聚合了设备的所有属性、操作函数指针、统计信息和数据队列。驱动开发者无需理解每个字段,但必须掌握以下几类核心成员。

2.1.1 标识与配置字段

字段名类型说明
namechar[IFNAMSIZ]设备名称,如 eth0、enp0s3
dev_idint用于区分同一名称下的多个设备
ifindexint内核全局唯一的接口索引
mtuunsigned int最大传输单元,默认 1500
typeunsigned short硬件类型,如 ARPHRD_ETHER
addr_lenunsigned char硬件地址长度,以太网为 6
featuresnetdev_features_t设备能力位图,如 NETIF_F_SG、NETIF_F_TSO

2.1.2 操作函数集:netdev_ops

netdev_ops 是驱动与内核网络协议栈交互的核心接口表。每个网络设备驱动必须实现以下关键回调:

  • ndo_open:打开设备,申请硬件资源、注册中断、启动 DMA 引擎。
  • ndo_stop:关闭设备,释放资源、注销中断、停止 DMA。
  • ndo_start_xmit:发送数据包,将 sk_buff 传递给硬件发送队列。
  • ndo_set_rx_mode:设置接收模式(单播/多播/混杂)。
  • ndo_change_mtu:动态修改 MTU 值。
  • ndo_tx_timeout:发送超时处理,通常用于复位硬件。

关键提示:在注册 net_device 之前,驱动必须将 netdev_ops 指针赋值给 dev->netdev_ops。内核在打开设备、发送数据包时直接调用这些回调,不会检查是否为 NULL,因此遗漏赋值将导致内核崩溃。

2.1.3 数据路径与中断相关字段

  • irq:设备使用的中断号,由驱动在探测时设置。
  • base_addr:设备 I/O 内存或端口基地址。
  • rx_queue / tx_queue:接收和发送队列,用于流量控制。
  • napi_struct:NAPI 轮询结构体,用于高效中断处理(后续章节详述)。
  • stats:struct net_device_stats,存放收发包计数、错误计数等统计信息。

2.2 网络设备注册与注销流程

设备注册是驱动向内核网络子系统宣告“我在这里”的过程。注销则是设备移除时的清理操作。这两个流程必须成对出现,且遵循严格的顺序。

网络设备注册与注销流程 网络设备注册与注销流程 注册流程 1. alloc_netdev() 2. 初始化字段/ops 3. register_netdev() 4. 内核创建 sysfs 条目 注销流程 1. 停止设备 (ndo_stop) 2. unregister_netdev() 3. 内核移除 sysfs 条目 4. free_netdev() 注意:register_netdev() 可能触发内核调用 ndo_open,因此驱动必须在注册前完成所有硬件探测。 注销时,必须先注销网络设备,再释放 net_device 结构体。

2.2.1 设备注册步骤

  1. 分配 net_device:调用 alloc_netdev() 或 alloc_etherdev() 分配结构体内存。驱动可指定私有数据区大小。
  2. 初始化字段:设置设备名称、MAC 地址、MTU、netdev_ops、ethtool_ops 等。此时不能操作硬件。
  3. 注册设备:调用 register_netdev(dev),内核将设备加入全局网络设备链表,创建 sysfs 条目,并可能调用 ndo_open。
  4. 后续操作:注册成功后,设备对用户空间可见(如 ifconfig -a)。驱动可继续设置硬件参数。

2.2.2 设备注销步骤

  1. 停止设备:确保设备已关闭(调用 ndo_stop),释放中断和 DMA 资源。
  2. 注销设备:调用 unregister_netdev(dev),内核从链表中移除设备,删除 sysfs 条目。
  3. 释放结构体:调用 free_netdev(dev) 释放内存。此函数必须在注销之后调用。

常见错误:在设备仍处于打开状态时直接调用 unregister_netdev(),会导致内核访问已释放的中断处理函数。正确的顺序是:先关闭设备 → 注销 → 释放。另外,不要在中断上下文中调用 register_netdev() 或 unregister_netdev(),这些函数可能睡眠。

2.3 网络设备打开与关闭流程

设备的打开(open)和关闭(stop)是驱动生命周期中最频繁的操作。用户执行 ifconfig eth0 up/down 或 ip link set eth0 up/down 时,内核会调用对应的回调函数。

网络设备打开与关闭状态机 网络设备打开与关闭状态机 DOWN 设备未启用 OPENING 正在打开 UP (RUNNING) 设备正常运行 CLOSING 正在关闭 ndo_open() 成功 ndo_stop() 关闭完成 失败回退

2.3.1 ndo_open 回调实现要点

当用户启用网络接口时,内核调用驱动的 ndo_open 函数。驱动在此函数中必须完成以下操作:

  • 申请硬件资源:如 I/O 内存、DMA 缓冲区、描述符环。
  • 注册中断处理函数:调用 request_irq(),注册设备的中断服务程序。
  • 启动硬件:使能 DMA 引擎、设置接收/发送描述符、开启硬件中断。
  • 启动 NAPI:如果使用 NAPI,调用 napi_enable()。
  • 更新链路状态:调用 netif_carrier_on() 或 netif_start_queue() 通知内核可以开始收发数据。
static int my_ndo_open(struct net_device *dev) {
    struct my_priv *priv = netdev_priv(dev);
    int ret;

    /* 1. 申请 DMA 描述符环 */
    ret = my_alloc_dma_rings(priv);
    if (ret) return ret;

    /* 2. 注册中断 */
    ret = request_irq(dev->irq, my_interrupt_handler, 0, dev->name, dev);
    if (ret) {
        my_free_dma_rings(priv);
        return ret;
    }

    /* 3. 启动硬件 */
    my_hw_start(priv);

    /* 4. 启用 NAPI */
    napi_enable(&priv->napi);

    /* 5. 通知内核可以发送 */
    netif_start_queue(dev);

    return 0;
}

2.3.2 ndo_stop 回调实现要点

关闭设备时,驱动必须执行与打开相反的操作,且顺序要严格对称:

  • 停止发送队列:调用 netif_stop_queue() 阻止内核继续提交数据包。
  • 禁用 NAPI:调用 napi_disable(),确保轮询函数不再运行。
  • 停止硬件:关闭 DMA 引擎、屏蔽硬件中断。
  • 释放中断:调用 free_irq(dev->irq, dev)。
  • 释放 DMA 资源:回收描述符环和缓冲区。
  • 更新链路状态:调用 netif_carrier_off()。
static int my_ndo_stop(struct net_device *dev) {
    struct my_priv *priv = netdev_priv(dev);

    /* 1. 停止发送队列 */
    netif_stop_queue(dev);

    /* 2. 禁用 NAPI */
    napi_disable(&priv->napi);

    /* 3. 停止硬件 */
    my_hw_stop(priv);

    /* 4. 释放中断 */
    free_irq(dev->irq, dev);

    /* 5. 释放 DMA 资源 */
    my_free_dma_rings(priv);

    /* 6. 关闭链路 */
    netif_carrier_off(dev);

    return 0;
}

最佳实践:在 ndo_open 中,如果某个步骤失败,必须回滚所有已成功的操作(释放已申请的资源)。建议使用 goto 错误处理模式,确保资源不会泄漏。ndo_stop 必须能够处理设备未完全初始化的情况(例如中断尚未注册),避免重复释放。

2.4 关键数据结构与函数速查表

函数/宏头文件作用
alloc_etherdev(sizeof_priv)linux/etherdevice.h分配以太网 net_device 及私有数据区
register_netdev(dev)linux/netdevice.h注册网络设备到内核
unregister_netdev(dev)linux/netdevice.h从内核注销网络设备
free_netdev(dev)linux/netdevice.h释放 net_device 结构体
netdev_priv(dev)linux/netdevice.h获取设备私有数据区指针
netif_start_queue(dev)linux/netdevice.h启用发送队列
netif_stop_queue(dev)linux/netdevice.h停用发送队列
netif_carrier_on/off(dev)linux/netdevice.h设置链路载波状态

2.5 本章小结

本章详细介绍了 Linux 网络设备驱动的三大基础模块:

  • net_device 结构体:作为设备驱动的核心数据结构,聚合了标识、操作函数集、数据路径和中断相关字段。
  • 注册与注销流程:必须遵循“分配 → 初始化 → 注册”和“停止 → 注销 → 释放”的严格顺序。
  • 打开与关闭流程:ndo_open 负责申请资源并启动硬件,ndo_stop 负责停止硬件并释放资源,两者必须对称实现。