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

ARM Linux 驱动开发篇--- Linux 并发与竞争全解析(原子操作/自旋锁/信号量/互斥体)--- Ubuntu20.04
🎬 渡水无言个人主页渡水无言

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

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

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

| 省级优秀毕业生获得者 | ZEEKLOG新星杯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; // 自旋锁,保护等待队列 };
👉 互斥体使用注意事项(必看!避免踩坑):不能用于中断上下文:会导致休眠,中断无法休眠,只能用自旋锁。临界区可以调用休眠函数:和信号量一样,因为线程可以休眠,所以允许调用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种并发保护机制(原子操作、自旋锁、信号量、互斥体)就全部讲完了!

机制

适用场景

能否用在中断

等待方式

临界区长度

原子操作

整形变量、位操作

可以

无等待(直接执行)

极短(单条操作)

自旋锁

复杂资源、短操作

可以(关中断)

忙等(自旋)

很短(几行代码)

信号量

多线程共享、长操作

不可以

休眠等待

较长(可休眠)

互斥体

线程互斥访问、通用场景

不可以

休眠等待

中长(可休眠)

Read more

人工智能:循环神经网络(RNN)与序列数据处理实战

人工智能:循环神经网络(RNN)与序列数据处理实战

循环神经网络(RNN)与序列数据处理实战 1.1 本章学习目标与重点 💡 学习目标:掌握循环神经网络的核心原理、经典变体结构,以及在文本序列任务中的实战开发流程。 💡 学习重点:理解 RNN 的循环计算机制,学会使用 TensorFlow/Keras 搭建基础 RNN 与 LSTM 模型,完成文本分类任务。 1.2 循环神经网络核心原理 1.2.1 为什么需要 RNN 💡 传统的前馈神经网络(如 CNN、全连接网络)的输入和输出是相互独立的。它们无法处理序列数据的上下文关联特性。 序列数据在现实中十分常见,比如自然语言文本、语音信号、时间序列数据等。这些数据的核心特点是,当前时刻的信息和之前时刻的信息紧密相关。 循环神经网络通过引入隐藏状态,可以存储历史信息,从而有效捕捉序列数据的上下文依赖关系。 1.2.2 RNN

By Ne0inhk
当人人都会用AI,你靠什么脱颖而出?

当人人都会用AI,你靠什么脱颖而出?

文章目录 * 一、引言:AI时代,你真的准备好了吗? * 二、脉向AI:连接AI与普通人的桥梁 * 2.1 什么是脉向AI? * 2.2 脉向AI的合作生态 * 2.3 为什么你需要关注脉向AI? * 三、本期重磅:《小Ni会客厅×AI熊厂长》深度对话 * 3.1 访谈背景 * 3.2 核心观点一:商业认知决定变现能力 * 3.3 核心观点二:个人标签决定商业价值 * 3.4 核心观点三:爆款策略决定起步速度 * 3.5 核心观点四:产品思维决定变现上限 * 四、从认知到行动:如何真正用AI赚到钱? * 4.1 建立正确的商业认知 * 4.2 找到你的70分领域

By Ne0inhk
Flutter 三方库 objectbox_generator — 自动化构建鸿蒙极速 NoSQL 数据库映射(适配鸿蒙 HarmonyOS Next ohos)

Flutter 三方库 objectbox_generator — 自动化构建鸿蒙极速 NoSQL 数据库映射(适配鸿蒙 HarmonyOS Next ohos)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net。 Flutter 三方库 objectbox_generator — 自动化构建鸿蒙极速 NoSQL 数据库映射(适配鸿蒙 HarmonyOS Next ohos) 在高性能移动应用开发中,本地数据的持久化存储效率往往是决定用户感知流畅度的木桶短板。传统的 SQLite 虽然结构化程度高,但在处理大规模对象关系映射(ORM)时,复杂的 SQL 拼接和反射解析往往会成为性能瓶颈。 ObjectBox 作为一个专为移动设备打造的、跨平台的超高速 NoSQL 数据库,已经成为了许多追求极致体验开发者的首选。而在 Flutter for OpenHarmony 开发中,配合 objectbox_generator,我们可以通过注解驱动的自动化流程,掌握这套高性能数据库的核心用法。 ⚠️ 鸿蒙适配现状提示:截至本文撰写时,ObjectBox 的 Dart 插件尚未提供官方的 OpenHarmony

By Ne0inhk
【Linux】Shell 脚本中的条件判断语句

【Linux】Shell 脚本中的条件判断语句

👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕Linux这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获! 文章目录 * Shell 脚本中的条件判断语句 🐚 * 一、什么是 Shell 条件判断?🎯 * 二、基础 if 语句结构 🧱 * 2.1 单分支 if * 2.2 双分支 if-else * 2.3 多分支 if-elif-else * 三、Java 对比示例 ☕ * 3.1 单分支 Java 示例 * 3.2 双分支 Java 示例 * 3.3 多分支

By Ne0inhk