【嵌入式Linux】并发与竞争 -----自旋锁、信号量、互斥体

以下笔记参考自左中凯大佬的开发笔记,学习过程中精简出以下内容
适合Linux内核和驱动开发入门,用以记录

1. 并发与竞争根本原因

根本原因:Linux 是个多任务操作系统,存在 多个任务同事访问同一片内存区域

  • 多线程并发访问
  • 抢占式并发访问
  • 中断程序并发访问
  • SMP 多核间并发访问

并发访问带来的问题就是竞争,形如 FreeRTOS 中的 临界区的概念,临界区就是共享数据段,临界区必须每次只能被一个线程访问。Linux并发与竞争保护的也是 多个线程都被访问到的共享数据

2. 自旋锁、信号量、互斥体通俗解释

1️⃣ :自旋锁

自旋就是“原地打转“,一直等待, 等待期间线程不能休眠

  • 公用电话厅中,线程A在打电话
  • 线程B也想打电话,所以线程B在原地 一直等待,不能离开
  • 线程A打完电话,离开电话亭,线程A才能进入使用

2️⃣ :信号量

相比于自旋锁,信号量可以让线程先休眠,不用一直傻傻的”干等“,但是开销大, 允许多个线程访问共享资源

  • 公用电话厅中,线程A在打电话
  • 线程B也想打电话,所以线程B还可以, 先休眠,等听到电话亭们一声巨响, 知道A结束了,线程B再醒来进入使用

3️⃣ :互斥体

相比于信号量,采用互斥访问,表示一次 只有一个线程可以访问共享资源

3. 解决方式

3.1 原子操作

atomic_t

【32位整型变量】 +1 -1

【位操作】 某个bit位 清零 置位

3.2 自旋锁

若想要保护的临界区不是一个整型变量, 而是一个稍微复杂的 结构体或者 一段代码区域,就采用自旋锁的形式

  • 假设线程AB 都想使用临界区资源
  • 当A线程持有临界区访问权限时,线程B就会处于一直等待的状态;

3.2.1【通俗的解释】

  • 自旋就是“原地打转“,一直等待, 等待期间线程不能休眠
  • 缺点:自旋的线程若等待时间太长,则会浪费处理器时间,造成系统性能降低
  • 适用:自旋锁适用于 短时期轻量级加锁
  • 不适用:遇到有长时间持有锁的线程的场景,自旋锁不适用了

Linux 内核使用结构体 spinlock_t 表示自旋锁

3.2.2 自旋锁类型

3.2.2.1读写自旋锁

比如一张表,可以允许多个线程一起读,那么用读写自旋锁原理

  • 这张表在被修改的时候,一次只能允许一个线程修改
  • 这张表在被修改的时候,不允许线程读取
  • 读写不能同时进行,但是没有写操作时,多个线程可以并发读取

结构体为 rwlock_t

3.2.2.2顺序锁
  • 顺序锁 允许同时读写 不允许多个线程并发的写操作
  • 顺序锁 保护的资源不能是指针,因为写操作时可能导致指针无效,若此时有读操作则可能读取到 野指针使得系统崩溃

结构体为 rwlock_t

3.2.3【注意事项】–不要出现死锁

  • 1️⃣ :自旋锁应用于线程与线程之间
  • 2️⃣ :wo旋锁保护的临界区不可以调用任何能引起休眠和阻塞的API,否则可能出现死锁,死锁原因
  	- 自旋锁禁止内核抢占
 	- A线程在持有锁期间,若进入了休眠
  	- B线程会一直等待锁的释放,但是A休眠了,无法释放
  	- 死锁发生了
  • 3️⃣ :中断中可以使用自旋锁,但是中断在获取锁之前一定要先禁止本地中断,否则可能出现死锁,死锁原因
	- 线程A持有锁,在运行,此时,来了中断
  	- 中断也要使用锁,但是锁被A持有
 	 - 中断的优先级高,一直等待锁被释放,但是中断执行完之前,线程A无法执行,因此锁一直没法释放,中断一直等待
  	- 死锁发生了

image-20240428142551278

  • 4️⃣ :若果临界区比较大(运行比较耗时),不建议使用自旋锁,可以使用 信号量 互斥体
  • 5️⃣ :不要死贵申请自旋锁,否则就会自己锁死自己

3.3 信号量

相比于自旋锁, 信号量可以使线程进入休眠状态

信号量相比于自旋锁的特点:

