[特殊字符][特殊字符]Linux驱动开发入门 | 并发与互斥机制详解
文章目录
- 👨💻Linux驱动开发入门 | 并发与互斥机制详解
- 📌为什么驱动中需要并发和互斥控制?
- 💡常见的并发控制机制
- 🔐自旋锁和信号量通俗理解
- 🌀自旋锁(Spinlock)——“厕所排队锁”
- 🚦信号量(Semaphore)——“停车场智能显示器”
- 🆚 自旋锁 vs 信号量
- 读写锁是什么?
- 🚨死锁问题和解决策略
- 一、预防死锁
- 1. 资源一次性分配(破坏请求与保持条件)
- 2. 可剥夺资源(破坏不可剥夺条件)
- 3. 资源有序分配法(破坏循环等待条件)
- 二、避免死锁
- 银行家算法
- 三、死锁检测
- 步骤如下:
- 四、解除死锁
- 1. 剥夺资源
- 2. 撤消进程
- 五、避免死锁的编程实践
- 1. 加锁顺序(Lock Ordering)
- 2. 加锁时限(Try Lock with Timeout)
- 3. 死锁检测机制
- 总结
- 🧪真实驱动例子:互斥访问设备寄存器
- 临界区:
- 🧠Q&A 常见问题
- ✅总结
👨💻Linux驱动开发入门 | 并发与互斥机制详解
📌为什么驱动中需要并发和互斥控制?
我们知道,在多线程或多任务并行执行的操作系统中,比如Linux内核,多个执行单元(线程或中断)可能同时访问共享资源(如全局变量、设备寄存器、缓冲区等),这就带来了“竞态条件(Race Condition)”的风险。
举个简单的例子:
假如两个线程A和B几乎在同一时间读取同一个计数变量x的值为10,然后各自+1并写回。你期望x变为12,但结果可能还是11。
这类问题就需要“互斥”机制来保护——确保同一时刻只能有一个执行单元访问共享资源,其访问的代码区域称为临界区(Critical Section)。
💡常见的并发控制机制
Linux驱动开发中,常见的互斥控制方式有以下几种:
互斥机制 | 特点 | 场景 |
---|---|---|
中断屏蔽 | 禁止中断上下文干扰 | 适用于简单、快速完成的临界区 |
原子操作 | 使用CPU原子指令保证变量操作完整 | 操作变量极少时 |
自旋锁(spinlock) | 自旋等待,适合短时间锁定 | 中断/进程上下文 |
信号量(semaphore) | 可睡眠等待,适合长时间持锁 | 进程上下文,驱动任务中常用 |
互斥锁(mutex) | 是信号量的简化版本 | 一般用于用户态/驱动模块 |
🔐自旋锁和信号量通俗理解
🌀自旋锁(Spinlock)——“厕所排队锁”
把共享资源想象成一个单人厕所。
- 线程A进入厕所,并锁门(获取锁);
- 线程B也想用厕所,只能在门口一直转圈圈(不停检查锁状态);
- A出来后释放锁,B才能进去。
自旋锁适合锁定时间非常短的临界区,因为等待期间线程一直占用CPU,不睡觉!
✅ 优点:
- 实时性好(适合中断上下文)
- 实现简单
❌ 缺点:
- CPU占用率高,锁持有久了会浪费资源
- 不可在临界区使用可能睡眠的代码!
🚦信号量(Semaphore)——“停车场智能显示器”
假设一个停车场有100个车位,信号量就相当于入口处的电子屏:
- 显示“当前车位:20”,车还能进;
- 显示“满”,车就得等;
- 有车离开,车位更新,通知其他等车入场。
信号量适合临界区操作时间较长、可能会阻塞的场景。
✅ 优点:
- 可睡眠等待,不占CPU
- 适合处理资源池问题,如连接池、缓存池等
❌ 缺点:
- 实时性差,不可用于中断处理
- 实现复杂,需考虑死锁
- 锁被短时间持有时,使用信号量就不太适宜了,因为睡眠引起的耗时可能比锁被占用的全部时间还要长。
🆚 自旋锁 vs 信号量
对比项 | 自旋锁 | 信号量 |
---|---|---|
是否睡眠 | ❌ 不可睡眠 | ✅ 可睡眠等待 |
适用上下文 | 中断上下文 | 进程上下文 |
临界区时长 | 极短 | 可长 |
是否允许抢占 | ❌ 不允许(禁抢) | ✅ 允许抢占 |
用于中断中 | ✅ 可以 | ❌ 禁止 |
是否可重入 | ❌ 否 | ✅ 是(看实现) |
在你占用信号量的同时不能占用自旋锁,因为在你等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。
读写锁是什么?
当临界区的一个文件可以被同时读取,但是并不能被同时读和写。如果一个线程在读,另一个线程在写,那么很可能会读取到错误的不完整的数据。读写自旋锁是可以允许对临界区的共享资源进行并发读操作的。但是并不允许多个线程并发读写操作
🚨死锁问题和解决策略
在操作系统或并发编程中,**死锁(Deadlock)**是一个经典问题。本文将带你由浅入深地了解死锁的处理方式,主要包括四种:预防死锁、避免死锁、检测死锁以及解除死锁。
一、预防死锁
死锁产生的四个必要条件是:互斥、不可剥夺、请求与保持、循环等待。
为了预防死锁,我们可以通过破坏其中一个或多个条件来避免死锁的发生。
1. 资源一次性分配(破坏请求与保持条件)
当一个进程申请资源时,必须一次性申请它执行所需的所有资源。如果一次申请不到,就什么也不分配,避免持有部分资源再申请其他资源。
2. 可剥夺资源(破坏不可剥夺条件)
允许系统在资源不足时,强行从某些进程中回收已分配的资源,重新分配给其他更需要的进程。
3. 资源有序分配法(破坏循环等待条件)
为所有资源编号,进程必须按编号递增的顺序申请资源。释放时则按编号递减顺序释放。这样可以避免资源请求形成闭环。
二、避免死锁
相比预防死锁,避免死锁不要求完全避免死锁条件的成立,而是在每次资源分配时判断是否安全。
银行家算法
预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。
这是最经典的死锁避免算法。
- 系统在每次资源分配前,模拟本次资源分配是否会导致系统进入不安全状态。
- 如果安全,则分配资源;否则让进程等待。
三、死锁检测
死锁检测是允许死锁发生,但系统会定期检查是否有死锁存在,一旦检测到就进行处理。
步骤如下:
- 系统记录所有进程与资源的指定一个唯一的号码,构建资源分配图或等待图。
- 检查是否存在环路(循环等待)结构。
- 若有环路,即可判定发生了死锁。
四、解除死锁
当检测到死锁后,需要采取措施解除死锁状态。常见方法如下:
1. 剥夺资源
从非死锁进程中剥夺资源分配给死锁进程,让后者能继续运行,释放资源。
2. 撤消进程
- 终止死锁进程或一些代价较小的进程,释放资源。
- 代价可以依据优先级、运行时间、完成率、业务重要性来评估。
五、避免死锁的编程实践
在多线程编程中(如Java、C++),我们还可以通过一些实际的编程技巧避免死锁:
1. 加锁顺序(Lock Ordering)
确保所有线程在获取多个锁时,始终按照固定顺序获取。例如:线程要获取锁A和锁B,必须先获取编号小的锁A,再获取锁B。
// Thread 1:
synchronized(lockA) {synchronized(lockB) {// do something}
}
// Thread 2: 也必须先获取lockA,再获取lockB
按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁,并对这些锁做适当的排序),但总有些时候是无法预知的。
2. 加锁时限(Try Lock with Timeout)
设置锁获取的超时时间,如果无法在一定时间内获取到锁,就放弃。
if(lock.tryLock(500, TimeUnit.MILLISECONDS)) {try {// do something} finally {lock.unlock();}
} else {// 获取锁失败,执行其他逻辑或重试
}
这种方式可以有效避免长时间等待。
3. 死锁检测机制
针对上面两种不适用的场景。那些不可能实现按序加锁并且锁超时也不可行的场景
使用数据结构记录线程和资源的持有与请求状态,在失败时主动检查是否形成了等待环。
当检测到环路时:
- 某些线程主动释放锁虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁,原因同超时类似,不能从根本上减轻竞争
- 或者优先级较低的线程撤退一段时间再重试
这种方式适合无法提前安排加锁顺序的复杂应用场景。
总结
方法 | 是否允许死锁发生 | 是否易于实现 | 是否影响性能 |
---|---|---|---|
预防死锁 | 否 | 较简单 | 高 |
避免死锁 | 否 | 中 | 中 |
死锁检测 | 是 | 中 | 中 |
解除死锁 | 是 | 复杂 | 低(只在死锁发生时影响) |
🧪真实驱动例子:互斥访问设备寄存器
假设我们要编写一个字符设备驱动,多个进程可能并发调用 read()
操作,访问同一片寄存器区域。
临界区:
static DEFINE_SPINLOCK(my_lock);ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *off) {unsigned long flags;spin_lock_irqsave(&my_lock, flags);// 临界区:访问共享寄存器data = ioread32(dev->reg_base);spin_unlock_irqrestore(&my_lock, flags);return 0;
}
注意:用
spin_lock_irqsave
是因为中断中也可能调用,必须禁止中断防止死锁!
🧠Q&A 常见问题
Q:单核CPU还需要加锁吗?
A:需要!因为即使单核,操作系统依然可以通过抢占调度让线程切换,导致共享变量被多个线程交叉访问。
Q:信号量可以用在中断中吗?
A:不能!因为信号量可能会休眠,而中断处理函数不能休眠,否则整个中断系统会挂死。
Q:spin_lock能不能睡眠?
A:不能!因为它禁止抢占,如果睡眠,系统可能无法调度其他任务,导致死锁。
✅总结
- 多线程 + 共享资源 = 必须互斥
- 自旋锁适合临界区非常短的场景;信号量适合长时间、可睡眠的场景
- 死锁问题复杂,要尽量规避:统一加锁顺序、设置超时、图算法检测
- 在驱动中使用锁时要特别考虑上下文(中断/进程)和是否可休眠