跳转至

嵌入式Linux驱动开发常见坑点总结

做嵌入式Linux驱动这几年,踩过不少坑。今天就把这些经历整理出来,希望能帮大家少走弯路。整体上来说都是比较简单,对于初学者可能会踩上,避坑。

1. 内存访问相关的坑

直接访问物理地址是大忌

刚接触驱动开发,写了这样的代码:

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这些函数去读写。这样才安全。

ioremap了就一定要iounmap

有些人很容易忽视这个。ioremap之后光顾着用,卸载驱动时就忘了iounmap。长期运行下去,虚拟地址空间就慢慢漏掉了。到后来各种诡异的问题就出现了。

我的经验是养成一个习惯:哪里ioremap,最后就在remove函数里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);  // 直接用未初始化的锁
    // ...
}

结果一跑起来,系统直接挂。原因是那个lock根本没初始化过,随便乱用肯定要出问题。

要这样做才对:

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和睡眠函数不能混用

这也是常犯的错误:

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

spinlock是自旋锁,你持着它的时候,处理器会一直轮询等待。要是你还敢睡眠,那就死锁了——处理器在那儿转圈圈等着锁释放,但你睡着了根本释放不了锁。

如果你的操作确实需要睡眠,就换成mutex:

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

或者把操作拆开,不要在持锁的时候睡眠。


3. 设备模型与Probe的坑

Probe没执行,新手一脸懵

很多人写好了probe函数,加载驱动一看,probe压根没被调用,然后对着代码发呆。

实际上probe什么时候执行,这有讲究。设备和驱动匹配上了才会执行probe。对于platform设备,如果你的设备树里没有对应节点,或者没有通过platform_add_devices注册,那probe就不会运行。

我一般调试的时候会这样查:

# 看驱动有没有加载
cat /proc/modules | grep my_driver

# 看设备有没有被发现
cat /sys/bus/platform/devices

# 看probe有没有跑过
dmesg | grep "my_device"

只要按照这个思路逐步排查,一般都能快速定位问题。

devm函数用起来爽,但也容易掉坑

devm系列函数确实方便,设备卸载时会自动释放资源。但这也是个双刃剑:

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. 中断处理的坑

申请了中断就一定要释放

看过一个哥们儿写的code:

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;  // 这儿就return了,free_irq呢?
    }
    return 0;
}

probe失败了,但中断申请的资源就留在那儿了。后来加载其他驱动的时候,各种奇怪的问题就出现了。

必须这样做:

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;
}

probe和remove要像镜子一样,一一对应。

中断处理函数里不能睡眠

中断处理函数运行在硬中断上下文,这是个特殊的地方——不能做任何可能睡眠的操作。

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

要不用spinlock(也是不能睡眠的,但这样的上下文可以用),要不就把复杂的操作扔到tasklet或work queue里:

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和驱动代码,compatible的名字对不上。结果驱动加载了,但设备从来没被发现过。

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函数不完整,问题一堆

我见过太多remove函数写得不完整的例子。最常见的情况就是只释放了部分资源:

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的镜像,probe里初始化了什么,remove里就得清理什么:

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);
    iounmap(dev->reg_base);
    kfree(dev);

    return 0;
}

卸载后sysfs属性还能访问,会崩溃

这也是个隐藏很深的bug。你定义了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属性文件。结果用户空间的程序有时候还在读这个属性,就读到了已经被释放的内存。boom!

正确的顺序是先删除属性,再释放内存:

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. 实用的调试技巧

遇到问题时,我一般这样排查:

# 驱动有没有加载进去
lsmod | grep driver_name

# 设备文件存不存在
ls -la /dev/my_device

# 驱动和设备有没有匹配
cat /sys/bus/platform/devices

# 设备树是否正确解析
cat /proc/device-tree/my_device/compatible

# 实时查看驱动的打印信息
dmesg -w

# 看中断有没有被触发
cat /proc/interrupts

这些命令虽然简单,但往往能快速定位问题所在。


经验总结

首先是成对操作。ioremap要iounmap,request_irq要free_irq,malloc要free。少一个就是内存泄漏。

其次是错误检查。所有可能失败的函数调用都得检查返回值,特别是那些申请资源的函数。

再就是理解上下文。spinlock不能用在睡眠的地方,中断处理函数不能睡眠。不理解这些会导致很难定位的bug。

还有设备树要对应。DTS和驱动代码一定得匹配,包括compatible字符串。差一个字符都不行。

最后就是完整卸载。remove函数必须和probe函数"镜像对称"。probe里初始化什么,remove里就得清理什么,顺序也很重要。

这些坑大多数都是从实际项目中积累的经验。避开这些,你就能少跟一堆莫名其妙的bug较劲。

欢迎大家补充你们踩过的坑,一起进步!