Linux - 并发控制

引用


一. 竞态条件

  • 竞态条件(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; 
operationeffect
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_rcuwriter端的操作可以是同步的,也就是说,完成更新操作之后,可以调用该接口函数等待所有在旧版本数据上的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 lockrwlock_init
获取指定的rw spin lockread_lock
write_lock
获取指定的rw spin lock同时disable本CPU中断read_lock_irq
write_lock_irq
保存本CPU当前的irq状态,disable本CPU中断并获取指定的rw spin lockread_lock_irqsave
write_lock_irqsave
获取指定的rw spin lock同时disable本CPU的bottom halfread_lock_bh
write_lock_bh
释放指定的spin lockread_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 halfread_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);把写者降级为读者。

3.7 per-CPU 量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值