ARM Linux 驱动开发篇--- Linux 并发与竞争全解析(原子操作/自旋锁/信号量/互斥体)--- Ubuntu20.04
ARM Linux 驱动开发篇--- Linux 并发与竞争全解析(原子操作/自旋锁/信号量/互斥体)--- Ubuntu20.04

🎬 渡水无言:个人主页渡水无言
❄专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》
❄专栏传送门: 《freertos专栏》 《STM32 HAL库专栏》
⭐️流水不争先,争的是滔滔不绝📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生
| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生
在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连
目录
前言
Linux 是多任务操作系统,多线程、中断、多核 CPU 都会同时访问共享资源(如全局变量、设备结构体、硬件寄存器)。若不对共享资源做保护,会导致数据错乱、设备异常,甚至系统崩溃。
就像共享单车需要 “扫码解锁” 的规则一样,Linux 驱动开发中必须给共享资源制定 “访问规则”—— 这就是并发控制。本章将详细讲解驱动开发中最常用的 4 种并发处理机制:原子操作、自旋锁、信号量、互斥体。
一、并发与竞争核心概念
1.1、什么是并发与竞争?
先举个生活中大家都能遇到的例子:公司只有一台打印机(共享资源),小李和小王同时要打印文件(多个用户访问)。
如果打印机不做任何控制,就可能出现小李的内容打印一行、小王的内容插一行的情况,最终打印出来的文档全是乱的——这就是典型的竞争问题!
对应到Linux系统,咱们这样理解就很简单:
- 并发:多个任务(线程、中断、多核CPU)同时访问同一个共享资源。
- 竞争:并发访问引发的资源冲突,不处理的话会导致内存数据被覆盖、逻辑异常,严重的直接系统崩溃!
这里提醒大家:Linux是多任务操作系统,并发是常态,所以写驱动必须考虑竞争问题!
1.2 Linux并发产生的4大原因(记牢!面试常问)
1. 多线程并发:最基础的原因,Linux是多任务(线程)系统,多个线程同时运行,必然会竞争共享资源(比如全局变量)。
2. 抢占式并发:从Linux 2.6内核开始支持内核抢占,调度程序可以随时抢占正在运行的线程,切换到其他线程执行——哪怕你当前线程还没执行完!
3. 中断并发:学过STM32的朋友都知道,硬件中断的优先级极高,会直接打断正在运行的线程。如果中断服务函数也访问共享资源,就会和线程产生竞争。
4. SMP多核并发:现在ARM架构的多核SOC很常见(比如I.MX6ULL是单核,但很多高端芯片是多核),多个CPU核同时运行,会出现核间并发访问共享资源的情况。
1.3 临界区与保护核心(重点!)
学过FreeRTOS小伙伴,应该对「临界区」这个概念不陌生——简单说,就是多个任务会共同访问的共享数据段。
我们要做的,就是保证临界区的原子访问。
原子访问:就是不可拆分的操作,要么全部执行,要么全部不执行,中间不能被任何任务打断。
注意:前面一直说要防止并发访问共享资源,换句话说就是要保护共享资源,防止进行并发访问。
共享资源:是数据,所以我们要保护的不是代码,而是数据!
-
无需保护:线程的局部变量(只有当前线程能访问,不会有竞争)。
-
必须保护:全局变量、设备结构体、共享内存等,多个任务都会访问的数据。
接下来我们就依次来学习一下,Linux 内核提供的几种并发和竞争的处理方法。
二、原子操作
2.1 原子操作简介
原子操作是最基础的并发保护机制,核心就是「不可再拆分」,但它有个限制——只能用于整形变量或位操作。
举个直观的例子:C语言中一句简单的 a = 3,看起来是一步操作,但编译成ARM汇编后,会拆分成3步:
ldr r0, =0X30000000 /* 变量a的地址 */
ldr r1, =3 /* 要写入的值 */
str r1, [r0] /* 将3写入a中 */
相关汇编知识可以参考我这篇博客:FreeRTOS基础--堆栈概念与汇编指令实战解析
如果线程A要给a赋值10,线程B要给a赋值20,理想中的执行顺序如下图所示:

