找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2618

积分

0

好友

368

主题
发表于 3 小时前 | 查看: 0| 回复: 0

做嵌入式Linux驱动开发这几年,确实踩过不少坑。今天把一些典型的、看似简单但容易绊倒初学者的经历整理出来,希望能帮大家少走些弯路,避开那些能“把人绕晕”的陷阱。

1. 内存访问:别把物理地址当虚拟地址用

直接访问物理地址?系统崩溃的捷径

很多开发者从单片机(MCU)转向Linux驱动时,会下意识地写出这样的代码:

unsigned int *reg = (unsigned int *)0x10000000;
*reg = 0x01;

这在裸机环境下或许可行,但在运行着完整虚拟内存管理的Linux系统中,这无异于“盲人摸象”。你操作的只是一个虚拟地址空间的随机位置,写入的数据去向不明,轻则数据丢失,重则直接导致内核崩溃。

正确的做法是使用内核提供的地址映射接口:

void __iomem *reg_base = ioremap(0x10000000, 0x1000);
if (!reg_base) {
    pr_err("ioremap failed\n");
    return -ENOMEM;
}
writel(0x01, reg_base);
iounmap(reg_base);

ioremap 函数会为你建立物理地址到内核虚拟地址的映射,之后使用 readl/writel 这类专门针对IO内存的操作函数进行读写,安全又规范。

ioremap之后,别忘了iounmap

这是另一个容易被忽视的资源泄漏点。ioremap 后光顾着使用,在驱动卸载时却忘了对应的 iounmap。长此以往,内核的虚拟地址空间会被慢慢“吃掉”,最终可能引发各种难以排查的诡异问题。

养成良好的习惯:在驱动模块的 remove 函数中,必须为每一个 ioremap 调用配对一个 iounmap。配对操作,缺一不可。

2. 并发与同步:锁的初始化与使用禁忌

未初始化的锁,一用就“炸”

曾经在一个项目中看到这样的代码片段:

struct my_device {
    spinlock_t lock;
    int value;
};

static int device_open(struct inode *inode, struct file *filp) {
    struct my_device *dev = container_of(inode->i_cdev, struct my_device, cdev);
    spin_lock(&dev->lock);  // 直接使用未初始化的锁
    // ...
}

结果驱动一加载运行,系统立刻挂起。原因很简单,结构体中的 spinlock_t lock 只是一个变量声明,并没有被初始化成一个有效的自旋锁。

正确的做法是在初始化设备(如在 probe 函数中)时显式初始化锁:

static int device_probe(struct platform_device *pdev) {
    struct my_device *dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    spin_lock_init(&dev->lock);  // 必须初始化
    return 0;
}

无论是自旋锁(spinlock_t)、互斥锁(mutex)还是信号量(semaphore),使用前都必须先初始化,没有例外。

持着自旋锁去睡眠?死锁在等你

这是一个经典的错误模式:

spin_lock(&dev->lock);
msleep(100);  // 大错特错!
spin_unlock(&dev->lock);

自旋锁(spin_lock)的设计理念是“忙等待”。持有它时,如果锁不可用,CPU会一直在那里循环检查,不会让出处理器。如果你在持有自旋锁期间调用了 msleep 这类会导致睡眠的函数,那就会发生死锁——CPU在忙等锁释放,但你的进程却睡着了,永远无法执行到解锁的代码。

如果需要执行可能引起睡眠的操作,请换用互斥锁(mutex):

mutex_lock(&dev->mutex);
msleep(100);  // 这样就没问题了
mutex_unlock(&dev->mutex);

或者,将耗时的、可能睡眠的操作移到锁的保护范围之外。

3. 设备模型与Probe:匹配与资源管理

为什么我的Probe函数没执行?

很多新手写好了 probe 函数,满怀期待地加载驱动,却发现 dmesg 里没有任何 probe 被调用的痕迹。问题出在“匹配”上。

probe 函数的执行条件是:设备驱动成功匹配。对于Platform设备,如果你的设备树(DTS)中没有对应的节点,或者没有通过 platform_add_devices 等API注册设备,驱动就找不到它要服务的对象,probe 自然不会执行。

调试时,可以按这个顺序排查:

# 看驱动模块是否成功加载
cat /proc/modules | grep my_driver

# 查看总线上的设备,看你的设备是否被枚举
cat /sys/bus/platform/devices

# 查看内核日志,寻找设备或驱动的踪迹
dmesg | grep "my_device"

