在linux内核中,主要的竞态发生于如下几种情况:
1.对称多处理器(SMP)的核间并发
cpu0的进程/中断与cpu1的进程\中断之间的并行。
2.单cpu内进程与抢占它的进程
进程与抢占它的进程访问共享资源类似于SMP的多个cpu
3.中断与进程之间(核间或核内并发)
中断可以打断正在执行的进程,如果中断服务程序访问进程正在访问的资源,则竞态也会发生。linux2.6.35之后就取消了中断的嵌套,所以核内没有中断嵌套引起的并发,核间可能有中断与中断的并行。
解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源时,其他的执行单元被禁止访问。访问共享资源的代码区域被称为临界区。中断屏蔽、自旋锁、互斥体是常用的互斥手段。
一 中断屏蔽
中断屏蔽local_irq_disable()和中断使能local_irq_enablle()都只能禁止和使能本cpu内的中断。所以中断屏蔽可以使得核内的中断与进程并发不再发生,由于linux进程调度依赖中断来实现,进程与抢占它的进程之间的并发也避免了。但不能解决SMP多cpu引发的竞态。所以屏蔽中断来避免竞态不是一种正确的做法。
local_irq_disable()//屏蔽中断
...
critical section//临界区
...
local_irq_enable()//使能中断
二 自旋锁
spin_lock():任何一个核拿到自旋锁,该核上的进程抢占调度就被禁止了,但是没有禁止其他核的抢占调度,由于锁的存在,可以保证临界区不受别的cpu或本cpu内的抢占进程打扰。
但是得到锁的代码路径在执行临界区时,还可能收到中断和底半部的影响。
spin_lock_irq=spin_lock+local_irq_disable 锁+关闭本核中断
spin_unlock_irq=spin_unlock+local_irq_enable
spin_lock_irqsave=spin_lock+local_irq_save
spin_locj_irqrestore=spin_unlock+local_irq_restore
spin_lock_bh = spin_lock+local_bh_disable
spin_unlock_bh = spin_unlock+local_bh_enable
如果进程和中断可能访问同一片资源,一般需要在进程上下文中使用spin_lock_irqsave/spin_unlock_irqrestore,避免核内进程与中断并发。在中断上下文中使用spin_lock/spin_unlock,避免核间中断与进程/中断并发。
自旋锁使用注意:占用锁的时间应该很短,即临界区域应很简短;自旋锁锁定期间不能调用可能引起进程调度的函数如copy_from_user/copy_to_user/kmalloc/msleep,其实就是进程获得锁之后,自己不能阻塞,因为阻塞后进程无法再调度;
spin_lock_t lock;
spin_lock_init(lock);
spin_lock(lock)
...
临界区
...
spin_unlock
if (spin_trylock(lock)) {
...//获得锁
临界区
spin_unlock(lock);
}
else {
//没有获得锁,不需要解锁
}
//禁止本核进程调度,屏蔽本核中断,保存状态
spin_lock_irqsave(lock,flags)
...
临界区//不要有可能引起阻塞的代码copy_from_user,copy_to_user,kmalloc,msleep
...
spin_lock_irqrestore(lock,flags)
三 读写自旋锁
只有读与读可以并发,读写不能并发,写与写不能并发。与自旋锁比较,区分临界区的读写操作。
读操作
read_lock(lock)-->read_unlock
read_lock_irqsave-->read)unlock_irqrestore
写操作
write_lock--->write_unlock
write_lock_irqsave-->write_lock_irqrestore
四 顺序自旋锁
只有写与写是互斥的,读与写不互斥,读操作期间可能发生了写操作,故需判断是否发生了写操作,有则重读。
写执行单元
write_seqlock-->write_sequnlock
write_seqtrylock-->
write_seqlock_irqsave-->
write_seqlock_irq-->
write_seqlock_bh-->
读执行单元
unsigned read_seqbegin(seqlock)返回一个顺序号
读完需判断是否需要重读
read_seqretry(lock, seqnum)
读操作用法:
do {
seqnum = read_seqbegin(lock);
...
} while (read_seqretry(lock, seqnum));
五 互斥体
mutex_lock与spin_lock相比,mutex_lock没获得锁会睡眠,发生进程上下文切换。进程上下文切换开销大,所以互斥体一般用于进程占用资源时间较长时。互斥体是进程级的,一般不能用于中断(因为会睡眠),一定要用可以用mutex_trylock,获取不到避免阻塞,中断可以用自旋锁。
struct mutex my_mutex;
mutex_init(&my_mutex);
mutex_lock(&my_mutex);//获取不到,睡眠,不可被信号打断
...
mutex_unlock(&my_mutex);
mutex_lock_interruptible(&my_mutex)//获取不到,睡眠,可被信号打断
mutex_trylock(&ny_mutex)//尝试获取,获取不到不会睡眠。