网络设备驱动核心:struct net_device 结构体详解、网络设备注册与注销、MTU 与 MAC 地址配置

各位做嵌入式Linux的朋友,咱们今天来啃一块硬骨头——网络设备驱动。说实话,我刚入行那会儿,看到 struct net_device 这个结构体,头都大了。几百行的成员变量,看着就让人发怵。但后来我发现,真正核心的其实就那么几个。

你想想看,一个网络设备在Linux内核里到底长什么样?说白了,就是 struct net_device 这个庞然大物。它就像设备的身份证,记录了设备的一切信息——名字、地址、状态、操作函数指针……

struct net_device 核心成员解析

这个结构体定义在 include/linux/netdevice.h 里,成员多得吓人。但我个人习惯,只关注以下几类:

分类 关键成员 说明
标识信息 char name[IFNAMSIZ] 设备名,如 eth0、wlan0
int ifindex 内核分配的唯一索引
地址信息 unsigned char dev_addr[MAX_ADDR_LEN] MAC 地址,驱动必须设置
unsigned char broadcast[MAX_ADDR_LEN] 广播地址,通常是 FF:FF:FF:FF:FF:FF
配置参数 int mtu 最大传输单元,默认 1500
unsigned int flags 设备标志,如 IFF_UP、IFF_RUNNING
操作函数集 const struct net_device_ops *netdev_ops 驱动核心回调,必须实现
const struct ethtool_ops *ethtool_ops ethtool 工具支持
数据收发 struct net_device_stats stats 统计信息,发包/收包计数
netdev_features_t features 硬件特性标志,如校验和卸载

我的经验: 写驱动时,netdev_ops 是重中之重。你至少要实现 ndo_open、ndo_stop、ndo_start_xmit 这三个回调。其他的像 ndo_set_mac_address、ndo_change_mtu 可以按需实现。我曾经偷懒没实现 ndo_set_mac_address,结果用户用 ifconfig 改 MAC 地址时直接报错——嗯,后来老老实实补上了。

网络设备注册与注销

设备注册,说白了就是把你的 net_device 告诉内核,让它纳入管理。流程其实不复杂:

  1. 分配结构体:用 alloc_etherdev() 或 alloc_netdev()
  2. 初始化成员:设置 MAC、MTU、操作函数集
  3. 注册设备:调用 register_netdev()
  4. 注销设备:调用 unregister_netdev() + free_netdev()

来看一段典型的注册代码:

// 分配以太网设备
struct net_device *ndev = alloc_etherdev(sizeof(struct my_priv));

if (!ndev) {
    printk(KERN_ERR "Failed to alloc netdev\n");
    return -ENOMEM;
}

// 设置操作函数集
ndev->netdev_ops = &my_netdev_ops;

// 设置 MAC 地址(从硬件读取或使用随机地址)
eth_hw_addr_set(ndev, mac_addr);

// 设置 MTU(可选,默认 1500)
ndev->mtu = 1500;

// 注册设备
int ret = register_netdev(ndev);
if (ret) {
    printk(KERN_ERR "Failed to register netdev\n");
    free_netdev(ndev);
    return ret;
}

// 保存私有数据指针
struct my_priv *priv = netdev_priv(ndev);
priv->ndev = ndev;

注意: 注册成功后,内核就认为你的设备可以工作了。所以在 register_netdev() 之前,一定要确保硬件已经初始化完毕。我曾经在注册后才初始化 DMA 缓冲区,结果设备一上线就收包,直接崩溃——血的教训。

注销时顺序反过来:

unregister_netdev(ndev);  // 先通知内核设备下线
// 这里可以释放硬件资源
free_netdev(ndev);        // 最后释放 net_device 结构体

MTU 与 MAC 地址配置

这两个参数,是网络设备最基础的配置。咱们一个一个说。

MTU——最大传输单元

MTU 决定了你的设备一次能发多大的包。默认 1500 字节,这是以太网的标准值。但有些场景需要调整:

  • VPN 隧道:需要减小 MTU,避免分片
  • Jumbo Frame:数据中心用 9000 字节,提升吞吐
  • 低速无线:可能设小一点,减少重传代价

驱动里怎么处理 MTU 变化?实现 ndo_change_mtu 回调:

static int my_change_mtu(struct net_device *ndev, int new_mtu)
{
    // 检查范围
    if (new_mtu < 68 || new_mtu > 9000) {
        netdev_err(ndev, "Invalid MTU %d\n", new_mtu);
        return -EINVAL;
    }

    // 更新 MTU
    ndev->mtu = new_mtu;

    // 如果设备正在运行,可能需要重新分配缓冲区
    if (netif_running(ndev)) {
        // 重新分配 DMA 缓冲区以适应新 MTU
        my_realloc_buffers(ndev);
    }

    return 0;
}

避坑指南: 我曾经遇到一个案子,用户把 MTU 改成 9000,结果设备还在用 1500 字节的缓冲区。发大包时直接内存越界,系统重启。所以记住——改 MTU 一定要同步调整缓冲区大小。

MAC 地址——设备的身份证

MAC 地址是网络设备的物理标识。驱动里通常有两种方式获取:

  1. 从硬件读取:EEPROM 或寄存器里存着厂商分配的 MAC
  2. 随机生成:有些虚拟设备用随机 MAC

设置 MAC 地址的标准做法:

// 从硬件读取 MAC
u8 mac_addr[ETH_ALEN];
read_from_hw_eeprom(mac_addr);

// 设置到 net_device
eth_hw_addr_set(ndev, mac_addr);

// 或者用随机 MAC(用于测试)
eth_random_addr(mac_addr);
eth_hw_addr_set(ndev, mac_addr);

如果想让用户能通过 ifconfig 改 MAC,需要实现 ndo_set_mac_address:

static int my_set_mac(struct net_device *ndev, void *addr)
{
    struct sockaddr *sa = addr;

    if (!is_valid_ether_addr(sa->sa_data))
        return -EADDRNOTAVAIL;

    // 更新硬件 MAC 寄存器
    write_mac_to_hw(sa->sa_data);

    // 更新软件 MAC
    eth_hw_addr_set(ndev, sa->sa_data);

    return 0;
}

知识结构总览

下面这张图,把咱们今天讲的内容串起来了:

网络设备驱动核心结构图 网络设备驱动核心结构 struct net_device 标识信息 name (eth0) ifindex (唯一索引) dev_addr (MAC) 配置参数 mtu (1500) flags (IFF_UP) features (硬件特性) 操作函数集 netdev_ops ethtool_ops stats (统计) 注册流程:alloc_etherdev → 初始化 → register_netdev 注销流程:unregister_netdev → 释放资源 → free_netdev 核心要点:net_device 是驱动与内核的桥梁,注册/注销是设备生命周期管理的关键 MTU 和 MAC 是基础配置,修改时必须同步硬件和软件状态

总结一下: struct net_device 是网络驱动的灵魂。你只要把标识信息填对、操作函数集实现好、注册注销流程走通,一个基本的网络驱动就成型了。MTU 和 MAC 地址的配置虽然基础,但坑不少——改 MTU 记得调缓冲区,改 MAC 记得写硬件。这些细节,往往就是驱动稳定性的分水岭。