但实际的执行流程可能如下图所示:

CPU在执行线程A的这3步汇编时,被调度程序切换到线程B,就会出现A的操作没完成、B的操作插入的情况,最终a的值就会不符合预期,如上图所示,线程 A 最终将变量 a 设置为了 20,而并不是要求的 10!这就是一个最简单的设置变量值的并发与竞争的例子。
要解决这个问题就要保证之前提到的 三行汇编指令作为一个整体运行,也就是作为一个原子存在。
而Linux内核提供的原子操作API,就能保证这3步操作作为一个整体执行,中间不会被打断。
Linux 内核提供了两组原子操作 API 函数,一组是对整形变量进行操作的,一组是对位进行操作的,我们接下来看一下这些 API 函数。
2.2 原子整形操作API
Linux内核用 atomic_t 结构体表示原子整形变量(定义在 include/linux/types.h 文件中),结构很简单:
typedef struct {
int counter;
} atomic_t;
使用前必须先定义并初始化,首先要先定义一个 atomic_t 的变量,如下所示:
atomic_t a; //定义 a
atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0
原子变量有了,接下来就是对原子变量进行操作,比如读、写、增加、减少等等,下面给大家整理了常用的API(32位系统,比如I.MX6ULL的Cortex-A7适用),直接抄作业就行:
| 函数 | 描述 |
|---|---|
| ATOMIC_INIT(int i) | 定义原子变量的时候对其初始化。 |
| int atomic_read(atomic_t *v) | 读取 v 的值,并且返回。 |
| void atomic_set(atomic_t *v, int i) | 向 v 写入 i 值。 |
| void atomic_add(int i, atomic_t *v) | 给 v 加上 i 值。 |
| void atomic_sub(int i, atomic_t *v) | 从 v 减去 i 值。 |
| void atomic_inc(atomic_t *v) | 给 v 加 1,也就是自增。 |
| void atomic_dec(atomic_t *v) | 从 v 减 1,也就是自减。 |
| int atomic_dec_return(atomic_t *v) | 从 v 减 1,并且返回 v 的值。 |
| int atomic_inc_return(atomic_t *v) | 给 v 加 1,并且返回 v 的值。 |
| int atomic_sub_and_test(int i, atomic_t *v) | 从 v 减 i,如果结果为 0 就返回真,否则返回假。 |
| int atomic_dec_and_test(atomic_t *v) | 从 v 减 1,如果结果为 0 就返回真,否则返回假。 |
| int atomic_inc_and_test(atomic_t *v) | 给 v 加 1,如果结果为 0 就返回真,否则返回假。 |
| int atomic_add_negative(int i, atomic_t *v) | 给 v 加 i,如果结果为负就返回真,否则返回假。 |
👉 实操示例:
// 定义并初始化原子变量v=0
atomic_t v = ATOMIC_INIT(0);
atomic_set(&v, 10); // 设置v=10(原子操作)
atomic_read(&v); // 读取v的值,结果为10
atomic_inc(&v); // v自增1,变为11
atomic_dec(&v); // v自减1,变回10
2.3 原子位操作API
除了整形变量,原子操作还支持直接对内存地址的某一位进行操作,不需要专门的结构体,常用API整理如下,大家按需取用:
|
API函数 |
功能描述(通俗理解) |
|
set_bit(int nr, void *p) |
将p地址的第nr位置1(nr从0开始计数) |
|
clear_bit(int nr, void *p) |
将p地址的第nr位清零 |
|
change_bit(int nr, void *p) |
将p地址的第nr位翻转(0变1,1变0) |
|
test_bit(int nr, void *p) |
获取p地址的第nr位的值(返回0或1) |
|
test_and_set_bit(int nr, void *p) |
将第nr位置1,并返回原来的值(比如原来为0,返回0,再置1) |
|
test_and_clear_bit(int nr, void *p) |
将第nr位清零,并返回原来的值(比如原来为1,返回1,再清零) |
💡 小提醒:原子位操作直接操作内存,效率很高,适合用于标志位的原子设置(比如设备的忙闲标志)。
三、自旋锁
3.1 自旋锁简介
前面讲的原子操作,只能保护整形变量或位操作,但实际驱动开发中,我们常需要保护设备结构体、共享缓冲区等复杂资源——这时候就需要自旋锁登场了!
自旋锁的核心逻辑很简单,记住两点就行:
锁只能被一个线程持有,只要这个线程不释放锁,其他线程就无法获取。
未获取到锁的线程,不会进入休眠,而是忙循环等待(原地“自旋”),直到锁被释放。
还是用生活例子类比:公用电话亭(共享资源),里面有人正在打电话(持有锁),你到了门口,只能在原地等着(自旋),不能离开,也不能做其他事,直到里面的人打完电话出来(释放锁),你才能进去用。
自旋锁的特点:
- -优点:没有线程切换的开销,响应速度快(因为线程一直在自旋,锁释放后能立刻获取)。
- -缺点:自旋期间会占用CPU资源,所以锁的持有时间必须极短(比如几行代码的操作),否则会严重降低系统性能。
👉 适用场景:临界区代码很短、执行速度很快的场景(比如操作设备结构体的某个成员变量)。
3.2 自旋锁基础API
Linux内核用 spinlock_t 结构体表示自旋锁(定义在 <include/linux/spinlock.h>),简化后结构如下(省略条件编译):
typedef struct spinlock {
struct raw_spinlock rlock;
} spinlock_t;
使用步骤:先定义自旋锁变量,再初始化,最后使用加锁、解锁API。常用基础API整理如下:
|
API函数 |
功能描述 |
|
DEFINE_SPINLOCK(lock) |
定义并初始化一个自旋锁(静态初始化,推荐使用) |
|
spin_lock_init(spinlock_t *lock) |
动态初始化自旋锁(适合在函数中初始化) |
|
spin_lock(spinlock_t *lock) |
获取自旋锁(加锁),获取不到就自旋等待 |
|
spin_unlock(spinlock_t *lock) |
释放自旋锁(解锁),释放后其他线程可获取 |
|
spin_trylock(spinlock_t *lock) |
尝试获取自旋锁,获取到返回0,获取不到返回非0(不会自旋) |
|
spin_is_locked(spinlock_t *lock) |
检查自旋锁是否被持有,被持有返回非0,否则返回0 |
上述自旋锁API 函数适用于SMP或支持抢占的单CPU下线程之间的并发访问,也就是用于线程与线程之间,被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数,否则的话会可能会导致死锁现象的发生。
3.3 自旋锁+中断
注意:中断可以打断线程,如果中断服务函数也访问同一个共享资源,并且也用了自旋锁,直接会导致死锁!
举个例子:
-
线程A先获取到自旋锁,正在执行临界区代码。
-
此时中断发生,打断线程A,中断服务函数也去获取同一个自旋锁。
-
中断服务函数获取不到锁,就会自旋等待;而线程A被中断打断,无法释放锁——结果就是两者一直僵持,死锁!
✅ 正确做法:线程中使用自旋锁时,先关闭本地中断(本CPU的中断),再获取锁;释放锁后,再恢复中断。
Linux内核提供了专门的API,推荐使用spin_lock_irqsave 和 spin_unlock_irqrestore(自动保存中断状态,避免手动管理出错):
|
API函数 |
功能描述 |
|
spin_lock_irqsave(spinlock_t *lock, unsigned long flags) |
保存中断状态,关闭本地中断,再获取自旋锁 |
|
spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) |
恢复中断状态,打开本地中断,再释放自旋锁 |
使用模板:
// 1. 定义并初始化自旋锁
DEFINE_SPINLOCK(led_lock);
// 2. 线程上下文(比如驱动的write函数)
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
unsigned long flags; // 保存中断状态
struct gpioled_dev *dev = filp->private_data;
spin_lock_irqsave(&led_lock, flags); // 关中断+加锁
// 临界区:操作共享资源(比如设备结构体的成员)
gpio_set_value(dev->led_gpio, 0); // 举例:控制LED点亮
spin_unlock_irqrestore(&led_lock, flags); // 恢复中断+解锁
return 0;
}
// 3. 中断服务函数
void led_irq_handler(void)
{
spin_lock(&led_lock); // 中断中直接加锁(无需关中断,因为线程已关中断)
// 临界区:操作共享资源
spin_unlock(&led_lock); // 解锁
}
补充说明:如果是下半部(BH)使用自旋锁,推荐以下函数:
| 函数 | 描述 |
|---|---|
void spin_lock_bh(spinlock_t *lock) |
关闭下半部,并获取自旋锁。 |
void spin_unlock_bh(spinlock_t *lock) |
打开下半部,并释放自旋锁。 |
3.4 其他类型的自旋锁
在基础自旋锁的基础上,Linux还衍生出了两种特定场景的自旋锁,驱动中用得不多,大家了解一下就行:
3.4.1 读写自旋锁(rwlock_t)
适用场景:读多写少的场景(比如学生信息表,多人读取,偶尔修改)。
核心规则:
-
读操作可以并发(多个线程同时读)。
-
写操作互斥(只能一个线程写,写的时候不能读)。
API函数和基础自旋锁类似,只是分读锁和写锁(比如 read_lock、write_lock),用法大同小异。
| 函数 | 描述 |
|---|---|
void rwlock_init(rwlock_t *lock) |
定义并初始化读写锁 |
void rwlock_init(rwlock_t *lock) |
初始化读写锁 |
| 读锁 | |
void read_lock(rwlock_t *lock) |
获取读锁 |
void read_unlock(rwlock_t *lock) |
释放读锁 |
void read_lock_irq(rwlock_t *lock) |
禁止本地中断,并且获取读锁 |
void read_unlock_irq(rwlock_t *lock) |
打开本地中断,并且释放读锁 |
void read_lock_irqsave(rwlock_t *lock, unsigned long flags) |
保存中断状态,禁止本地中断,并且获取读锁 |
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags) |
将中断状态恢复到以前的状态,并且激活本地中断,释放读锁 |
void read_lock_bh(rwlock_t *lock) |
关闭下半部,并获取读锁 |
void read_unlock_bh(rwlock_t *lock) |
打开下半部,并释放读锁 |
| 写锁 | |
void write_lock(rwlock_t *lock) |
获取写锁 |
void write_unlock(rwlock_t *lock) |
释放写锁 |
void write_lock_irq(rwlock_t *lock) |
禁止本地中断,并且获取写锁 |
void write_unlock_irq(rwlock_t *lock) |
打开本地中断,并且释放写锁 |
void write_lock_irqsave(rwlock_t *lock, unsigned long flags) |
保存中断状态,禁止本地中断,并且获取写锁 |
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags) |
将中断状态恢复到以前的状态,并且激活本地中断,释放读锁 |
void write_lock_bh(rwlock_t *lock) |
关闭下半部,并获取读锁 |
void write_unlock_bh(rwlock_t *lock) |
打开下半部,并释放读锁 |
3.4.2 顺序锁(seqlock_t)
适用场景:需要同时读写的场景(比如日志打印,一边写日志,一边读日志)。
核心规则:
写操作可以和读操作同时进行(不会阻塞读)。
读操作如果发现读的过程中发生了写操作,需要重新读取(保证数据完整性)。
不能保护指针(写操作可能导致指针无效,读操作访问会崩溃)。
| 函数 | 描述 |
|---|---|
DEFINE_SEQLOCK(seqlock_t sl) |
定义并初始化顺序锁 |
void seqlock_init(seqlock_t *sl) |
初始化顺序锁 |
| 顺序锁写操作 | |
void write_seqlock(seqlock_t *sl) |
获取写顺序锁 |
void write_sequnlock(seqlock_t *sl) |
释放写顺序锁 |
void write_seqlock_irq(seqlock_t *sl) |
禁止本地中断,并且获取写顺序锁 |
void write_sequnlock_irq(seqlock_t *sl) |
打开本地中断,并且释放写顺序锁 |
void write_seqlock_irqsave(seqlock_t *sl, unsigned long flags) |
保存中断状态,禁止本地中断,并获取写顺序锁 |
void write_sequnlock_irqrestore(seqlock_t *sl, unsigned long flags) |
将中断状态恢复到以前的状态,并且激活本地中断,释放写顺序锁 |
void write_seqlock_bh(seqlock_t *sl) |
关闭下半部,并获取写读锁 |
void write_sequnlock_bh(seqlock_t *sl) |
打开下半部,并释放写读锁 |
| 顺序锁读操作 | |
unsigned read_seqbegin(const seqlock_t *sl) |
读单元访问共享资源的时候调用此函数,此函数会返回顺序锁的顺序号 |
unsigned read_seqretry(const seqlock_t *sl, unsigned start) |
读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读 |
3.5 自旋锁使用注意事项
1. 锁的持有时间必须极短:自旋期间占用CPU,若临界区代码太长(比如有延时、拷贝数据),会严重降低系统性能。
2. 临界区不能调用休眠函数:比如 msleep、copy_from_user(可能阻塞),否则会导致死锁(线程休眠后无法释放锁,其他线程一直自旋)。
3. 不能递归加锁:如果线程已经持有锁,再递归申请同一个锁,会自己自旋等待自己释放锁——直接死锁!
4. 保证可移植性:不管是单核还是多核SOC,都按多核来写驱动(统一用自旋锁保护),避免移植时出问题。
四、信号量(Semaphore)
4.1 信号量简介
学过FreeRTOS的小伙伴,对信号量肯定不陌生吧!它本质是一种同步机制,Linux内核也提供了信号量功能,核心作用就是控制共享资源的访问,尤其适合临界区较长的场景。
给大家举个最直观的生活例子,一看就懂:
某个停车场有100个停车位(这就是共享资源),大家都可以来停车。你开车到停车场,肯定要先看一下当前停了多少车、还有没有空位——这个“当前停车数量”,就相当于信号量;具体的停车数量,就是信号量的值。
当信号量值达到100时,说明停车场满了,你只能等着;等有车开出去,停车数量减1(信号量减1),就空出一个车位,你就能开进去,停车数量加1(信号量加1)。这个场景用的,就是「计数型信号量」。
如下图所示:

