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:初始化回调函数。这里你要设置好设备的操作函数集、特性标志等。
我的经验:setup函数里别忘了设置netdev_opsethtool_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没设置完整
  • 内存不足
注意:register_netdev()调用之后,内核就可能开始调用你的ndo_open()了。所以注册之前,硬件资源(中断、IO内存)必须已经准备好。我曾经在注册后才去申请中断,结果用户态一配IP,驱动直接崩溃。

注册成功后,你可以用ip link看到新设备。但此时它还是DOWN状态,需要用户手动ip link set eth0 up才会触发你的ndo_open()

4.3 unregister_netdev():优雅地退出

设备移除时,先调用unregister_netdev()。这个函数会:

  1. 通知内核网络子系统:这个设备要走了
  2. 关闭设备(如果还在运行)
  3. 释放所有内核资源(如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画的设备生命周期。你看一眼就明白了:

网络设备生命周期 alloc_netdev() 初始化私有数据 register_netdev() 正常运行 (ndo_open等) unregister_netdev() free_netdev() 分配内存和私有数据 设置硬件资源、锁等 加入内核网络子系统 收发数据包 从内核移除,但内存还在 释放内存,彻底结束

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里的那些回调函数。


专注资料整理