🎬 渡水无言个人主页渡水无言

专栏传送门: linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》

专栏传送门freertos专栏》 《STM32 HAL库专栏
⭐️流水不争先,争的是滔滔不绝

 📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生

| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生

在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连

目录

前言

一、并发与竞争核心概念

1.1、什么是并发与竞争?

1.2 Linux并发产生的4大原因(记牢!面试常问)

1.3 临界区与保护核心(重点!)

二、原子操作

2.1 原子操作简介

2.2 原子整形操作API

2.3 原子位操作API

三、自旋锁

3.1 自旋锁简介

3.2 自旋锁基础API

3.3 自旋锁+中断

3.4 其他类型的自旋锁

3.4.1 读写自旋锁(rwlock_t)

3.4.2 顺序锁(seqlock_t)

3.5 自旋锁使用注意事项

四、信号量(Semaphore)

4.1 信号量简介

4.2 信号量API函数

4.3、使用模板:

五、互斥体(Mutex)

5.1 互斥体简介

5.2 互斥体API函数

 5.3标准使用模板:

总结


前言

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,理想中的执行顺序如下图所示:

按照上图 所示的流程,确实可以实现线程 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_irqsavespin_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)

适用场景:需要同时读写的场景(比如日志打印,一边写日志,一边读日志)。

核心规则:

写操作可以和读操作同时进行(不会阻塞读)。

读操作如果发现读的过程中发生了写操作,需要重新读取(保证数据完整性)。

不能保护指针(写操作可能导致指针无效,读操作访问会崩溃)。

关于顺序锁的 API 函数如下表:
函数 描述
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个核心特点(记牢!):

  1. 支持线程休眠,适合临界区较长、占用资源较久的场景(比如拷贝数据、I/O操作)。

  2. 不能用于中断上下文!因为中断不能休眠,而信号量会导致线程休眠,用在中断里直接报错。

  3. 不适合短临界区场景:频繁的线程休眠、切换,开销比自旋锁的“忙等”更大,反而降低系统性能。

再补充一个关键知识点:信号量分两种,按需选择即可:

  • 计数型信号量:初始化时信号量值>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; // 自旋锁,保护等待队列
};

👉 互斥体使用注意事项(必看!避免踩坑):

  1. 不能用于中断上下文:会导致休眠,中断无法休眠,只能用自旋锁。

  2. 临界区可以调用休眠函数:和信号量一样,因为线程可以休眠,所以允许调用copy_from_user、msleep等函数。

  3. 不能递归操作:同一线程不能多次上锁,否则死锁。

  4. 严格互斥:一次只能一个线程持有,适合需要“独占资源”的场景(比如设备的打开/关闭操作)。

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种并发保护机制(原子操作、自旋锁、信号量、互斥体)就全部讲完了!

机制

适用场景

能否用在中断

等待方式

临界区长度

原子操作

整形变量、位操作

可以

无等待(直接执行)

极短(单条操作)

自旋锁

复杂资源、短操作

可以(关中断)

忙等(自旋)

很短(几行代码)

信号量

多线程共享、长操作

不可以

休眠等待

较长(可休眠)

互斥体

线程互斥访问、通用场景

不可以

休眠等待

中长(可休眠)

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