网络设备驱动核心: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 告诉内核,让它纳入管理。流程其实不复杂:
- 分配结构体:用
alloc_etherdev()或alloc_netdev() - 初始化成员:设置 MAC、MTU、操作函数集
- 注册设备:调用
register_netdev() - 注销设备:调用
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 地址是网络设备的物理标识。驱动里通常有两种方式获取:
- 从硬件读取:EEPROM 或寄存器里存着厂商分配的 MAC
- 随机生成:有些虚拟设备用随机 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 是网络驱动的灵魂。你只要把标识信息填对、操作函数集实现好、注册注销流程走通,一个基本的网络驱动就成型了。MTU 和 MAC 地址的配置虽然基础,但坑不少——改 MTU 记得调缓冲区,改 MAC 记得写硬件。这些细节,往往就是驱动稳定性的分水岭。