再对比一下前面讲的自旋锁,大家就能快速分清两者的区别:
假设A、B、C合租一套房,只有一个厕所(共享资源),一次只能一个人用。某天早上A先去上厕所了,过一会儿B也想用:
-
如果B一直站在厕所门口等,不做其他事,直到A出来——这就是自旋锁(忙等)。
-
如果B告诉A“你出来叫我一声”,然后自己回房间睡觉,等A出来再通知他——这就是信号量(休眠等待)。
从这个例子就能看出信号量的核心优势:不会浪费CPU资源。因为等待的线程会进入休眠,CPU可以去处理其他任务,等资源释放了再唤醒它。
但凡事有得有失,信号量的开销比自旋锁大——因为线程休眠、唤醒会涉及线程切换,这是有性能损耗的。
👉 总结信号量的3个核心特点(记牢!):
-
支持线程休眠,适合临界区较长、占用资源较久的场景(比如拷贝数据、I/O操作)。
-
不能用于中断上下文!因为中断不能休眠,而信号量会导致线程休眠,用在中断里直接报错。
-
不适合短临界区场景:频繁的线程休眠、切换,开销比自旋锁的“忙等”更大,反而降低系统性能。
再补充一个关键知识点:信号量分两种,按需选择即可:
计数型信号量:初始化时信号量值>1,允许多个线程同时访问共享资源(比如停车场100个车位,允许100辆车同时停)。
二值信号量:初始化时信号量值=1,只允许一个线程访问共享资源,本质就是简易的互斥(比如厕所,一次只能一个人用)。
简单理解:信号量就像一把把钥匙,信号量值就是钥匙的数量,要访问资源必须先拿一把钥匙(获取信号量),用完再还回去(释放信号量)。钥匙被拿完了,其他人就只能等着。
4.2 信号量API函数
Linux内核用 struct semaphore 结构体表示信号量(定义在 <include/linux/semaphore.h>),结构很清晰,简化后如下:
struct semaphore {
raw_spinlock_t lock; // 自旋锁,保护信号量本身
unsigned int count; // 信号量值(钥匙数量)
struct list_head wait_list; // 等待队列,保存等待信号量的线程
};
下面整理了常用API:
|
API函数 |
功能描述(通俗理解) |
|
DEFINE_SEMAPHORE(name) |
静态定义并初始化信号量,默认信号量值=1(二值信号量,常用) |
|
sema_init(struct semaphore *sem, int val) |
动态初始化信号量,val是信号量初始值(可设1或大于1) |
|
down(struct semaphore *sem) |
获取信号量(P操作),获取不到就休眠,不能被信号打断,不能用在中断 |
|
down_trylock(struct semaphore *sem) |
尝试获取信号量,获取到返回0,获取不到返回非0,不会休眠 |
|
down_interruptible(struct semaphore *sem) |
获取信号量,获取不到休眠,可以被信号打断(推荐用,更灵活) |
|
up(struct semaphore *sem) |
释放信号量(V操作),唤醒等待队列中第一个线程 |
4.3、使用模板:
// 方式1:静态定义并初始化(二值信号量,常用)
DEFINE_SEMAPHORE(led_sem);
// 方式2:动态定义并初始化(适合在函数中初始化,可设为计数型)
struct semaphore sem;
sema_init(&sem, 1); // 初始化信号量值为1(二值),设为5就是计数型
// 线程上下文(比如驱动的read函数)
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
struct gpioled_dev *dev = filp->private_data;
// 获取信号量(推荐用down_interruptible,可被信号打断)
if (down_interruptible(&led_sem)) {
return -ERESTARTSYS; // 被信号打断,返回错误码
}
// 临界区:操作共享资源(比如读取设备状态,可调用休眠函数)
copy_to_user(buf, &dev->led_status, sizeof(dev->led_status));
up(&led_sem); // 释放信号量
return sizeof(dev->led_status);
}
小提醒:信号量和自旋锁的核心区别,就是“是否允许休眠”——能休眠就用信号量(长临界区),不能休眠就用自旋锁(短临界区)。
五、互斥体(Mutex)
5.1 互斥体简介
前面讲信号量的时候提到,把信号量值设为1,就能实现互斥访问(一次只能一个线程访问资源)。但Linux内核专门提供了一个更专业的互斥机制——互斥体。
简单说:互斥体就是“专门用来做互斥”的工具,比二值信号量更轻量、更规范,是驱动开发中实现互斥访问的首选。
学过FreeRTOS的小伙伴肯定清楚,互斥体的核心就是“一次只能一个持有者”,和二值信号量类似,但有几个关键区别:
互斥体不能递归上锁/解锁:线程已经持有互斥体,再去申请同一个,直接死锁(自己等自己释放)。
互斥体的持有者必须释放:谁上锁,谁解锁,不能由其他线程释放(信号量可以)。
互斥体只能用于线程上下文:和信号量一样,会导致线程休眠,不能用于中断。
Linux内核用 struct mutex 结构体表示互斥体(定义在 <include/linux/mutex.h>),省略条件编译后结构如下:
struct mutex {
/* 1: 未上锁, 0: 已上锁, 负数: 已上锁且有等待线程 */
atomic_t count;
spinlock_t wait_lock; // 自旋锁,保护等待队列
};
👉 互斥体使用注意事项(必看!避免踩坑):
不能用于中断上下文:会导致休眠,中断无法休眠,只能用自旋锁。
临界区可以调用休眠函数:和信号量一样,因为线程可以休眠,所以允许调用copy_from_user、msleep等函数。
不能递归操作:同一线程不能多次上锁,否则死锁。
严格互斥:一次只能一个线程持有,适合需要“独占资源”的场景(比如设备的打开/关闭操作)。
5.2 互斥体API函数
互斥体的使用步骤和信号量、自旋锁一致:定义 → 初始化 → 上锁 → 解锁。常用API整理如下,用法和信号量很像,容易记:
|
API函数 |
功能描述(通俗理解) |
|
DEFINE_MUTEX(name) |
静态定义并初始化互斥体(推荐使用,最简单) |
|
mutex_init(struct mutex *lock) |
动态初始化互斥体(适合在函数中初始化) |
|
mutex_lock(struct mutex *lock) |
获取互斥体(上锁),获取不到就休眠,不能被信号打断 |
|
mutex_unlock(struct mutex *lock) |
释放互斥体(解锁),唤醒等待队列中第一个线程 |
|
mutex_trylock(struct mutex *lock) |
尝试获取互斥体,成功返回1,失败返回0,不会休眠 |
|
mutex_is_locked(struct mutex *lock) |
判断互斥体是否被持有,被持有返回1,否则返回0 |
|
mutex_lock_interruptible(struct mutex *lock) |
获取互斥体,获取不到休眠,可以被信号打断(推荐用) |
5.3标准使用模板:
// 方式1:静态定义并初始化互斥体(推荐)
DEFINE_MUTEX(led_mutex);
// 方式2:动态定义并初始化
struct mutex lock;
mutex_init(&lock);
// 线程上下文(比如驱动的open函数,需要互斥访问)
static int led_open(struct inode *inode, struct file *filp)
{
// 获取互斥体(推荐用可被打断的版本)
if (mutex_lock_interruptible(&led_mutex)) {
return -ERESTARTSYS;
}
// 临界区:操作共享资源(比如初始化设备,可调用休眠函数)
filp->private_data = dev; // 给文件私有数据赋值
// 这里可以加其他操作,比如判断设备是否已被占用
if (dev->dev_busy) {
mutex_unlock(&led_mutex); // 先解锁,再返回错误
return -EBUSY;
}
dev->dev_busy = 1;
mutex_unlock(&led_mutex); // 释放互斥体
return 0;
}
// 驱动的release函数,释放设备
static int led_release(struct inode *inode, struct file *filp)
{
struct gpioled_dev *dev = filp->private_data;
mutex_lock_interruptible(&led_mutex);
dev->dev_busy = 0; // 标记设备为空闲
mutex_unlock(&led_mutex);
return 0;
}
总结
到这里,Linux驱动开发中最常用的4种并发保护机制(原子操作、自旋锁、信号量、互斥体)就全部讲完了!
|
机制 |
适用场景 |
能否用在中断 |
等待方式 |
临界区长度 |
|
原子操作 |
整形变量、位操作 |
可以 |
无等待(直接执行) |
极短(单条操作) |
|
自旋锁 |
复杂资源、短操作 |
可以(关中断) |
忙等(自旋) |
很短(几行代码) |
|
信号量 |
多线程共享、长操作 |
不可以 |
休眠等待 |
较长(可休眠) |
|
互斥体 |
线程互斥访问、通用场景 |
不可以 |
休眠等待 |
中长(可休眠) |
更多推荐



所有评论(0)