3、网络设备驱动基础:net_device 结构与生命周期管理

网络设备驱动是 Linux 内核中连接协议栈与物理网卡(或虚拟网卡)的桥梁。驱动开发的核心工作,几乎全部围绕 struct net_device 展开:分配、填充、注册、实现操作函数、处理状态变更,最后在卸载时安全注销。本章将深入剖析 net_device 结构体的关键字段、注册与注销的完整流程、设备打开/关闭/启动/停止的语义差异,以及 MTU 与 MAC 地址的管理机制。理解这些基础,是后续分析中断处理、NAPI、流量控制等高级主题的前提。

net_device 生命周期与核心操作 Linux 内核协议栈(TCP/IP) net_device 核心结构 标识与配置 • name: eth0 • ifindex: 2 • dev_addr: MAC • mtu: 1500 操作函数集 • ndo_open() • ndo_stop() • ndo_start_xmit() • ndo_set_mac_address() 状态与统计 • state: __LINK_STATE_* • flags: IFF_UP, IFF_RUNNING • stats: rx_packets • priv_destructor 注册流程 alloc_netdev() → 填充字段 → register_netdev() → 通知链 → 设备可见,可被用户态配置 注销流程 unregister_netdev() → 通知链 → 设备从内核移除 → free_netdev() 释放内存 设备状态机 DOWN open() UP start() RUNNING stop() STOPPED close()

上图展示了 net_device 在驱动中的核心地位:它向上对接协议栈,向下管理硬件,同时承载注册/注销流程和状态机转换。下面我们逐一拆解每个环节的实现要点。

3.1 net_device 结构体:驱动与协议栈的“合同”

struct net_device 是 Linux 内核中最大的结构体之一(不同内核版本约 200-300 个字段),它定义了网络设备的所有属性和操作。驱动开发者不需要理解每个字段,但必须掌握以下关键分组:

字段分组代表字段驱动职责
标识信息name, ifindex, dev_addr设备命名、内核索引、MAC 地址存储
配置参数mtu, flags, features最大传输单元、IFF_* 标志、硬件特性位
操作函数netdev_ops, ethtool_ops实现 open/stop/xmit/ioctl 等回调
状态管理state, reg_state链路状态位图、注册状态机
统计信息stats, tstats记录收发包计数、错误计数
私有数据priv, priv_destructor驱动自定义数据区,跟随设备生命周期

其中 netdev_ops 是驱动必须实现的核心回调集合。以 Intel e1000 驱动为例,其 e1000_netdev_ops 包含约 20 个函数指针,但最小可用驱动只需实现 ndo_open、ndo_stop、ndo_start_xmit 和 ndo_get_stats64 四个函数。

核心原理:net_device 采用“组合而非继承”的设计。驱动通过 alloc_netdev() 分配设备时,内核会一次性分配 sizeof(struct net_device) + sizeof(priv) 的连续内存。私有数据区紧跟在 net_device 之后,通过 netdev_priv() 宏访问。这种设计保证了缓存局部性,同时避免了额外的指针跳转。

3.2 注册与注销流程:设备如何“上线”与“下线”

设备注册是驱动加载后让内核认识设备的过程,注销则是反向操作。完整流程如下:

  1. 分配设备:调用 alloc_netdev(sizeof_priv, name, name_assign_type, setup),其中 setup 回调负责初始化默认字段。
  2. 填充字段:设置 netdev_ops、ethtool_ops、MAC 地址、MTU、硬件特性(如 NETIF_F_IP_CSUM)等。
  3. 注册设备:调用 register_netdev(),内核会:分配 ifindex → 将设备加入全局链表 → 触发 NETDEV_REGISTER 通知链 → 创建 sysfs 条目(/sys/class/net/eth0)。
  4. 注销设备:调用 unregister_netdev(),内核会:触发 NETDEV_UNREGISTER 通知链 → 从全局链表移除 → 删除 sysfs 条目 → 等待所有引用释放。
  5. 释放内存:调用 free_netdev(),此时会调用 priv_destructor 回调,释放驱动私有资源。
