3. 设备注册与注销:设备节点重复注册、驱动卸载时资源未完全释放导致系统崩溃

设备注册与注销,听起来像是基础操作对吧?但说实话,我见过太多系统崩溃,根源就在这两个环节。今天咱们就聊聊,为什么一个简单的register_deviceunregister_device,能搞出这么多幺蛾子。

3.1 设备节点重复注册:你注册了两次,系统就懵了

先说说重复注册的问题。你想想看,一个设备节点就像一个人的身份证号。如果你给同一个人发了两个不同的身份证号,系统该信哪个?

在PX4里,设备节点通常通过register_driverregister_class_dev这类函数注册。如果你不小心调用了两次,内核会返回一个错误码,但很多新手直接忽略了返回值。

⚠️ 警告: 忽略注册函数的返回值,是驱动开发中最常见的低级错误之一。内核已经告诉你“这个节点已经存在了”,你却当没看见。

我遇到过这样一个案例:有个同事写了一个传感器驱动,在probe函数里注册了设备节点,又在init函数里注册了一次。结果系统启动时,第二个注册覆盖了第一个,但第一个注册的资源没有释放。最后导致设备节点指向了错误的内存地址,一读数据就崩溃。

正确的做法是什么?很简单:

/* 错误示范:不检查返回值 */
register_driver(&my_driver);

/* 正确示范:检查返回值 */
int ret = register_driver(&my_driver);
if (ret != 0) {
    PX4_ERR("Failed to register driver: %d", ret);
    return ret;
}

嗯,这里要注意一点:register_driver返回-EEXIST时,不代表系统还能正常工作。你得主动处理这个错误,要么卸载旧的驱动,要么直接报错退出。

3.2 资源未完全释放:你走了,但东西没带走

驱动卸载时资源未释放,这个问题更隐蔽。说白了,就是驱动被卸载了,但它占用的内存、中断、定时器、工作队列等资源还留在系统里。

为什么会这样?我总结了一下,主要有三个原因:

  • 忘记释放动态分配的内存:用mallocnew分配的内存,在remove函数里没有freedelete
  • 中断处理函数没注销:驱动卸载了,但中断还在触发,系统会跳转到已经释放的内存地址执行——直接崩溃。
  • 工作队列或定时器还在运行:这些异步任务可能在驱动卸载后还在执行,访问已经释放的数据结构。
💡 核心原则: 驱动卸载时,必须按照“申请的反顺序”释放所有资源。你申请的顺序是A→B→C,释放的顺序就得是C→B→A。

我曾经在调试一个GPS驱动时,发现卸载驱动后系统偶尔会死机。查了两天才找到原因:驱动里注册了一个定时器,用于定期读取GPS数据。但在remove函数里,我先释放了数据缓冲区,然后才注销定时器。结果定时器触发时,缓冲区已经没了,系统直接访问了非法地址。

正确的卸载顺序应该是:

static int my_driver_remove(struct device *dev)
{
    /* 第一步:停止所有异步操作 */
    hrt_cancel(&my_timer);
    work_cancel(&my_work);

    /* 第二步:注销中断 */
    irq_unregister(my_irq);

    /* 第三步:注销设备节点 */
    unregister_driver(&my_driver);

    /* 第四步:释放动态内存 */
    free(my_data);

    return 0;
}

3.3 一个典型的崩溃场景分析

咱们来看一个真实案例。有个飞控的PWM输出驱动,卸载时总是导致系统重启。我接手后,把代码翻了个底朝天,发现问题出在下面这个函数里:

static int pwm_remove(struct device *dev)
{
    /* 先释放了pwm_channel数组 */
    free(pwm_channels);

    /* 然后才停止PWM输出 */
    pwm_stop();

    /* 最后注销中断 */
    irq_unregister(pwm_irq);

    return 0;
}

你看出来问题了吗?pwm_stop()函数内部会访问pwm_channels数组,但数组已经被释放了。这就是典型的“释放后使用”漏洞。

修复方法很简单:把free(pwm_channels)放到最后执行。

🔧 避坑指南: 我曾经在代码审查时,要求团队所有驱动必须写一个“资源释放检查清单”。每次提交代码前,对照清单逐项检查。这个习惯救了我们好几次。

3.4 如何系统性地避免这些问题?

说了这么多坑,咱们来点实用的。我总结了一套检查流程,你可以直接拿来用:

  1. 注册时:检查所有注册函数的返回值,一个都不能漏。
  2. 卸载时:先停止所有异步操作(定时器、工作队列、DMA),再注销中断,然后注销设备节点,最后释放内存。
  3. 测试时:反复加载卸载驱动至少100次,观察系统是否稳定。
  4. 代码审查时:重点检查proberemove函数的对称性。

下面这张图,是我画的一个资源管理流程图,你可以参考一下:

设备驱动资源管理流程图 注册流程 (probe) 1. 分配内存 / 初始化数据结构 2. 注册中断处理函数 3. 注册设备节点 4. 启动定时器 / 工作队列 卸载流程 (remove) 4. 停止定时器 / 工作队列 3. 注销设备节点 2. 注销中断处理函数 1. 释放内存 / 清理数据结构 注册与卸载必须完全对称 顺序相反,一一对应

说白了,设备注册与注销的核心就一句话:你怎么申请的资源,就怎么释放回去,顺序反过来。这个原则听起来简单,但实际做起来,需要你在写代码时时刻保持清醒。

嗯,最后再提醒一句:如果你在调试时发现系统莫名其妙地崩溃,先别急着怀疑硬件。回头检查一下你的remove函数,八成是资源没释放干净。


专注资料整理