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
博文中的一张图片