linux源码解析16- Linux内核常用锁机制总结
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux源码解析16- Linux内核常用锁机制总结相关的知识,希望对你有一定的参考价值。
首先看看Linux内核中的并发场景;
单CPU多进程系统,产生并发访问因素有:
- 中断处理程序可以打断软中断,tasklet和进程上下文;
- 软中断和tasklet之间不会并发,但可以打断进程上下文;
- 在支持抢占的内核中,进程上下文之间会并发;
- 在不支持抢占的内核中,进程上下文不会并发;
在多CPU和多进程系统中,产生并发访问因素:
- 同一类型的中断处理程序不会并发,但不同类型的中断有可能分发到不同CPU上响应,可能产生并发;
- 同一类型的软中断会在不同CPU上并发;
- 同一类型的tasklet是串行执行的,不会在多个CPU并发;
- 不同CPU上的进程上下文会并发;
使用锁的原则:
保护资源或者数据,而不是保护代码;
Linux内核中的锁机制
1.自旋锁
特点:临界区不允许调度和睡眠,禁止抢占;解锁恢复抢占,可以用于进程上下文,中断上下文;
自旋锁主要用途是多处理器之间的并发控制,适用于竞争不太激烈的场景。如果竞争激烈,大量时间浪费在自旋锁,导致整体性能下降。
内核提供API:
spin_lock(lock);//加锁,成功后返回,否则自旋等待
//当锁进程和中断,有并发访问时,关闭中断
spin_lock_irqsave(lock, flags); //加锁,并关闭中断
spin_lock_bh(lock); //加锁,并关闭软中断
spin_unlock(lock);//解锁
spin_unlock_irqrestore(lock, flags); //解锁,打开硬中断
spin_unlock_bh(lock); //解锁,打开软中断
ps:为什么有的代码用spin_lock(),有的代码使用raw_spin_lock()?
实时补丁RT-patch, spinlock变成可抢占和睡眠的锁; 在绝对不允许被抢占和睡眠的临界区,使用raw_spin_lock,否则使用spinlock;
驱动工程师应该谨慎使用自旋锁:
- 自旋锁实际上是忙等待,当临界区很大时,或有共享设备的时候,需要较长时间占用锁,会降低系统的性能;
- 自旋锁可能导致系统死锁,一般是由递归使用一个自旋锁引起;
- 持有自旋锁期间,不能调用可能引起进程调度的函数; copy+from/to_user(),kmalloc()msleep等;
- 在单核下编程,也要考虑多核场景。 比如进程持有spin_lock_irqsave(),单核情况下中断不调用spin_lock也没问题,因为spin_lock_irqsave()保证了这个CPU中断服务程序不会执行。 但是在多核环境,spin_lock_irqsave()不能屏蔽另外一个核的中断,另外一个核就可能造成并发问题;
2.读写自旋锁:rwlock_t
允许多个读者同时持有锁; 只允许一个写者同时持有锁;
读写锁适合读者多,写者少的应用场景;
内核支持API:
DEFINE_RWLOCK(lock);
rwlock_init(lock);
read_lock(lock);
write_lock(lock);
read_unlock(lock);
write_unlock(lock);
3.信号量:
信号量是多值的,当其用作二值信号时,类似于锁,一个值代表未锁,另一个代表已锁;
原理: 获取锁的过程中,若不能立即得到,会发生调度,进入睡眠;
特点: 锁的竞争不是忙等,信号量临界区允许调度和睡眠而不会导致死锁; 锁的竞争者会转入睡眠,让出CPU,因此锁的竞争不会影响系统整体性能;
内核执行路径释放锁时,唤醒等待该锁的执行路径;
自旋锁vs信号量:
-
自旋锁问题: 持有自旋锁的临界区不允许调度和睡眠,竞争激烈时整体性能不好;
-
信号量缺点: 中断上下文要求整体运行时间可预测(不能太长),而信号量临界区允许睡眠,可能发生调度,因此不能用于中断上下文;
如果抢锁的过程很短,那么信号量不划算,因为进程睡眠加上唤醒代价太大,消耗CPU资源可能远大于短时间忙等待;
内核的semaphore实现:
struct semaphore
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
wait_list字段当信号量为忙时,所有等待信号量的进程列表,而lock则是保护wait_list的自旋锁;
信号量的API:
DEFINE_SEMPHORE(sem); //静态定义sem信号量
void sema_init(struct semaphore *sem, int val); ///初始化一个信号量sem,计数初值为val
void down(struct semaphore *sem); //减少信号量sem计算器,如果失败(count已经为0),转入睡眠,不能被信号唤醒(TASK_UNINTERRUPTIBLE),并把当前进程挂到wait_list;被唤醒后继续尝试获取锁
void up(struct semaphore *sem); //增加信号量sem计数器(类似释放锁),然后唤醒wait_list里的第一个进程(如果有的话);
int down_interruptible(struct semaphore *sem);//可以被信号唤醒,驱动推荐使用
int down_trylock(struct semaphore *sem);//立即返回
void down_timeout(struct semaphore *sem,long timeout);//timeout超时返回
4.读写信号量
类似读写自旋锁,为了区分不同的竞争者,比如允许读者共享,而写者互斥
struct rw_semaphore
long count;
struct list_head wait_list;
raw_spinlock_t wait_lock;
DECLARE_RWSEM(sem);//静态声明sem变量
init_rwsem(sem);//初始化一个sem
down_read(struct rw_semaphore *sem); //读者减少信号量sem计算器,类似获取锁
down_write(struct rw_semaphore *sem); //写者减少sem计数器
up_read(struct rw_semaphore *sem); //增加sem计数器
up_write(struct rw_semaphore *sem); //增加sem计数器
5.互斥体:
本质上是二值的信号量;
struct mutex
atomic_t count;
spinlock_t wait_lock;
struct list_head wait_list;
DEFINE_MUTEX(mutex);//静态定义mutex互斥量
mutex_init(mutex);//初始化mutex,初始未锁;
void mutex_lock(struct mutex *lock); //加锁,若失败,转入D状态(TASK_UNINTERRUPTIBLE),并把当前进程挂到wait_list
void mutex_unlock(struct mutex *lock); //解锁,并唤醒wait_list里第一个进程
mutex特点:
- 同一个时刻,只有一个线程可以持有mutex;
- 只有锁持有者可以解锁,因此mutex不适合多进程负载同步场景;
- 不允许递归加锁或解锁;
- 当进程持有mutex时,进程不可以退出;
- mutex必须使用官方API初始化;
- mutex可以睡眠,所以不能用在中断处理程序,或中断下半部,比如tasklet,定时器等;
自旋锁和互斥体: 自旋锁属于更底层实现,互斥体基于自旋锁是实现;
- 互斥体的开销是进程上下文的切换,自旋锁的开销是等待获取锁。若临界区较小,用自旋锁,若临界区较大,应该用互斥体;
- 互斥体保护的临界区可能包含睡眠的代码,而自旋锁不允许阻塞,睡眠,因为睡眠意味着要进行进程切换,如果切换出去,另一个进程试图获取本自旋锁,就死锁了。
- 互斥体存在于进程上下文,所以在中断,软中断环境,只能用自旋锁。如果一定要使用互斥体,应该使用mutex_trylock(),不能获取锁立即返回,避免阻塞;
6.RCU机制:
提高读锁的性能; 原理,没搞懂
以上是关于linux源码解析16- Linux内核常用锁机制总结的主要内容,如果未能解决你的问题,请参考以下文章
Linux 内核 内存管理RCU 机制 ⑤ ( RCU 层次架构概念 | RCU 层次架构源码解析 | RCU 层次架构每层最多叶子数 | RCU 层次架构每个叶子 CPU 数量 )