Linux内核设计与实现(10)第十章:内核同步方法

1. 原子操作

原子(atomic)本意是“不能被进一步分割的最小粒子”,
原子操作:意为“不可被中断的一个或一系列操作”。
总结就是: 不可中断的操作。

原子操作是由编译器来保证的,保证一个线程对数据的操作不会被其他线程打断。

详细请参考之前的博文:
多线程(12)atomic 原子操作系列接口
https://blog.csdn.net/lqy971966/article/details/104751966

2. 自旋锁

2.1 自旋锁

自旋锁:
执行时间短/临界区小,选择自旋锁
特点:临界区要小;不允许睡眠;可以再中断上下文中执行
优缺点:不睡眠,线程会忙等待,直到它拿到锁

2.2 自旋锁API

方法				描述
spin_lock()			获取指定的自旋锁
spin_lock_irq()		禁止本地中断并获取指定的锁
spin_lock_irqsave()	保存本地中断的当前状态,禁止本地中断,并获取指定的锁
spin_unlock()		释放指定的锁
spin_unlock_irq()	释放指定的锁,并激活本地中断
spin_unlock_irqstore()	释放指定的锁,并让本地中断恢复到以前状态
spin_lock_init()	动态初始化指定的spinlock_t
spin_trylock()		试图获取指定的锁,如果未获取,则返回0
spin_is_locked()	如果指定的锁当前正在被获取,则返回非0,否则返回0

2.3 自旋锁伪代码实现

spinlock_t lock;
spin_lock_init(lock);	//初始化
……
spin_lock(&lock);	//加锁
/*  临界区 */
spin_unlock(&lock);	//释放锁

详细请参考
Linux 锁机制(3)之自旋锁
https://blog.csdn.net/lqy971966/article/details/103541614

3. 信号量

信号量
信号量不一定是锁定某一个资源,而是流程上的概念。如果是流程上,则选择信号量。
互斥量则纯粹是“锁住某一资源”的概念;独占情况下使用互斥量
特点:会有上下文切换/睡眠

在面对互斥体和信号量的选择时,只要满足互斥体的使用场景就尽量优先使用互斥体。

Linux 锁机制(4)之信号量
https://blog.csdn.net/lqy971966/article/details/119326689

4. 互斥锁

首选互斥锁,互斥锁能够满足各类功能性要求。
优点: 1. 简单效率高;
	2. 线程会睡眠,所以等待的过程不会占用CPU时间。
		所以互斥锁或者信号量都是适用于等待时间较长的临界区。
缺点:开销大/会膨胀

4.1 互斥体和信号量场景选择:

只要满足互斥体的使用场景就尽量优先使用互斥体。

4.2 互斥体和自旋锁场景选择:

需求		建议的加锁方法
低开销加锁	优先使用自旋锁
短期锁定	优先使用自旋锁
长期加锁	优先使用互斥体
中断上下文中加锁	使用自旋锁
持有锁需要睡眠	使用互斥体

Linux 锁机制(1)之互斥量 mutex
https://blog.csdn.net/lqy971966/article/details/119108670

5. 完成变量(没用过)

完成变量的机制类似于信号量。
比如一个线程A进入临界区之后,另一个线程B会在完成变量上等待,线程A完成了任务出了临界区之后,使用完成变量来唤醒线程B。

完成变量的头文件:<linux/completion.h>

完成变量的API也很简单:

方法	描述
init_completion(struct completion *)	初始化指定的动态创建的完成变量
wait_for_completion(struct completion *)	等待指定的完成变量接受信号
complete(struct completion *)	发信号唤醒任何等待任务

我未使用过!!!

6. 大内核锁(已经弃用)

BKL是一个全局自旋锁,使用它主要是为了方便实现从Linux最初的SMP过渡到细粒度枷锁机制。
新代码中不再使用。

7. 顺序锁(没用过)