// 典型注册流程示例(伪代码)
struct net_device *ndev;
ndev = alloc_netdev(sizeof(struct my_priv), "eth%d", NET_NAME_ENUM, my_setup);
if (!ndev) return -ENOMEM;

ndev->netdev_ops = &my_netdev_ops;
ndev->ethtool_ops = &my_ethtool_ops;
memcpy(ndev->dev_addr, mac_addr, ETH_ALEN);
ndev->mtu = 1500;
ndev->features |= NETIF_F_HW_CSUM | NETIF_F_SG;

err = register_netdev(ndev);
if (err) { free_netdev(ndev); return err; }

风险提示:注册成功后,设备立即可被用户态访问(如 ip link set eth0 up)。因此,register_netdev() 之前必须完成所有硬件初始化(如 PCI 资源申请、DMA 缓冲池建立)。否则,用户态可能在驱动尚未就绪时触发 open 操作,导致空指针崩溃。

3.3 设备打开/关闭与启动/停止:状态机的精确定义

很多初学者混淆“打开/关闭”与“启动/停止”的概念。实际上,这两组操作处于不同层次:

操作触发方式内核语义驱动职责
open()ip link set eth0 up设备从 DOWN → UP,协议栈开始使用该设备申请 IRQ、启用 DMA、初始化硬件收发队列
stop()ip link set eth0 down设备从 UP → DOWN,协议栈停止发送释放 IRQ、停止 DMA、关闭硬件
start_xmit()协议栈有数据包设备处于 UP 状态,可发送将 skb 写入硬件发送队列,触发 DMA
tx_timeout()发送超时(默认 5s)设备可能卡死重置硬件,统计超时次数

关键区别在于:open/stop 是设备级状态切换,而 start/stop 是数据通路操作。一个 UP 状态的设备,其发送队列可能因流控而暂时停止(如 netif_stop_queue()),但这不影响设备本身的 UP 状态。

实战场景:在 e1000 驱动中,e1000_open() 会做以下事情:分配 RX/TX 描述符环 → 设置 MAC 地址过滤 → 使能中断 → 调用 netif_start_queue() 启动发送队列。而 e1000_stop() 则反向操作:调用 netif_stop_queue() → 关闭中断 → 释放描述符环。如果驱动在 open 时忘记调用 netif_start_queue(),设备虽然 UP,但 ndo_start_xmit 永远不会被调用,表现为“网卡无法发送数据”。

3.4 MTU 与 MAC 地址管理:配置变更的完整链路

MTU(Maximum Transmission Unit)和 MAC 地址是网络设备最常用的可配置属性。驱动必须正确处理它们的变更请求。

MTU 管理

协议栈通过 ndo_change_mtu() 回调通知驱动 MTU 变更。驱动需要验证新值是否在硬件支持范围内,并重新计算相关缓冲区大小。典型流程:

  1. 用户执行 ip link set eth0 mtu 9000。
  2. 内核调用 dev_set_mtu() → 检查 min_mtu/max_mtu 字段 → 调用驱动的 ndo_change_mtu()。
  3. 驱动验证新 MTU(如 e1000 支持 68-1500 或 68-9000 取决于硬件)→ 更新 ndev->mtu → 重新分配 RX 缓冲区(如果使用页分配器则无需)。
  4. 如果变更失败,返回负错误码,内核保持原 MTU。
static int my_change_mtu(struct net_device *ndev, int new_mtu)
{
    if (new_mtu < 68 || new_mtu > 9000)
        return -EINVAL;
    
    ndev->mtu = new_mtu;
    /* 如果硬件需要,重新配置 RX 缓冲区大小 */
    my_reconfigure_rx_buffers(ndev);
    return 0;
}

MAC 地址管理