小心devm_*系列函数的“自动”陷阱

devm_kzalloc, devm_ioremap 等函数非常方便,它们会与设备(struct device)的生命周期绑定,当设备被移除时自动释放资源。但这把“双刃剑”也可能伤到自己:

int *temp = devm_kzalloc(dev, sizeof(int), GFP_KERNEL);
*temp = 100;
g_temp = temp;  // 存到全局变量里
// 驱动卸载,temp被自动释放
// 但g_temp还指向那片内存,use-after-free bug就来了

所以,如果某个资源需要在驱动卸载后继续存在(例如被其他模块引用),就不要使用 devm_ 系列函数,而应使用 kmalloc/kzalloc 等传统函数,并手动管理其生命周期。

4. 中断处理:申请与释放的配对艺术

中断申请了,就一定要记得释放

看看这段有问题的 probe 代码:

static int device_probe(struct platform_device *pdev) {
    int irq = platform_get_irq(pdev, 0);
    request_irq(irq, my_irq_handler, IRQF_SHARED, "my_device", dev);

    if (some_error_condition) {
        return -EINVAL;  // 这里直接返回了,free_irq呢?
    }
    return 0;
}

如果 some_error_condition 成立,函数提前返回,那么已经申请的中断资源就泄露了。这可能会导致后续加载的其他驱动无法申请到该中断,或者产生其他不可预知的问题。

正确的做法是确保资源申请的每个失败路径都得到妥善清理,并在 remove 函数中镜像释放:

static int device_probe(struct platform_device *pdev) {
    int irq = platform_get_irq(pdev, 0);
    if (irq < 0) return irq;

    int ret = request_irq(irq, my_irq_handler, IRQF_SHARED, "my_device", dev);
    if (ret) return ret;

    dev->irq = irq;  // 记录下来,后面要用
    return 0;
}

static int device_remove(struct platform_device *pdev) {
    struct my_device *dev = platform_get_drvdata(pdev);
    free_irq(dev->irq, dev);  // 必须释放
    return 0;
}

中断上下文的“戒律”:禁止睡眠

中断处理函数运行在“硬中断上下文”,这是一个非常受限的环境——不能进行任何可能导致睡眠的操作

static irqreturn_t my_irq_handler(int irq, void *dev_id) {
    mutex_lock(&dev->mutex);  // 错!中断上下文不能用mutex(可能睡眠)
    // ...
}

在中断处理函数中,如果需要同步,只能使用不会睡眠的自旋锁(spin_lock)。如果处理逻辑复杂或耗时,应该将主要工作推送到下半部机制中,如 Tasklet 或 Workqueue:

static irqreturn_t my_irq_handler(int irq, void *dev_id) {
    spin_lock(&dev->lock);
    // 只做最必要的快速处理,例如清除中断标志、记录状态
    queue_work(dev->workqueue, &dev->work); // 将耗时工作放入工作队列
    spin_unlock(&dev->lock);
    return IRQ_HANDLED;
}

static void my_work_handler(struct work_struct *work) {
    // 复杂的、可能访问睡眠锁、可能阻塞的操作在这里安全执行
}

5. 设备树(DTS)的“一字千金”

Compatible字段:差一个字符都不行

这是新手最容易犯的配置错误之一。设备树(DTS)中定义的兼容性字符串,必须与驱动代码中的完全一致,否则无法匹配。

DTS 中写的是:

my_device@10000000 {
    compatible = "mycompany,my-device-v1";
    reg = <0x10000000 0x1000>;
};

驱动里却匹配了另一个版本:

static const struct of_device_id my_of_match_table[] = {
    { .compatible = "mycompany,my-device-v2" },  // 版本号对不上!
    { }
};

这样一来,即使驱动和设备都已就位,probe 函数也永远不会被调用。所以,每次修改兼容性字符串时,一定要仔细核对两边。

设备树节点引用:用完记得“放手”

使用 of_find_compatible_node 等函数查找到设备树节点后,系统会为其增加引用计数。使用完毕后,必须调用 of_node_put 来减少引用计数:

struct device_node *node = of_find_compatible_node(NULL, NULL, "mycompany,child");
if (node) {
    // 使用node做一些操作
    of_node_put(node);  // 必须释放引用
}

如果忘记 of_node_put,该节点占用的内存将永远不会被释放,虽然短时间内可能看不出影响,但长期运行会导致内核内存缓慢泄漏。

