做嵌入式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. 经验总结:避坑的核心原则
回顾这些常见的“坑”,可以总结出几条核心原则来指导我们的开发:
- 成对操作原则:
ioremap/iounmap, request_irq/free_irq, kmalloc/kfree。每一个资源的申请,都必须有明确且可靠的释放路径,缺一不可。
- 错误检查原则:对所有可能失败的函数调用(尤其是资源申请类)进行返回值检查。一个未被处理的错误可能成为后续一系列问题的根源。
- 上下文理解原则:深刻理解代码运行的上下文(进程上下文、中断上下文)。在中断上下文中禁止睡眠,在持有自旋锁时禁止调用可能睡眠的函数。这是写出稳定驱动的基础。
- 精确匹配原则:设备树(DTS)与驱动代码中的配置(如
compatible字符串)必须精确匹配,一字不差。
- 镜像清理原则:驱动模块的
remove 函数必须与 probe 函数镜像对称,以相反的顺序释放所有初始化的资源,并确保在释放资源前,外部已无法访问这些资源。
这些经验大多源于实际项目的教训。避开它们,你就能节省大量与那些“莫名其妙”的 bug 纠缠的时间,更专注于驱动逻辑本身的实现。希望这些总结能对大家的开发工作有所帮助。如果你想深入探讨某个具体问题,或者分享自己的踩坑经验,欢迎在 云栈社区 的技术论坛板块与其他开发者一起交流学习,那里汇聚了许多乐于分享的同行,共同沉淀实用的 技术文档 与避坑指南。
