关注+星标公众号,不容错过精彩

作者:HywelStar
做嵌入式Linux驱动这几年,踩过不少坑。今天就把这些经历整理出来,希望能帮大家少走弯路。整体上来说都是比较简单,对于初学者可能会踩上,避坑。none !important
刚接触驱动开发,因为接触过MCU,写了这样的代码:none !important
unsigned int *reg = (unsigned int *)0x10000000;*reg = 0x01;这在裸机上可能没问题,但在Linux里不行。你写的是虚拟地址空间,直接转个物理地址就写入,那写的内容根本不知道去哪儿了,轻则数据丢失,重则整个系统崩溃。none !important
正确的方法得这样做:none !important
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这些函数去读写。这样才安全。none !important
有些人很容易忽视这个。ioremap之后光顾着用,卸载驱动时就忘了iounmap。长期运行下去,虚拟地址空间就慢慢漏掉了。到后来各种诡异的问题就出现了。none !important
经验是养成一个习惯:哪里ioremap,最后就在remove函数里iounmap。配对操作,缺一不可。none !important
之前在一个项目里见过这样的代码:none !important
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根本没初始化过,随便乱用肯定要出问题。none !importantnone !important
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;}所有的锁、信号量这些同步原语,用之前都得先初始化。没有例外。none !important
这也是常犯的错误:none !important
spin_lock(&dev->lock);msleep(100); // 大错特错!spin_unlock(&dev->lock);spinlock是自旋锁,你持着它的时候,处理器会一直轮询等待。要是你还敢睡眠,那就死锁了——处理器在那儿转圈圈等着锁释放,但你睡着了根本释放不了锁。none !important
如果操作确实需要睡眠,就换成mutex:none !important
mutex_lock(&dev->mutex);msleep(100); // 这样就没问题了mutex_unlock(&dev->mutex);或者把操作拆开,不要在持锁的时候睡眠。none !important
很多人写好了probe函数,加载驱动一看,probe压根没被调用,然后对着代码发呆。none !important
实际上probe什么时候执行,这有讲究。设备和驱动匹配上了才会执行probe。对于platform设备,如果你的设备树里没有对应节点,或者没有通过platform_add_devices注册,那probe就不会运行。none !important
一般调试的时候会这样查:none !important
# 看驱动有没有加载cat /proc/modules | grep my_driver# 看设备有没有被发现cat /sys/bus/platform/devices# 看probe有没有跑过dmesg | grep "my_device"只要按照这个思路逐步排查,一般都能快速定位问题。none !important
devm系列函数确实方便,设备卸载时会自动释放资源。但这也是个双刃剑:none !important
int *temp = devm_kzalloc(dev, sizeof(int), GFP_KERNEL);*temp = 100;g_temp = temp; // 存到全局变量里// 驱动卸载,temp被自动释放// 但g_temp还指向那片内存,use-after-free bug就来了所以如果这块资源需要在驱动卸载后仍然存活,就别用devm,老老实实用kmalloc/kzalloc,手动管理生命周期。none !important
看下面一段code:none !important
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失败了,但中断申请的资源就留在那儿了。后来加载其他驱动的时候,各种奇怪的问题就出现了。none !important
必须这样做:none !important
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要像镜子一样,一一对应。none !important
中断处理函数运行在硬中断上下文,这是个特殊的地方——不能做任何可能睡眠的操作。none !important
static irqreturn_t my_irq_handler(int irq, void *dev_id) { mutex_lock(&dev->mutex); // 错!中断上下文不能用mutex // ...}要不用spinlock(也是不能睡眠的,但这样的上下文可以用),要不就把复杂的操作扔到tasklet或work queue里:none !important
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) { // 复杂的、可能睡眠的操作放这儿}新手写DTS和驱动代码,compatible的名字对不上。结果驱动加载了,但设备从来没被发现过。none !important
DTS里写的是:none !important
my_device@10000000 { compatible = "mycompany,my-device-v1"; reg = <0x10000000 0x1000>;};驱动里却写的是:none !important
static conststruct of_device_id my_of_match_table[] = { { .compatible = "mycompany,my-device-v2" }, // 版本号不一样! { }};这两个对不上,probe永远别想执行。所以一定得仔细对。none !important
用of_find_compatible_node找节点的时候,用完了一定要of_node_put:none !important
struct device_node *node = of_find_compatible_node(NULL, NULL, "mycompany,child");if (node) { // 用node of_node_put(node); // 必须释放}of_node_put是在减引用计数。不释放的话,这块内存就一直占着,虽然看不出什么大问题,但长期运行会慢慢漏掉。none !important
见过太多remove函数写得不完整的例子。最常见的情况就是只释放了部分资源:none !important
static int device_remove(struct platform_device *pdev) {struct my_device *dev = platform_get_drvdata(pdev); kfree(dev); return 0; // 但free_irq、iounmap都没做}结果卸载驱动之后,中断处理函数还在那儿跑,IO内存映射还在那儿占着。一旦有其他驱动或代码试图用这些资源,就要出问题。none !important
remove函数应该是probe的镜像,probe里初始化了什么,remove里就得清理什么:none !important
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;}这也是个隐藏很深的bug。定义了sysfs属性:none !important
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!none !important
正确的顺序是先删除属性,再释放内存:none !important
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;}遇到问题时,我一般这样排查:none !important
# 驱动有没有加载进去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这些命令虽然简单,但往往能快速定位问题所在。none !important
首先是成对操作。ioremap要iounmap,request_irq要free_irq,malloc要free。少一个就是内存泄漏。none !important其次是错误检查。所有可能失败的函数调用都得检查返回值,特别是那些申请资源的函数。none !important再就是理解上下文。spinlock不能用在睡眠的地方,中断处理函数不能睡眠。不理解这些会导致很难定位的bug。none !important还有设备树要对应。DTS和驱动代码一定得匹配,包括compatible字符串。差一个字符都不行。none !important最后就是完整卸载。remove函数必须和probe函数"镜像对称"。probe里初始化什么,remove里就得清理什么,顺序也很重要。none !important
这些坑大多数都是从实际项目中积累的经验。避开这些,你就能少跟一堆莫名其妙的bug较劲。
往期推荐
嵌入式软件面试八股文(四) - Linux 内核驱动篇
嵌入式软件面试八股文(三) - 数据结构
嵌入式软件面试八股文(二) - 操作系统
嵌入式软件面试八股文(一)-C语言基础篇
面试技巧-1.STAR法则
闲聊嵌入式
如何写好嵌入式软件简历
组播进阶:加组控制与实战踩坑
单播、广播、组播到底有什么区别?
流媒体服务器搭建指南
戳“阅读原文”一起来充电吧!