6. 驱动卸载:清理工作要“镜像”进行

Remove函数:Probe的“完美倒影”

驱动卸载时,remove 函数必须完整地清理 probe 函数中初始化的所有资源。常见的错误是只释放了部分资源:

static int device_remove(struct platform_device *pdev) {
    struct my_device *dev = platform_get_drvdata(pdev);
    kfree(dev);
    return 0;
    // 但之前申请的free_irq、iounmap都没做!
}

这样卸载后,中断处理函数可能还在异常运行,IO内存映射仍然占用着地址空间,为系统埋下了隐患。

remove 函数应该是 probe 函数的镜像,遵循“后申请的先释放”原则通常是个好习惯:

static int device_remove(struct platform_device *pdev) {
    struct my_device *dev = platform_get_drvdata(pdev);

    free_irq(dev->irq, dev);
    misc_deregister(&dev->miscdev); // 如果注册了misc设备
    iounmap(dev->reg_base);
    kfree(dev);

    return 0;
}

Sysfs属性:先删除,再释放内存

这个问题比较隐蔽。假设你定义了一个 sysfs 属性:

static ssize_t show_value(struct device *dev,
                         struct device_attribute *attr, char *buf) {
    struct my_device *my_dev = dev_get_drvdata(dev);
    return sprintf(buf, "%d\n", my_dev->value);
}

如果在 remove 函数中先 kfree(my_dev),然后再删除 sysfs 属性文件,那么在删除属性文件之前,用户空间的程序可能还在并发地读取该属性。这时,show_value 函数就会访问到一个已经被释放的 my_dev 指针,导致内核崩溃(Use-After-Free)。

正确的顺序是:先断绝外部的访问途径,再释放内部资源。

static int device_remove(struct platform_device *pdev) {
    struct my_device *dev = platform_get_drvdata(pdev);
    device_remove_file(&pdev->dev, &dev_attr_value);  // 先删除属性文件
    kfree(dev);  // 再释放内存
    return 0;
}

7. 实用的调试技巧:快速定位问题

当驱动行为异常时,下面这些简单的命令组合往往能帮你快速缩小排查范围:

# 1. 检查驱动模块是否成功加载
lsmod | grep driver_name

# 2. 检查设备文件是否创建成功
ls -la /dev/my_device

# 3. 查看总线上设备与驱动的匹配状态
cat /sys/bus/platform/devices
cat /sys/bus/platform/drivers

# 4. 确认设备树节点是否被正确解析
cat /proc/device-tree/my_device/compatible

# 5. 实时追踪内核打印信息(最重要的手段)
dmesg -w

# 6. 查看中断统计,判断中断是否被触发
cat /proc/interrupts

8. 经验总结:避坑的核心原则

回顾这些常见的“坑”,可以总结出几条核心原则来指导我们的开发:

  1. 成对操作原则ioremap/iounmap, request_irq/free_irq, kmalloc/kfree。每一个资源的申请,都必须有明确且可靠的释放路径,缺一不可。
  2. 错误检查原则:对所有可能失败的函数调用(尤其是资源申请类)进行返回值检查。一个未被处理的错误可能成为后续一系列问题的根源。
  3. 上下文理解原则:深刻理解代码运行的上下文(进程上下文、中断上下文)。在中断上下文中禁止睡眠,在持有自旋锁时禁止调用可能睡眠的函数。这是写出稳定驱动的基础。
  4. 精确匹配原则:设备树(DTS)与驱动代码中的配置(如compatible字符串)必须精确匹配,一字不差。
  5. 镜像清理原则:驱动模块的 remove 函数必须与 probe 函数镜像对称,以相反的顺序释放所有初始化的资源,并确保在释放资源前,外部已无法访问这些资源。

这些经验大多源于实际项目的教训。避开它们,你就能节省大量与那些“莫名其妙”的 bug 纠缠的时间,更专注于驱动逻辑本身的实现。希望这些总结能对大家的开发工作有所帮助。如果你想深入探讨某个具体问题,或者分享自己的踩坑经验,欢迎在 云栈社区 的技术论坛板块与其他开发者一起交流学习,那里汇聚了许多乐于分享的同行,共同沉淀实用的 技术文档 与避坑指南。

低电量与满电量对比动图




上一篇:Spring MVC Controller 逻辑边界终极指南:到底该写什么?
下一篇:Kubernetes (K8s) 详解:容器编排核心架构与组件工作原理
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 16:12 , Processed in 0.235879 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表