4. 网络设备注册与注销:核心API详解与实战
网络设备驱动开发中,最基础也最关键的一步,就是设备的注册与注销。说白了,就是让内核知道「嘿,我这儿有个网卡,你管一下」,以及「这网卡要拆了,你清理一下」。今天我们就来聊聊这四个核心函数:alloc_netdev()、register_netdev()、unregister_netdev()、free_netdev()。
我个人习惯把这四个函数分成两组:分配/释放 和 注册/注销。分配和释放管的是内存,注册和注销管的是内核链路。顺序不能乱,否则系统会给你颜色看。
4.1 alloc_netdev():给网络设备安个家
先看分配函数。原型长这样:
struct net_device *alloc_netdev(int sizeof_priv,
const char *name,
unsigned char name_assign_type,
void (*setup)(struct net_device *));
参数不多,但每个都有讲究:
- sizeof_priv:私有数据结构体的大小。我习惯把硬件寄存器地址、中断号、锁这些东西放进去。传0也行,但你就得用
netdev_priv()自己折腾了。 - name:设备名模板,比如
"eth%d"。内核会自动把%d替换成空闲的序号。 - name_assign_type:名字分配方式。一般用
NET_NAME_ENUM,让内核自动编号。 - setup:初始化回调函数。这里你要设置好设备的操作函数集、特性标志等。
netdev_ops和ethtool_ops。我曾经漏了后者,结果调试时ethtool命令死活没反应,排查了半天才发现是ops没挂上。
来看一个实际例子:
static void my_netdev_setup(struct net_device *ndev)
{
ether_setup(ndev); // 设置以太网默认参数
ndev->netdev_ops = &my_netdev_ops;
ndev->ethtool_ops = &my_ethtool_ops;
ndev->features |= NETIF_F_HW_CSUM; // 硬件校验和
}
static int my_probe(struct platform_device *pdev)
{
struct net_device *ndev;
struct my_priv *priv;
ndev = alloc_netdev(sizeof(struct my_priv), "eth%d",
NET_NAME_ENUM, my_netdev_setup);
if (!ndev)
return -ENOMEM;
priv = netdev_priv(ndev);
// 初始化私有数据...
platform_set_drvdata(pdev, ndev);
// 后面还要register...
}
4.2 register_netdev():让内核认识你
分配完内存,设备还是个「黑户」。必须调用register_netdev(),内核才会把它加入网络子系统。原型很简单:
int register_netdev(struct net_device *ndev);
返回0表示成功,负值表示失败。常见的失败原因有:
- 设备名冲突(比如已经有
eth0了) - ops没设置完整
- 内存不足
ndo_open()了。所以注册之前,硬件资源(中断、IO内存)必须已经准备好。我曾经在注册后才去申请中断,结果用户态一配IP,驱动直接崩溃。
注册成功后,你可以用ip link看到新设备。但此时它还是DOWN状态,需要用户手动ip link set eth0 up才会触发你的ndo_open()。
4.3 unregister_netdev():优雅地退出
设备移除时,先调用unregister_netdev()。这个函数会:
- 通知内核网络子系统:这个设备要走了
- 关闭设备(如果还在运行)
- 释放所有内核资源(如sysfs条目)
调用之后,设备对用户态就不可见了。但注意:内存还没释放。
static int my_remove(struct platform_device *pdev)
{
struct net_device *ndev = platform_get_drvdata(pdev);
unregister_netdev(ndev);
// 此时ndev还在,但已经不在内核网络栈里了
// 可以做一些清理工作...
free_netdev(ndev); // 释放内存
return 0;
}
4.4 free_netdev():最后的告别
free_netdev()负责释放alloc_netdev()分配的内存。调用后,ndev指针就变成野指针了,千万别再访问。
核心原则:
- 先unregister,再free。顺序反了会导致内核崩溃。
- free之后,所有对ndev的引用都必须已经清除。
- 不要在中断上下文调用free_netdev(),它可能会睡眠。
4.5 完整生命周期流程图
下面这张图,是我用SVG画的设备生命周期。你看一眼就明白了:
4.6 实战:一个完整的驱动模板
把上面这些串起来,就是一个标准的网络驱动骨架:
#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/platform_device.h>
struct my_priv {
void __iomem *base;
int irq;
spinlock_t lock;
};
static const struct net_device_ops my_netdev_ops = {
.ndo_open = my_open,
.ndo_stop = my_stop,
.ndo_start_xmit = my_start_xmit,
.ndo_set_mac_address = my_set_mac,
// ... 其他操作
};
static void my_setup(struct net_device *ndev)
{
ether_setup(ndev);
ndev->netdev_ops = &my_netdev_ops;
ndev->flags |= IFF_MULTICAST;
}
static int my_probe(struct platform_device *pdev)
{
struct net_device *ndev;
struct my_priv *priv;
int ret;
ndev = alloc_netdev(sizeof(*priv), "myeth%d",
NET_NAME_ENUM, my_setup);
if (!ndev)
return -ENOMEM;
priv = netdev_priv(ndev);
spin_lock_init(&priv->lock);
// 映射IO内存、申请中断等...
ret = register_netdev(ndev);
if (ret) {
free_netdev(ndev);
return ret;
}
platform_set_drvdata(pdev, ndev);
return 0;
}
static int my_remove(struct platform_device *pdev)
{
struct net_device *ndev = platform_get_drvdata(pdev);
unregister_netdev(ndev);
// 释放硬件资源...
free_netdev(ndev);
return 0;
}
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my_net_driver",
},
};
module_platform_driver(my_driver);
my_remove()里先调了free_netdev()再调unregister_netdev(),结果内核直接Oops。嗯,从那以后我就在代码里加了个注释:/* 先注销,再释放!顺序不能错! */
4.7 常见问题与排查
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 注册返回-ENODEV | setup函数没调ether_setup() | 检查setup回调 |
| 设备看不到 | register_netdev()没调用或失败 | 检查返回值 |
| 卸载时崩溃 | unregister/free顺序反了 | 先unregister再free |
| 内存泄漏 | free_netdev()没调用 | 确保remove路径调用了 |
这四个函数,说白了就是网络驱动的「生老病死」。分配是出生,注册是上户口,注销是销户,释放是火化。顺序对了,一切安好;顺序错了,内核给你蓝屏。
我个人建议,写驱动时先把probe/remove的骨架搭好,再填充具体功能。这样不容易漏掉关键步骤。嗯,今天就聊到这儿,下次我们深入聊聊net_device_ops里的那些回调函数。