3、网络设备驱动基础:net_device 结构与生命周期管理
网络设备驱动是 Linux 内核中连接协议栈与物理网卡(或虚拟网卡)的桥梁。驱动开发的核心工作,几乎全部围绕 struct net_device 展开:分配、填充、注册、实现操作函数、处理状态变更,最后在卸载时安全注销。本章将深入剖析 net_device 结构体的关键字段、注册与注销的完整流程、设备打开/关闭/启动/停止的语义差异,以及 MTU 与 MAC 地址的管理机制。理解这些基础,是后续分析中断处理、NAPI、流量控制等高级主题的前提。
上图展示了 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 注册与注销流程:设备如何“上线”与“下线”
设备注册是驱动加载后让内核认识设备的过程,注销则是反向操作。完整流程如下:
- 分配设备:调用
alloc_netdev(sizeof_priv, name, name_assign_type, setup),其中 setup 回调负责初始化默认字段。 - 填充字段:设置
netdev_ops、ethtool_ops、MAC 地址、MTU、硬件特性(如NETIF_F_IP_CSUM)等。 - 注册设备:调用
register_netdev(),内核会:分配 ifindex → 将设备加入全局链表 → 触发 NETDEV_REGISTER 通知链 → 创建 sysfs 条目(/sys/class/net/eth0)。 - 注销设备:调用
unregister_netdev(),内核会:触发 NETDEV_UNREGISTER 通知链 → 从全局链表移除 → 删除 sysfs 条目 → 等待所有引用释放。 - 释放内存:调用
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 变更。驱动需要验证新值是否在硬件支持范围内,并重新计算相关缓冲区大小。典型流程:
- 用户执行
ip link set eth0 mtu 9000。 - 内核调用
dev_set_mtu()→ 检查min_mtu/max_mtu字段 → 调用驱动的ndo_change_mtu()。 - 驱动验证新 MTU(如 e1000 支持 68-1500 或 68-9000 取决于硬件)→ 更新
ndev->mtu→ 重新分配 RX 缓冲区(如果使用页分配器则无需)。 - 如果变更失败,返回负错误码,内核保持原 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/stop | open 中设置 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 进行回归测试。这种增量开发方式能快速定位问题,避免大而全的实现导致排障困难。