Linux并发与竞争
原子操作
原子操作能够在无锁条件下实现线程安全操作,避免多个线程同时修改变量导致数据竞争。比较适用于简单的计数或标志变量等小型共享数据。但是原子操作只能保证单个原子变量的原子性,不能保护复杂数据结构。并且虽然其不会导致线程阻塞,但可能导致活锁,即多个线程不断重试并竞争CPU资源。
驱动中通过原子操作来保护进程的方式为:
- 在设备结构体中声明原子变量
1 |
|
- open函数中判断原子变量的值来检查目标设备有无被别的应用调用
1 |
|
这段代码的逻辑是,atomic_dec_and_test
将原子变量递减并检查其是否为0.如果为0,表示当前线程获得访问权限;如果递减后原子变量仍大于0,说明已经有其他线程持有权限,当前线程不能访问。如果当前线程可以访问,则将原子变量恢复原值,避免影响计数。
在驱动文件关闭时,释放原子变量:
1 |
|
在驱动入口函数内,初始化原子变量为1:
1 |
|
自旋锁(Spinlock)
这是一种轻量级的锁,通过不断循环检查锁的状态来等待资源,而非让线程进入睡眠。
- 线程在等待锁时会不断检查,不释放CPU时间
- 适用于临界区执行时间短的场景,否则浪费CPU资源
- 需要显式加锁/解锁,适用于保护复杂的数据结构
自旋锁保护的是临界区,即spin_lock_irqsave()
到spin_unlock_irqrestore()
之间的
代码所操作的共享变量。
自旋锁适用于临界区执行时间短或锁的持有时间短的情况,或多核CPU场景。操作自旋锁时,一般使用spin_lock_irqsave
和spin_unlock_irqrestore
函数,其会关闭中断,避免IRQ抢占锁以造成竞争问题。解锁时函数会一并恢复中断,防止中断永久关闭。
1 |
|
驱动通过自旋锁来保护线程的方法:
- 设备结构体中声明设备状态和自旋锁:
1 |
|
- open函数中检查锁的状态
1 |
|
- release函数中更新锁的状态
1 |
|
- 设备初始化时初始化自旋锁
1 |
|
问题:为什么不在设备(线程)被使用后直接上锁,而是上锁后又解锁? 这样不就防止其他线程访问了吗?
简单来说,锁的作用不是用来“锁住整个设备”,而是用来“保护对共享资源的访问”。
在 led_open()
里,我们用自旋锁来保证检查 & 修改设备状态这段代码是原子的,而不是用来锁住整个设备的访问权限。
假设我们在 gpioled.dev_stats++
之后不解锁,而是让锁一直保持不释放,其他线程就无法再进入 led_open()
,但这带来了两个大问题:
- 整个设备会被锁死,这个设备无法再被其他线程使用
led_release
无法再获取锁,设备无法被释放,造成死锁。
也就是说,锁保护的不是这个设备,而是设备中的某个量(这里就是gpioled.dev_stats
),这个量用于检查该设备是否已经/正在被其他线程使用。
信号量(Semaphore)
和自旋锁不同,信号量可以阻塞线程,如果获取不到信号量,线程将会进入睡眠以释放CPU资源,适用于长时间访问资源的场景(如访问文件、操作设备等)。线程会进入睡眠这点涉及上下文切换,有一定的开销。
1 |
|
互斥体(Mutex)
和信号量不同,互斥体中只有一个线程能获得锁(信号量允许多个线程同时访问),其他线程会阻塞(睡眠)。
- 结构体中声明互斥体
1 |
|
- open函数中获取互斥体(
interruptible
表示该函数可被信号打断)
1 |
|
- release时释放互斥锁
1 |
|
- 初始化设备时初始化互斥锁
1 |
|
对比
假设你去银行🏦取钱:
-
🔢 原子变量(Atomic):银行门口有一个“当前排队人数”显示屏,每个人来都可以安全地加 1 或减 1,但不会控制谁去办业务(只适用于简单计数)。
-
🔄 自旋锁(Spinlock):你去银行取钱,发现柜台有人,你站在那里等,直到轮到你(CPU 忙等)。
-
🔢 信号量(Semaphore):银行有多个柜台,你可以去任何空闲的柜台办理业务(多个线程同时访问)。
-
🛑 互斥体(Mutex):你去银行取钱,发现柜台有人,你去等候区坐着,等柜台空了再去(线程睡眠)。