Linux并发与竞争

原子操作

原子操作能够在无锁条件下实现线程安全操作,避免多个线程同时修改变量导致数据竞争。比较适用于简单的计数或标志变量等小型共享数据。但是原子操作只能保证单个原子变量的原子性,不能保护复杂数据结构。并且虽然其不会导致线程阻塞,但可能导致活锁,即多个线程不断重试并竞争CPU资源。

驱动中通过原子操作来保护进程的方式为:

  1. 在设备结构体中声明原子变量
1
2
3
4
struct gpioled_dev{
......
atomic_t lock; /* 原子变量 */
}
  1. open函数中判断原子变量的值来检查目标设备有无被别的应用调用
1
2
3
4
5
6
7
static int led_open(...)
{
if (!atomic_dec_and_test(&gpioled.lock)){
atomic_inc(&gpioled.lock);
return -EBUSY;
}
}

这段代码的逻辑是,atomic_dec_and_test将原子变量递减并检查其是否为0.如果为0,表示当前线程获得访问权限;如果递减后原子变量仍大于0,说明已经有其他线程持有权限,当前线程不能访问。如果当前线程可以访问,则将原子变量恢复原值,避免影响计数。

在驱动文件关闭时,释放原子变量:

1
2
3
4
5
static int led_release(...)
{
......
atomic_inc(&dev->lock);
}

在驱动入口函数内,初始化原子变量为1:

1
2
3
4
5
6
static int __init led_init(void)
{
...

atomic_set(&gpioled.lock, 1);
}

自旋锁(Spinlock)

这是一种轻量级的锁,通过不断循环检查锁的状态来等待资源,而非让线程进入睡眠。

  • 线程在等待锁时会不断检查,不释放CPU时间
  • 适用于临界区执行时间短的场景,否则浪费CPU资源
  • 需要显式加锁/解锁,适用于保护复杂的数据结构

自旋锁保护的是临界区,即spin_lock_irqsave()spin_unlock_irqrestore()之间的
代码所操作的共享变量。

自旋锁适用于临界区执行时间短或锁的持有时间短的情况,或多核CPU场景。操作自旋锁时,一般使用spin_lock_irqsavespin_unlock_irqrestore函数,其会关闭中断,避免IRQ抢占锁以造成竞争问题。解锁时函数会一并恢复中断,防止中断永久关闭。

1
2
3
4
5
6
7
8
9
函数声明:
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
函数功能:
1. 保存本地中断状态
2. 关闭本地中断
3. 获取自旋锁
参数说明:
lock:被定义且初始化过的锁;
flags:保存本地中断状态;

驱动通过自旋锁来保护线程的方法:

  1. 设备结构体中声明设备状态和自旋锁:
1
2
3
4
5
strcut gpioled_dev {
......
int dev_stats; /* 设备状态,0:未使用;>0:已经被使用 */
spinlock_t lock; /* 自旋锁 */
}
  1. open函数中检查锁的状态
1
2
3
4
5
6
7
8
9
10
11
static int led_open(...)
{
......
spin_lock_irqsave(&gpioled.lock ,flags); /* 上锁 */
if (gpioled.dev_stats){ /* 如果设备已经被其他线程使用 */
spin_unlock_irqrestore(&gpioled.lock, flags); /* 解锁 */
return -EBUSY; /* 设备正忙 */
}
gpioled.dev_stats++; /* 如果设备未打开,则更新设备状态为已经打开 */
spin_unlock_irqrestore(&gpioled.lock, flags); /* 解锁 */
}
  1. release函数中更新锁的状态
1
2
3
4
5
6
7
8
9
static int led_release(...)
{
......
spin_lock_irqsave(&dev->lock, flags); /* 上锁 */
if (dev->dev_stats){
dev->dev_stats--; /* 更新设备状态至未使用 */
}
spin_unlock_irqrestore(&dev->lock, flags); /* 解锁 */
}
  1. 设备初始化时初始化自旋锁
1
2
3
4
5
static int __init led_init(void)
{
......
spin_lock_init(&gpioled.lock);
}

问题:为什么不在设备(线程)被使用后直接上锁,而是上锁后又解锁? 这样不就防止其他线程访问了吗?
简单来说,锁的作用不是用来“锁住整个设备”,而是用来“保护对共享资源的访问”。
led_open() 里,我们用自旋锁来保证检查 & 修改设备状态这段代码是原子的,而不是用来锁住整个设备的访问权限。

假设我们在 gpioled.dev_stats++ 之后不解锁,而是让锁一直保持不释放,其他线程就无法再进入 led_open(),但这带来了两个大问题:

  1. 整个设备会被锁死,这个设备无法再被其他线程使用
  2. led_release无法再获取锁,设备无法被释放,造成死锁。

也就是说,锁保护的不是这个设备,而是设备中的某个量(这里就是gpioled.dev_stats),这个量用于检查该设备是否已经/正在被其他线程使用。

信号量(Semaphore)

和自旋锁不同,信号量可以阻塞线程,如果获取不到信号量,线程将会进入睡眠以释放CPU资源,适用于长时间访问资源的场景(如访问文件、操作设备等)。线程会进入睡眠这点涉及上下文切换,有一定的开销。

1
2
3
4
5
6
struct semaphore sem;
sema_init(&sem, 1); // 初始化信号量,初始值为1(类似互斥锁)

down(&sem); // 🔒 获取信号量(如果已经被占用,当前线程会进入睡眠)
shared_resource++; // 访问共享资源
up(&sem); // 🔓 释放信号量

互斥体(Mutex)

和信号量不同,互斥体中只有一个线程能获得锁(信号量允许多个线程同时访问),其他线程会阻塞(睡眠)。

  1. 结构体中声明互斥体
1
struct mutex lock;
  1. open函数中获取互斥体(interruptible表示该函数可被信号打断)
1
2
3
if (mutexc_lock_interruptible(&gpioled.lock)){
return -ERESTARTSYS;
}
  1. release时释放互斥锁
1
mutex_unlock(&dev->lock);
  1. 初始化设备时初始化互斥锁
1
mutex_init(&gpioled.lock);

对比

假设你去银行🏦取钱:

  • 🔢 原子变量(Atomic):银行门口有一个“当前排队人数”显示屏,每个人来都可以安全地加 1 或减 1,但不会控制谁去办业务(只适用于简单计数)。

  • 🔄 自旋锁(Spinlock):你去银行取钱,发现柜台有人,你站在那里等,直到轮到你(CPU 忙等)。

  • 🔢 信号量(Semaphore):银行有多个柜台,你可以去任何空闲的柜台办理业务(多个线程同时访问)。

  • 🛑 互斥体(Mutex):你去银行取钱,发现柜台有人,你去等候区坐着,等柜台空了再去(线程睡眠)。


Linux并发与竞争
http://akichen891.github.io/2025/03/24/Linux并发与竞争/
作者
Aki
发布于
2025年3月24日
更新于
2025年4月1日
许可协议