引用
- 喔窝科技-Linux内核同步机制之(-): 原子操作
- 宋宝华:关于ARM Linux原子操作的实现
- 罗玉平:关于ARM Linux原子操作的底层支持
- 自旋锁(spinlock) 解释得经典,透彻
- 蜗窝科技-Linux内核同步机制之(四):spin lock
- 蜗窝科技-Linux内核的自旋锁
- 宋宝华: 几个人一起抢spinlock,到底谁先抢到?
- 宋宝华: 是谁关闭了Linux抢占,而抢占又关闭了谁?
- Linux内核的信号量
- RCU机制
- 谢宝友: 深入理解Linux RCU之一——从硬件说起
- RCU synchronize原理分析
- Linux RCU原理剖析(一)-初窥门径
- Linux RCU原理剖析(二)-渐入佳境
- Linux内核同步机制之(七):RCU基础
- Linux内核同步机制之(三):memory barrier
- Why Memory Barriers?中文翻译(上)
- Linux内核同步机制之(二):Per-CPU变量
- linux spinlock/rwlock/seqlock原理剖析(基于ARM64)
一. 竞态条件
- 竞态条件(race condition): 几个user在访问资源时,彼此干扰的情况。
- 临界区(Critical Sections):
问题:进程的执行在不应该的地方被中断,从而导致进程工作得不正确。
二. 内核锁机制
- 原子操作 Atomic Operations:最简单的锁操作,它们保证简单的操作,例如计数器加1之类,可以不中断地原子执行。即使操作由几个汇编语句组成,也可以保证。
- 自旋锁 Spinlocks: 最常用的锁选项,用于短期保护某段代码,以防止其他处理器的访问。在内核等待自旋锁释放时,会重复检查是否能获取锁,而不会进入睡眠状态(忙等待)。当然,如果等待时间较长,则效率显然不高。
- 信号量 Semaphores: 用经典方法实现的。在等待信号量释放时,内核进入睡眠状态,直至被唤醒。唤醒后,内核才重新尝试获取信号量。互斥量是信号量的特例,互斥量保护的临界区,每次只能有一个用户进入。
- 读者/写者锁 Reader/Writer Locks: 会区分对数据结构的两种不同类型的访问。任意数目的处理器都可以对数据结构进行并发读访问,但只有一个处理器能进行写访问。事实上,在进行写访问时,读访问是无法进行的。
三. Linux 内核API
3.1 对整数的原子操作
<asm-arch/atomic.h> | |
typeof struct { volatile in counter;} atomic_t; | |
operation | effect |
atomic_read(atomic_t *v) | 读取原子变量v的值 |
atomic_set(atomic_t *v, int i) | 将v设置为i |
atomic_add(int i, atomic_t *v) | 给原子变量v增加i |
atomic_add_return(int i, atomic_t *v) | 同上,只不过将变量v的最新值返回。 |
atomic_sub(int i, atomic_t *v) | 给原子变量v减去i |
atomic_sub_return(int i, atomic_t *v) | 同上,只不过将变量v的最新值返回。 |
atomic_sub_and_test(int i, atomic_t *v) | Adds i to v. Returns true if the result is 0, otherwise false. |
atomic_inc(atomic_t *v) | 原子变量v加1 |
atomic_inc_return(atomic_t *v) | 同上,只不过将变量v的最新值返回。 |
atomic_inc_and_test(atomic_t *v) | Adds 1 to v. Returns true if the result is 0, otherwise false. |
atomic_dec(atomic_t *v) | Subtracts 1 from v. |
atomic_dec_return(atomic_t *v) | 同上,只不过将变量v的最新值返回。 |
atomic_dec_and_test(atomic_t *v) | Subtracts 1 from v. Returns true if the result is 0, otherwise false. |
atomic_add_negative(int i, atomic_t *v) | Adds i to v. Returns true if the result is less than 0, otherwise false. |
atomic_cmpxchg(atomic_t *ptr, int old, int new) | 比较old和原子变量ptr中的值,如果相等,那么就把new值赋给原子变量。 返回旧的原子变量ptr中的值 |
3.2 自旋锁
- spin_lock
DEFINE_SPINLOCK(test_spin_lock) | 定义并且初始化静态自旋锁test_spin_lock |
spin_lock_init(test_spin_lock) | 在运行时动态初始化自旋锁test_spin_lock |
void spin_lock(spinlock_t *lock); | 申请自旋锁,如果锁被其他处理器占有,当前处理器自旋等待。 |
void spin_lock_bh(spinlock_t *lock); | 申请自旋锁,并且,禁止当前处理器的软中断。 |
void spin_lock_irq(spinlock_t *lock); | 申请自旋锁,并且,禁止当前处理器的硬中断。 |
spin_lock_irqsave(lock, flags); | 申请自旋锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断。 |
int spin_trylock(spinlock_t *lock); | 申请自旋锁,如果申请成功,返回1;如果锁被其他处理器占有,当前处理器不等待,立即返回0。 |
void spin_unlock(spinlock_t *lock); | 释放自旋锁 |
void spin_unlock_bh(spinlock_t *lock); | 释放自旋锁,并且,开启当前处理器的软中断。 |
void spin_unlock_irq(spinlock_t *lock); | 释放自旋锁,并且,开启当前处理器的硬中断。 |
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags); | 释放自旋锁,并且恢复当前处理器的硬中断状态。 |
- raw_spin_lock
DEFINE_RAW_SPINLOCK(x); | 定义并且初始化静态原始自旋锁x |
raw_spin_lock_init (x); | 在运行时动态初始化原始自旋锁x |
raw_spin_lock(lock) | 申请原始自旋锁,如果锁被其他处理器占有,当前处理器自旋等待。 |
raw_spin_lock_bh(lock) | 申请原始自旋锁,并且禁止当前处理器的软中断。 |
raw_spin_lock_irq(lock) | 申请原始自旋锁,并且禁止当前处理器的硬中断。 |
raw_spin_lock_irqsave(lock, flags) | 申请原始自旋锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断。 |
raw_spin_trylock(lock) | 申请原始自旋锁,如果申请成功,返回1;如果锁被其他处理器占有,当前处理器不等待,立即返回0。 |
raw_spin_unlock(lock) | 释放原始自旋锁 |
raw_spin_unlock_bh(lock) | 释放原始自旋锁,并且开启当前处理器的软中断。 |
raw_spin_unlock_irq(lock) | 释放原始自旋锁,并且开启当前处理器的硬中断。 |
raw_spin_unlock_irqrestore(lock, flags) | 释放原始自旋锁,并且恢复当前处理器的硬中断状态。 |
- spinlock和raw_spinlock(原始自旋锁)有什么关系?
Linux内核有一个实时内核分支(开启配置宏CONFIG_PREEMPT_RT)来支持硬实时特性,内核主线只支持软实时。
对于没有打上实时内核补丁的内核,spinlock只是封装raw_spinlock,它们完全一样。如果打上实时内核补丁,那么spinlock使用实时互斥锁保护临界区,在临界区内可以被抢占和睡眠,但raw_spinlock还是自旋锁。
目前主线版本还没有合并实时内核补丁,说不定哪天就会合并进来,为了使代码可以兼容实时内核,最好坚持3个原则:
(1)尽可能使用spinlock。
(2)绝对不允许被抢占和睡眠的地方,使用raw_spinlock,否则使用spinlock。
(3)如果临界区足够小,使用raw_spinlock。
- spin_lock与抢占的关系?
spin_lock_irq() -> raw_spin_lock_irq() -> __raw_spin_lock_irq()
static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)
{
local_irq_disable();
preempt_disable();
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
spin_lock()会调用preempt_disable() 导致本核的抢占调度被关闭(preempt_disable函数实际增加preempt_count来达到此效果),其次我们理解spin_lock_irq()是local_irq_disable()+preempt_disable()的合体。
local_irq_disable()/local_irq_save()的disable和save版的唯一区别是,要不要保存CPU对中断的屏蔽状态。
spin_lock_irq()/spin_lock_irqsave(lock, flags)的唯一区别是,要不要保存CPU对中断的屏蔽状态。
/************************中断返回时 会被调用*****************************/
/*
* this is the entry point to schedule() from kernel preemption
* off of irq context.
* Note, that this is called and return with irqs disabled. This will
* protect us against recursive calling from irq.
*/
asmlinkage __visible void __sched preempt_schedule_irq(void)
{
enum ctx_state prev_state;
/* Catch callers which need to be fixed */
BUG_ON(preempt_count() || !irqs_disabled());
prev_state = exception_enter();
do {
preempt_disable();
local_irq_enable();
__schedule(true);
local_irq_disable();
sched_preempt_enable_no_resched();
} while (need_resched());
exception_exit(prev_state);
}
/**************************抢占开启preempt_enable()时,会被调用到*********************/
/*
* this is the entry point to schedule() from in-kernel preemption
* off of preempt_enable. Kernel preemptions off return from interrupt
* occur there and call schedule directly.
*/
asmlinkage __visible void __sched notrace preempt_schedule(void)
{
/*
* If there is a non-zero preempt_count or interrupts are disabled,
* we do not want to preempt the current task. Just return..
*/
if (likely(!preemptible()))
return;
preempt_schedule_common();
}
#ifdef CONFIG_PREEMPT_COUNT
#define preemptible() (preempt_count() == 0 && !irqs_disabled())
#else
#define preemptibel() 0
#endif
对于ARM处理器而言,判断irqs_disabled(),其实就是判断CPSR中的IRQMASK_I_BIT是否被设置。
所以,我们得出一个结论,前言这一节里面,列出的所有函数,都能关闭本核的抢占调度。因为,无论是preempt_count计数状态,还是中断被关闭,都会导致kernel认为无法抢占!
3.3 信号量,互斥量
DECLARE_MUTEX(name) | 该宏声明一个信号量name并初始化他的值为1,即声明一个互斥锁。 |
DECLARE_MUTEX_LOCKED(name) | 该宏声明一个互斥锁name,但把他的初始值设置为0,即锁在创建时就处在已锁状态。因此对于这种锁,一般是先释放后获得。 |
void sema_init (struct semaphore *sem, int val); | 用于数初始化设置信号量的初值,他设置信号量sem的值为val。 |
void init_MUTEX (struct semaphore *sem); | 用于初始化一个互斥锁,即他把信号量sem的值设置为1。 |
void init_MUTEX_LOCKED (struct semaphore *sem); | 用于初始化一个互斥锁,但他把信号量sem的值设置为0,即一开始就处在已锁状态。 |
void down(struct semaphore * sem); | 用于获得信号量sem,他会导致睡眠,因此不能在中断上下文(包括IRQ上下文和softirq上下文)使用该函数。该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。 |
int down_interruptible(struct semaphore * sem); | 和down类似,不同之处为,down不会被信号(signal)打断,但down_interruptible能被信号打断,因此该函数有返回值来区分是正常返回还是被信号-EINTR中断,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR。 |
int down_trylock(struct semaphore * sem); | 试着获得信号量sem,如果能够即时获得,他就获得该信号量并返回0,否则,表示不能获得信号量sem,返回值为非0值。因此,他不会导致调用者睡眠,能在中断上下文使用。 |
int down_killable(struct semaphore *sem); | 可被杀死地获取信号量。如果睡眠被致命信号中断,返回错误-EINTR。 |
int down_timeout(struct semaphore *sem, long jiffies); | 在指定的时间jiffies内获取信号量,若超时未获取,返回错误-ETIME。 |
int down_timeout_interruptible(struct semaphore *sem, long jiffies); | |
void up(struct semaphore * sem); | 放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。 |
DEFINE_MUTEX(mutexname) | 创建和初始化互斥锁 |
void mutex_lock(struct mutex *lock) | 加锁 |
void mutex_unlock(struct mutex *lock) | 解锁 |
void mutex_trylock(struct mutex *lock) | 尝试加锁 |
3.4 RCU
RCU涉及的数据有两种,一个是指向要保护数据的指针,我们称之RCU protected pointer。另外一个是通过指针访问的共享数据,我们称之RCU protected data,当然,这个数据必须是动态分配的 。对共享数据的访问有两种,一种是writer,即对数据要进行更新,另外一种是reader。如果在有reader在临界区内进行数据访问,对于传统的,基于锁的同步机制而言,reader会阻止writer进入(例如spin lock和rw spin lock。seqlock不会这样,因此本质上seqlock也是lock-free的),因为在有reader访问共享数据的情况下,write直接修改data会破坏掉共享数据。怎么办呢?当然是移除了reader对共享数据的访问之后,再让writer进入了(writer稍显悲剧)。对于RCU而言,其原理是类似的,为了能够让writer进入,必须首先移除reader对共享数据的访问,怎么移除呢?创建一个新的copy是一个不错的选择。因此RCU writer的动作分成了两步:
(1)removal。write分配一个new version的共享数据进行数据更新,更新完毕后将RCU protected pointer指向新版本的数据。一旦把RCU protected pointer指向的新的数据,也就意味着将其推向前台,公布与众(reader都是通过pointer访问数据的)。通过这样的操作,原来read 0、1、2对共享数据的reference被移除了(对于新版本的受RCU保护的数据而言),它们都是在旧版本的RCU protected data上进行数据访问。
(2)reclamation。共享数据不能有两个版本,因此一定要在适当的时机去回收旧版本的数据。当然,不能太着急,不能reader线程还访问着old version的数据的时候就强行回收,这样会让reader crash的。reclamation必须发生在所有的访问旧版本数据的那些reader离开临界区之后再回收,而这段等待的时间被称为grace period。
顺便说明一下,reclamation并不需要等待read3和4,因为write端的为RCU protected pointer赋值的语句是原子的,乱入的reader线程要么看到的是旧的数据,要么是新的数据。对于read3和4,它们访问的是新的共享数据,因此不会reference旧的数据,因此reclamation不需要等待read3和4离开临界区。
reader: | |
rcu_read_lock | 用来标识RCU read side临界区的开始。 |
rcu_dereference | 该接口用来获取RCU protected pointer。reader要访问RCU保护的共享数据,当然要获取RCU protected pointer,然后通过该指针进行dereference的操作。 |
rcu_read_unlock | 用来标识reader离开RCU read side临界区 |
writer: | |
rcu_assign_pointer | 该接口被writer用来进行removal的操作,在witer完成新版本数据分配和更新之后,调用这个接口可以让RCU protected pointer指向RCU protected data。 |
synchronize_rcu | writer端的操作可以是同步的,也就是说,完成更新操作之后,可以调用该接口函数等待所有在旧版本数据上的reader线程离开临界区,一旦从该函数返回,说明旧的共享数据没有任何引用了,可以直接进行reclaimation的操作。 |
call_rcu | 当然,某些情况下(例如在softirq context中),writer无法阻塞,这时候可以调用call_rcu接口函数,该函数仅仅是注册了callback就直接返回了,在适当的时机会调用callback函数,完成reclaimation的操作。这样的场景其实是分开removal和reclaimation的操作在两个不同的线程中:updater和reclaimer。 |
3.5 内存和优化屏障
barrier() | 优化屏障,阻止编译器为了进行性能优化而进行的memory access reorder |
mb() | 内存屏障(包括读和写),用于SMP和UP |
rmb() | 读内存屏障,用于SMP和UP |
wmb() | 写内存屏障,用于SMP和UP |
smp_mb() | 用于SMP场合的内存屏障,对于UP不存在memory order的问题(对汇编指令),因此,在UP上就是一个优化屏障,确保汇编和c代码的memory order是一致的 |
smp_rmb() | 用于SMP场合的读内存屏障 |
smp_wmb() | 用于SMP场合的写内存屏障 |
- barrier()这个接口和编译器有关,对于gcc而言,其代码如下:
#define barrier() __asm__ __volatile__("": : :"memory")
这里的__volatile__主要是用来防止编译器优化的。
3.6 读者/写者锁
接口API描述 | rw spinlock API |
定义rw spin lock并初始化 | DEFINE_RWLOCK |
动态初始化rw spin lock | rwlock_init |
获取指定的rw spin lock | read_lock |
write_lock | |
获取指定的rw spin lock同时disable本CPU中断 | read_lock_irq |
write_lock_irq | |
保存本CPU当前的irq状态,disable本CPU中断并获取指定的rw spin lock | read_lock_irqsave |
write_lock_irqsave | |
获取指定的rw spin lock同时disable本CPU的bottom half | read_lock_bh |
write_lock_bh | |
释放指定的spin lock | read_unlock |
write_unlock | |
释放指定的rw spin lock同时enable本CPU中断 | read_unlock_irq |
write_unlock_irq | |
释放指定的rw spin lock同时恢复本CPU的中断状态 | read_unlock_irqrestore |
write_unlock_irqrestore | |
获取指定的rw spin lock同时enable本CPU的bottom half | read_unlock_bh |
write_unlock_bh | |
尝试去获取rw spin lock,如果失败,不会spin,而是返回非零值 | read_trylock |
write_trylock |
DECLARE_RWSEM(name) | 声明名为name的读写信号量,并初始化它。 |
void init_rwsem(struct rw_semaphore *sem); | 对读写信号量sem进行初始化。 |
void down_read(struct rw_semaphore *sem); | 读者用来获取sem,若没获得时,则调用者睡眠等待。 |
void up_read(struct rw_semaphore *sem); | 读者释放sem。 |
int down_read_trylock(struct rw_semaphore *sem); | 读者尝试获取sem,如果获得返回1,如果没有获得返回0。可在中断上下文使用。 |
void down_write(struct rw_semaphore *sem); | 写者用来获取sem,若没获得时,则调用者睡眠等待。 |
int down_write_trylock(struct rw_semaphore *sem); | 写者尝试获取sem,如果获得返回1,如果没有获得返回0。可在中断上下文使用 |
void up_write(struct rw_semaphore *sem); | 写者释放sem。 |
void downgrade_write(struct rw_semaphore *sem); | 把写者降级为读者。 |