1、对称多处理器(SMP)的多个CPU:
多个CPU共同使用系统总线,可访问共同点的外设和存储器。
2、单CPU内核进程与抢占它的进程:
一个进程的执行可被另一高优先级进程打断。
3、中断(硬中断、软中断、Tasklet,底半部)与进程之间:
中断可以打断正在执行的进程,若访问该进程正在访问的空间,将引发竞态。
解决竞态问题的方法:
1、中断屏蔽
2、原子操作
3、自旋锁
4、顺序锁
5、信号量
6、互斥体
一、中断屏蔽
使用方法:
local_irq_disable() //屏蔽中断
...
critical section //临界区
...
local_irq_enable() //开中断
local_irq_disable/enable只能禁止/使能本CPU内的中断,不能解决SMP多CPU引发的竞态,故不推荐使用,其适宜于自旋锁联合使用。
二、原子操作
使用方法:
//设置原子变量的值
static __inline__ void atomic_set(atomic_t *v, int i); //输入原子指针,将原子变量置为i
atomic_t v = ATOMIC_INIT(i); //直接将v的原子变量初始化为i
//原子变量的基本操作
atomic_read(atmic_t *v); //读值
void atomic_add/sub(int i, atomic_t *v); //加/减i操作
void atomic_inc/dec(atomic_t *v); //自加/自减操作
int atomic_inc/dec_test(atomic_t *v); //自加/自减后测试,为0返回ture,否则返回false
int atomic_sub_and_test(int i, atomic_t *v); //减i后测试,为0返回ture,否则返回false
int atomic_add/sub_return(int i, atomic_t *v); //加/减i后return
int atomic_inc/dec_return(atomic_t *v); //自加/自减后return
示例程序:
static atomic_t xxx_atomic = ATOMIC_INIT(1); //初始化
static int xxx_open(struct inode *inode, struct file *filp)
{
...
if(!atomic_dec_and_test(&xxx_atomic)){
//首次调用xxx_atomic时,其为1,则test后返回ture
atomic_inc(&xxx_availavle);
//再次调用是执行if(!false){}的内容
return - EBUSY;
}
...
return 0;
}
static int xxx_release(struct inode *inode, struct file *filp)
{
atomic_inc(&xxx_atomic);
//清除调用,使其变回初值
return 0;
}
//这里只是举例,并非一定要先dec_test然后inc, 只要前后的操作,不互相冲突, 实现再次调用时返回忙,而释放时使原子变量回到调用前的值即可
三、自旋锁
自旋锁是一种典型的对临界资源进行互斥访问的手段。
为了获得一个自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试并设置(test-and-set)某个内存变量,由于它是原子操作,所以在该操作完成之前,其他单元无法访问这个内存变量。
自旋锁有四种操作:
//定义自旋锁
spinlock_t lock;
//初始化自旋锁
spin_lock_init(lock);
//获得自旋锁
spin_lock(lock); //若获得则返回,否则自旋
tryspin_lock(lock); //若获得返回真,否则返回假
//释放自旋锁
spin_unlock(lock);
//自旋锁主要针对SMP或单CPU但内核可以抢占的情况,其他系统或不可抢占的CPU中,自旋锁为空操作
驱动程序中应该谨慎使用自旋锁,原因如下:
1、自旋锁是忙等待锁,当等待时间较长的时候将降低系统系能;
2、自旋锁可能导致系统死锁(锁陷阱);
3、自旋锁锁定器件不能调用可能引起进程调度的函数。如果进程获得自旋锁之后再阻塞,如调用copy_from_user()、copy_to_user()、kmalloc()和msleep()等函数,则可能导致内核崩溃。
四、顺序锁
顺序锁是对读写锁的一种优化,若使用顺序锁,读与写操作不阻塞,只阻塞同种操作,即读与读/写与写操作。
写执行单元的操作顺序如下:
//获得顺序锁
void write_seqlock(seqlock_t *s1);
int write_tryseqlock(seqlock_t *s1);
write_seqlock_irqsave(lock, flags)
write_seqlock_irq(lock)
write_seqlock_bh(lock)
//释放顺序锁
void write_sequnlock(seqlock_t *s1);
write_sequnlock_irqrestore(lock, flags)
write_sequnlock_irq(lock)
write_sequnlock_bh(lock)
读执行单元的操作顺序:
//读开始
unsinged read_seqbegin(const seqlock_t *s1);
read_seqbegin_irqsave(lock, flags)
//重读,读执行单元在访问完被顺序锁s1保护的共享资源后需要调用该函数来检查在读操作器件是否有写操作,如果有,读执行单元需要从新读一次。
int reead_seqretry(const seqlock_t *s1, unsigned iv);
read_seqretry_irqrestore(lock, iv, flags)
五、信号量
信号量是用于保护临界区的一种常用方法,它的使用方式和自旋锁类似。
相同点:只有得到信号量的进程才能执行临界区的代码。
(linux自旋锁和信号量锁采用的都是“获得锁-访问临界区-释放锁”,可以称为“互斥三部曲”,实际存在于几乎所有多任务操作系统中)
不同点:当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。
//信号量的结构
struct semaphore sem;
//初始化信号量
void sema_init(struct semaphore *sem, int val)
//常用下面两种形式
#define init_MUTEX(sem) sema_init(sem, 1)
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0)
//以下是初始化信号量的快捷方式,最常用的
DECLARE_MUTEX(name) //初始化name的信号量为1
DECLARE_MUTEX_LOCKED(name) //初始化信号量为0
//常用操作
DECLARE_MUTEX(mount_sem);
down(&mount_sem); //获取信号量
...
critical section //临界区
...
up(&mount_sem); //释放信号量
六、完成量
completion是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成。
DECLARE_COMPLETION(my_completion);/* 创建completion(声明+初始化) */
/
struct completion my_completion;/* 动态声明completion 结构体*/
static inline void init_completion(&my_completion);/*动态初始化completion*/
///
void wait_for_completion(struct completion*c);/* 等待completion */
void complete(struct completion*c);/*唤醒一个等待completion的线程*/
void complete_all(struct completion*c);/*唤醒所有等待completion的线程*/
/*如果未使用completion_all,completion可重复使用;否则必须使用以下函数重新初始化completion*/
INIT_COMPLETION(struct completion c);/*快速重新初始化completion*/
信号量用于同步时只能唤醒一个执行单元,而完成量(completion)用于同步时可以唤醒所有等待的执行单元。
completion的典型应用是模块退出时的内核线程终止。
七、互斥体
mutex的使用方法和信号量用于互斥体的场合完全一样。
//定义和初始化互斥体
struct mutex my_mutex;
mutex_init(&my_mutex);
//获取互斥体
void fastcall mutex_lock(struct mutex *lock);
void fastcall mutex_lock_interruptible(struct mutex *lock);
void fastcall mutex_trylock(struct mutex *lock);
//释放互斥体
void fastcall mutex_unlock(structmutex *lock);
自旋锁与互斥锁的选择
1、当锁不能被获取到时,使用信号量的开销是进程上下文切换时间Tsw,使用自旋锁的开始是等待获取自旋锁的时间Tcs,若Tcs比较小,则应使用自旋锁,否则应使用信号量
2、信号量锁保护的临界区可以包含引起阻塞的代码,而自旋锁则却对要避免使用包含阻塞的临界区代码,否则很可能引发锁陷阱
3、信号量存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在信号量和自旋锁之间只能选择自旋锁。当然,如果一定要使用信号量,则只能通过down_trylock()方式进行,不能获取就立即返回以避免阻塞。
读写信号量:与读写信号锁相似,是一种放宽粒度的实现机制。