嵌入式Linux驱动开发常见坑点总结¶
做嵌入式Linux驱动这几年,踩过不少坑。今天就把这些经历整理出来,希望能帮大家少走弯路。整体上来说都是比较简单,对于初学者可能会踩上,避坑。
1. 内存访问相关的坑¶
直接访问物理地址是大忌¶
刚接触驱动开发,写了这样的代码:
这在裸机上可能没问题,但在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和睡眠函数不能混用¶
这也是常犯的错误:
spinlock是自旋锁,你持着它的时候,处理器会一直轮询等待。要是你还敢睡眠,那就死锁了——处理器在那儿转圈圈等着锁释放,但你睡着了根本释放不了锁。
如果你的操作确实需要睡眠,就换成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里写的是:
驱动里却写的是:
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较劲。
欢迎大家补充你们踩过的坑,一起进步!