1️⃣ :信号量可以使得等待资源的线程进入休眠状态,适用于占用资源比较久的场合

2️⃣ :信号量不能用于中断,因为中断不能休眠

3️⃣ :共享资源持有时间比较短不建议用信号量,因为 频繁休眠、切换线程对于操作系统来说 开销较大

结构体为 semaphore

3.4互斥体

互斥访问表示 一次只有一个线程可以访问共享资源,不能递归申请互斥体。

1️⃣ mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。

2️⃣ 和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数。

3️⃣ 因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。

结构体为 mutex

                                                      分割线,以下内容可不看
                                                      分割线,以下内容可不看
                                                      分割线,以下内容可不看

————————————————————————————————————————————————————————
————————————————————————————————————————————————————————
————————————————————————————————————————————————————————
————————————————————————————————————————————————————————

3.5 分割线,以下内容可不看

3.5 ————————————————

3.5分割线,以下内容可不看

自旋锁

​ 当一个线程要访问某个共享资源的时候首先要先获取相应的锁, 锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。 对于**自旋锁**而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁可用。

​ 通俗的讲,自旋锁的“自旋”也就是“原地打转”的意思,“原地打转”的目的是为了等待自旋锁可以用,可以访问共享资源。 比如现在有个公用电话亭,一次肯定只能进去一个人打电话,现在电话亭里面有人正在打电话,相当于获得了自旋锁。此时你到了电话亭门口,因为里面有人,所以你不能进去打电话,相当于没有获取自旋锁,这个时候你肯定是站在原地等待,你可能因为无聊的等待而转圈圈消遣时光,反正就是哪里也不能去,要一直等到里面的人打完电话出来。终于,里面的人打完电话出来了,相当于释放了自旋锁,这个时候你就可以使用电话亭打电话了,相当于获取到了自旋锁 。

​ 自旋锁只适用于短时期的轻量级加锁,原因就是等待自旋锁的线程会一直处于自旋状态,会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长 。

死锁现象自旋锁会自动禁止抢占,也就说当线程 A得到锁以后会暂时禁止内核抢占 。线程A在持有锁期间若调用了引起休眠或者阻塞的函数,那么线程A自动放弃CPU使用权,但是线程A休眠没有执行完,就不会解锁,线程B 一直等待A解锁,并且这个时候还禁止内核抢占。一直无法释放锁,那么死锁就发生了。

自锁使用注意:

①、因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,比如稍后要讲的信号量和互斥体。
②、自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死锁。
③、不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须“自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了!
④、在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管你用的是单核的还是多核的 SOC,都将其当做多核 SOC 来编写驱动程序

信号量

信号量常常用于控制对共享资源的访问

​ 通俗的解释:比如 A 与 B、 C 合租了一套房子,这个房子只有一个厕所,一次只能一个人使用。某一天早上 A 去上厕所了,过了一会 B 也想用厕所,因为 A 在厕所里面,所以 B 只能等到 A 用来了才能进去。B 要么就一直在厕所门口等着,等 A 出来,这个时候就相当于**自旋锁。 B 也可以告诉 A,让 A 出来以后通知他一下,然后 B 继续回房间睡觉,这个时候相当于信号量。可以看出,使用信号量会提高处理器的使用效率,毕竟不用一直傻乎乎的在那里“自旋”等待。但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。总结一下信号量的特点:
①、因为信号量可以使等待资源线程进入休眠状态,因此
适用于占用资源比较久的场合。
②、因此信号量
不能用于中断**中,因为信号量会引起休眠,中断不能休眠。
③、如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势 。

​ 信号量有一个信号量值,相当于一个房子有 10 把钥匙,这 10 把钥匙就相当于信号量值为10。因此,可以通过信号量来控制访问共享资源的访问数量,如果要想进房间,那就要先获取一把钥匙,信号量值减 1,直到 10 把钥匙都被拿走,信号量值为 0,这个时候就不允许任何人进入房间了,因为没钥匙了。如果有人从房间出来,那他要归还他所持有的那把钥匙,信号量值加 1,此时有 1 把钥匙了,那么可以允许进去一个人。相当于通过信号量控制访问资源的线程数,在初始化的时候将信号量值设置的大于 1,那么这个信号量就是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。

互斥体—mutex

互斥访问表示一次只有一个线程可以访问共享资源 。

①mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
②和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数。
③因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

积跬步、至千里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值