MAC 地址变更通过 ndo_set_mac_address() 回调实现。驱动需要:验证地址合法性(组播/广播地址不可作为单播地址)→ 将新地址写入硬件寄存器 → 更新 ndev->dev_addr。对于支持多个 MAC 地址的硬件(如 VF 设备),还需处理 ndo_set_rx_mode() 中的地址列表。

常见误区:有些驱动在 ndo_set_mac_address() 中直接修改 ndev->dev_addr 而不更新硬件。这会导致设备显示新 MAC,但实际收发仍使用旧地址。正确做法是:先写硬件,成功后再更新 dev_addr。另外,某些虚拟设备(如 bond、vlan)的 MAC 变更不应传递到物理硬件,驱动需通过 IFF_LIVE_ADDR_CHANGE 标志声明是否支持运行时变更。

3.5 案例拆解:从零实现一个虚拟网卡驱动

为了巩固上述概念,我们拆解一个最小虚拟网卡驱动的实现。该驱动创建名为 vnet%d 的设备,仅支持简单的数据包收发(通过内存拷贝模拟)。

实现步骤关键代码/操作涉及知识点
1. 定义私有数据结构struct vnet_priv { struct net_device_stats stats; };私有数据区用法
2. 实现 setup 回调设置 min_mtu=68, max_mtu=65535, 填充 netdev_ops设备初始化默认值
3. 实现 open/stopopen 中设置 netif_start_queue();stop 中 netif_stop_queue()队列启停
4. 实现 start_xmit将 skb->data 拷贝到接收端(模拟环回),更新统计发送路径
5. 注册设备module_init 中 alloc_netdev + register_netdev注册流程
6. 注销设备module_exit 中 unregister_netdev + free_netdev注销流程

该驱动的核心发送函数如下:

static netdev_tx_t vnet_start_xmit(struct sk_buff *skb, struct net_device *ndev)
{
    struct vnet_priv *priv = netdev_priv(ndev);
    
    /* 模拟发送:统计计数,然后释放 skb */
    priv->stats.tx_packets++;
    priv->stats.tx_bytes += skb->len;
    
    /* 如果是环回模式,模拟接收 */
    if (ndev->flags & IFF_LOOPBACK) {
        priv->stats.rx_packets++;
        priv->stats.rx_bytes += skb->len;
    }
    
    dev_kfree_skb(skb);
    return NETDEV_TX_OK;
}

这个案例虽然简单,但完整展示了 net_device 生命周期管理的所有关键点:分配、填充、注册、操作实现、注销。实际硬件驱动的复杂度主要在于 DMA 管理、中断处理和硬件寄存器操作,但生命周期框架完全一致。

3.6 常见误区与总结复盘

  • 误区一:注册前未完成硬件初始化。这会导致设备可见但不可用,用户态操作触发空指针。正确做法:所有硬件资源在 register_netdev() 之前准备就绪。
  • 误区二:open/stop 与 start/stop 混淆。open 是设备级操作,start_xmit 是数据通路操作。设备 UP 但队列停止是合法状态(如流控)。
  • 误区三:MTU 变更未验证范围。直接修改 ndev->mtu 而不检查硬件限制,可能导致 DMA 缓冲区溢出。必须使用 min_mtu/max_mtu 字段或自定义验证。
  • 误区四:MAC 地址更新顺序错误。先更新 dev_addr 再写硬件,如果硬件写入失败,设备状态不一致。正确顺序:硬件成功 → 更新内存。
  • 误区五:注销时未等待引用计数归零。调用 unregister_netdev() 后立即 free_netdev(),但仍有用户态 socket 引用该设备。应使用 netdev_wait_allrefs() 或确保所有引用已释放。

经验总结:编写网络设备驱动时,建议遵循以下顺序:① 先实现最小可用的 open/stop/start_xmit 三个函数,验证设备能收发数据;② 再逐步添加 ethtool 支持、统计信息、多队列等高级特性;③ 每次修改后使用 ip link 和 ping 进行回归测试。这种增量开发方式能快速定位问题,避免大而全的实现导致排障困难。