Linux并发控制(看内核源码....)
1.linux并发与竞态
在linux设备驱动中--存在多个进程对资源共享并发访问,并发会导致竞态。
并发--多个执行单元同时、并行被执行,而执行单元对共享资源的访问会导致竞态。
竞态出现的情况:1.SMP对称多处理(Symmetrical Multi-Processing)的多个CPU---cpu与cpu之间,进程与进程之间,中断与中断之间。
2.单cpu内进程与抢占它的进程
3.中断与进程之间
解决竞态的途径是保证对共享资源的互斥访问
方式:中断屏蔽、原子操作、自旋锁、信号量、互斥体等。
2.编译乱序和执行乱序
2.1 编译乱序
主要出现的原因:编译器可以对访存指令进行乱序,减少逻辑上不必要的访存,以提高Cache命中率和CPU的Load/Store单元的工作效率。
解决编译乱序可以通过barrier()--编译屏障,可以阻挡编译器的优化。
2.2 执行乱序
高级的CPU可以根据自己的缓存的组织特性,将访存指令重新排序,连续地址的访问可能会先执行,因为这样缓存命中率较高。
ARM处理器的屏障指令包括:
1.DMB(数据内存屏障):在DMB之后的显示内存访问执行前,保证所有在DMB指令之前的内存访问完成;
2.DSB(数据同步屏障):等待所有在DSB指令之前的指令完成;
3.ISB(指令同步屏障): Flush流水线,使得所有ISB之后执行的指令都是从缓存或内存中获得的;
3.中断屏蔽
对于单cpu, 在进入临界区之前屏蔽系统中的中断时避免竞态的一种简单而有效的方法。(驱动程序中不推荐使用)
cpu一般都具备中断关闭和打开的功能,这项功能保证了正在执行的内核执行路线不被中断处理程序所抢占,防止了某些竞态的发生。
中断屏蔽使得中断和进程之间的并发不再发生。由于linux内核调度(时间片)等操作都依赖中断来实现,内核抢占进程之间的并发也就得以避免。
常见操作:
local_irq_enable()
local_irq_disable()
//禁止中断,并保存/恢复状态
local_irq_save()
local_irq_restore()
//禁止中断底部
lock_bh_enable()
lock_bh_disable()
4.原子操作
保证对一个整形数据的修改是排他性的。
对于ARM处理器底层使用LDREX和STREX指令实现原子操作。
主要有整形原子操作和位原子操作。
常见操作:
atomic_set();
atomic_read()
atomic_add()
atomic_sub()
atomic_inc()
atomic_dec()
atomic_inc_and_test()
atomic_dec_and_test()
atomic_sub_and_test()
set_bit()
clear_bit()
change_bit()
test_bit()
test_and_set_bit()
test_and_clear_bit()
test_and_change_bit()
5.自旋锁---在多处理器系统能够提供对共享数据的保护
spin_lock-->raw_spin_lock-->_raw_spin_lock-->__raw_spin_lock-->do_raw_spin_lock-->arch_spin_trylock;
spin Lock是一种对临界资源进行互斥访问的手段.
关于spin_unlock_irq()--->主要应用在中断处理程序中,它做主要的工作是关闭中断、停止抢占、获取锁;
常见操作:
spinloc_t lock;//定义自旋锁
spin_lock_init(lock);//初始化自旋锁
spin_lock(lock)//获取自旋锁,如果锁已经被占用,就原地等待
spin_trylock(lock)//获取自旋锁,如果锁已经被占用,立即返回
spin_unlock(lock);//释放自旋锁
//下面三个主要使用的原因:如果一个进程持有自旋锁spinlock,而中断发生后进程休眠,进入中断处理程序中执行,刚好使用同一个spinlock
//这个时候中断处理程序就会阻塞,造成死锁。
//一般需要在进程上下文中调用spin_lock_irq()这类函数,来阻止中断和内核抢占。
//在中断上下文中使用spin_lock();
spin_lock_irq()
spin_unlock_irq()
spin_lock_irqsave() //关闭本地的中断,多核编程中不使用
spin_unlock_irqrestore()
spin_lock_bh()
spin_unlock_bh()
注意:
1.自旋锁应用在占有锁较短的时间内,否则会降低系统性能(忙则等待)
2.不能两次或两次以上获取同一个锁
3.在自旋锁锁定期间不能调用引起进程调度的函数
6.读写锁
允许任意数量的读取者同时进入临界区,但是写入者必须进行互斥访问。
如果当前进程正在写,那么其他进程就不能读,也不能写
如果当前进程正在读,那么其他进程可以读,但是不能写
常见操作:
//定义及初始化
rwlock_t my_rwlock;
rwlock_init()
//读操作
read_lock()
read_lock_irqsave()
read_lock_irq()
read_lock_bh()
//写操作
write_lock()
write_lock_irqsave()
write_lock_irq()
write_lock_bh()
7.顺序锁
顺序锁是对读写锁的一种优化,若使用顺序锁,读执行单元不会被写执行单元阻塞。
8.RCU(Read_Copy_Update)--读写更新锁(***)
RCU(Read-Copy Update),顾名思义就是读-拷贝修改,它是基于其原理命名的。
对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,
最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。
这个时机就是所有引用该数据的CPU都退出对共享数据的操作。
常用操作:
rcu_read_lock()
rcu_read_unlock()
synchronize_rcu()-->call_rcu() //又名call_rcu_sched()
rcu_assign_pointer()
rcu_dereference()
8.信号量(semaphore)
linux中信号量是一种睡眠锁。如果有一个任务试图获得不可用的信号量时,信号量会将其推进一个等待队列,让其睡眠。
当信号量可用时,处于等待队列中的任务被唤醒,获得信号量。
注意:
信号量适用于锁被长时间占用。
由于执行线程在锁被争用时会睡眠,信号量只能在进程上下文中使用,不能再中断上下文中使用的。
占有信号量的同事不能占用自旋锁,等待信号量的会导致进程睡眠,而持有自旋锁时不能睡眠。
常见操作:
struct semaphore name;
sema_init(&name, count);//初始化信号量
init_MUTEX(&name) //以计数值为1初始化动态创建的信号量(互斥信号量)
down_interrupt(&name)//争用一个信号量,如果不可用,将进程设置为可中断-task_interruptible状态-进入睡眠。(多用)
down(&name)//争用一个信号量,如果不可用,将进程设置为不可中断-task_uninterruptible状态-进入睡眠。(少用)
up(&name)//释放指定的信号量,如果睡眠队列不为空,则唤醒其中的一个任务。
9.互斥锁(mutex)
linux争对 count=1 的信号量重新定义了一个数据结构mutex
常用操作:
struct mutex my_mutex; //定义互斥锁
mutex_init() //初始化互斥锁
mutex_lock() //获取互斥锁,如果锁被占用,就将进程设置为不可中断睡眠
mutex_lock_interrputible() //获取互斥锁,如果锁被占用,将进程设置为可中断睡眠。
mutex_trylock() //获取互斥锁,如果锁被占用,就返回
mutex_unlock() //释放互斥锁
10.完成量 (completion)