顺序锁为读写共享数据提供了一种简单的实现机制。
顺序锁则与之不同,读锁被获取的情况下,写锁仍然可以被获取。

处理流程:
使用顺序锁的读操作在读之前和读之后都会检查顺序锁的序列值,如果前后值不符,则说明在读的过程中有写的操作发生,那么读操作会重新执行一次,直至读前后的序列值是一样的。

特点:
顺序锁优先保证写锁的可用,所以适用于那些读者很多,写者很少,且写优于读的场景。

系统读写 xtime 时用的就是顺序锁。(下一章中讲述)

读写锁:
如果能区分出读写操作,读写锁就是第一选
特点:写独占,读共享;写锁优先级高

Linux 锁机制(2)之读写锁 rwlock_t
https://blog.csdn.net/lqy971966/article/details/103541567

8. 禁止抢占

其实使用自旋锁已经可以防止内核抢占了,但是有时候仅仅需要禁止内核抢占,不需要像自旋锁那样连中断都屏蔽掉。
这时候就需要使用禁止内核抢占的方法了:

preempt_disable() 增加抢占计数值,从而禁止内核抢占
preempt_enable() 减少抢占计算,并当该值降为0时检查和执行被挂起的需调度的任务
preempt_enable_no_resched()	激活内核抢占但不再检查任何被挂起的需调度的任务
preempt_count()	返回抢占计数

9. 顺序和内存屏障

9.1 内存屏障背景

内存屏障背景:编译器的优化和cpu乱序
对于一段代码,编译器或者处理器在编译和执行时可能会对执行顺序进行一些优化,从而使得代码的执行顺序和我们写的代码有些区别。

内存乱序访问主要发生在两个阶段:

编译时,编译器优化导致内存乱序访问(指令重排)
运行时,多 CPU 间交互引起内存乱序访问

9.2 内存屏障例子说明

如:

func()
{
	a = 5;
	b = 4;
	……
}
由于编译器或者处理器的优化,线程A中的赋值顺序可能是b先赋值后,a才被赋值。

为了保证代码的执行顺序,引入了一系列屏障方法来阻止编译器和处理器的优化。

解决:

a = 5;
mb(); 
/* 
 * mb()阻止跨越屏障的载入和存储动作重新排序
	保证在对b进行载入和存储值(值就是4)的操作之前
 * mb()代码之前的所有载入和存储值的操作全部完成(即 a = 5;已经完成)
 * 只要保证a的赋值在b的赋值之前进行,那么线程B的执行结果就和预期一样了
 */
b = 4;

为了保证代码的执行顺序,引入了一系列屏障方法来阻止编译器和处理器的优化。
加这个表

wmb() 阻止跨越屏障的存储动作发生重排序
smp_wmb() 在SMP上提供wmb()功能,在UP上提供barrier()功能
这两个啥区别 ? up 啥意思 ?

9.3 详细参考:Linux RCU机制+内存屏障

https://blog.csdn.net/lqy971966/article/details/118993557

9.4 内存屏障API

方法	描述
rmb()	读内存屏障,阻止跨越屏障的载入动作发生重排序
wmb()	写内存屏障,阻止跨越屏障的存储动作发生重排序
mb()	读写内存屏障,阻止跨越屏障的载入和存储动作重新排序
smp_rmb()	在SMP上提供rmb()功能,在UP上提供barrier()功能
smp_wmb()	在SMP上提供wmb()功能,在UP上提供barrier()功能
smp_mb()	在SMP上提供mb()功能,在UP上提供barrier()功能
barrier()	阻止编译器跨越屏障对载入或存储操作进行优化
read_barrier_depends()	阻止跨越屏障的具有数据依赖关系的载入动作重排序
smp_read_barrier_depends()	在SMP上提供read_barrier_depends()功能,在UP上提供barrier()功能

10. 总结

这里借用
https://www.cnblogs.com/wang_yb/archive/2013/05/01/3052865.html
博文中的一张图片
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/lqy971966/article/details/119599426