蜕变成蝶~Linux设备驱动中的并发控制

并发和竞争发生在两类体系中:html

  •     对称多处理器(SMP)的多个CPU
  •     内核可抢占的单CPU系统

  访问共享资源的代码区域称为临界区critical sections,临界区须要以某种互斥机制加以保护。在驱动程序中,当多个线程同时访问相同的资源critical sections时(驱动程序中的全局变量是一种典型的共享资源),可能会引起"竞态",所以咱们必须对共享资源进行并发控制。Linux内核中解决并发控制的方法又中断屏蔽、原子操做、自旋锁、信号量。(后面为主要方式)node

 

中断屏蔽:linux

  使用方法数据结构

local_irq_disable()  //屏蔽中断
...
critical section        //临界区
...
local_irq_enable()   //开中断

  local_irq_disable/enable只能禁止/使能本CPU内的中断,不能解决SMP多CPU引起的竞态,故不推荐使用,其适宜于自旋锁联合使用。并发

 

原子操做:  函数

  原子操做是一系列的不能被打断的操做。linux内核提供了一系列的函数来实现内核中的原子操做,这些函数分为2类,分别针对位和整型变量进行原子操做。性能

实现整型原子操做的步骤以下:测试

1.定义原子变量并设置变量值
优化

void atomic_set(atomic_t *v , int i); //设置原子变量值为i
atomic_t v = ATOMIC_INIT(0); //定义原子变量v,初始化为0

2.获取原子变量的值
ui

atomic_read(atomic_t *v);

3.原子变量加减操做

void atomic_add(int i,atomic_t *v);//原子变量加i
void atomic_sub(int i ,atomic_t *v);//原子变量减i

4.原子变量自增/自减

void atomic_inc(atomic_t *v);//自增1
void atomic_dec(atomic_t *v);//自减1

5.操做并测试:对原子变量执行自增、自减后(没有加)测试其是否为0,若是为0返回true,不然返回false。

int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i ,atomic_t *v);

6.操做并返回

int atomic_add_return(int i , atomic_t *v);
int atomic_sub_return(int i , atomic_t *v);
int atomic_inc_return(atomic_t * v);
int atomic_dec_return(atomic_t * v);

 

实现 位原子操做以下:

// 设置位
void set_bit(nr, void *addr);  // 设置addr地址的第nr位,即将位写1
 
// 清除位
void clear_bit(nr, void *addr);  // 清除addr地址的第nr位,即将位写0
 
// 改变位
void change_bit(nr, void *addr);  // 对addr地址的第nr位取反
 
// 测试位
test_bit(nr, void *addr); // 返回addr地址的第nr位
 
// 测试并操做:等同于执行test_bit(nr, void *addr)后再执行xxx_bit(nr, void *addr)
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr)

 

下面来举一个实例,是原子变量使用实例,使设备只能被一个进程打开:

static atomic_t xxx_available = ATOMIC_INIT(1);  // 定义原子变量
 
static int xxx_open(struct inode *inode, struct file *filp)
{
    ...
    if(!atomic_dec_and_test(&xxx_available))
    {
        atomic_inc(&xxx_availble);
        return - EBUSY;  // 已经打开
    }
    ...
    return 0;  // 成功
}
 
static int xxx_release(struct inode *inode, struct file *filp)
{
    atomic_inc(&xxx_available);  // 释放设备
    return 0;
}

  

我要着重谈一下:

自旋锁VS信号量

  从严格意义上来讲,信号量和自旋锁属于不一样层次的互斥手段,前者的实现依赖于后者,在多CPU中须要自旋锁来互斥。信号量是进程级的,用于多个进程之间对资源的互斥,虽然也在内核中,可是该内核执行路径是以进程的身份,表明进程来争夺资源的。若是竞争失败,会切换到下个进程,而当前进程进入睡眠状态,所以,当进程占用资源时间较长时,用信号量是较好的选择。

       当所要保护的临界访问时间比较短时,用自旋锁是很是方便的,由于它节省了上下文切换的时间。可是CPU得不到自旋锁是,CPU会原地打转,直到其余执行单元解锁为止,因此要求锁不能在临界区里停留时间过长。

  自旋锁的操做步骤:

1.定义自旋锁
spinlock_t lock;
2.初始化自旋锁
spin_lock_init(lock);这是个宏,它用于动态初始化自旋锁lock;
3.得到自旋锁
spin_lock(lock);该宏用于加锁,若是可以当即得到锁,它就能立刻返回,不然,他将自旋在那里,直到该自旋锁的保持者释放。
spin_trylock(lock);可以得到,则返回真,不然返回假,其实是不在原地打转而已。
4.释放自旋锁
spin_unlock(lock);

 

  自旋锁持有期间内核的抢占将被禁止。 自旋锁能够保证临界区不受别的CPU和本CPU内的抢占进程打扰,可是获得锁的代码路径在执行临界区的时候还可能受到中断和底半部(BH)的影响。为防止这种影响,须要用到自旋锁的衍生:

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_unlock_irqrestore() = spin_unlock() + local_irq_restore()

spin_lock_bh() = spin_lock() + local_bh_disable()

spin_unlock_bh() = spin_unlock() + local_bh_enable()

 注意:自旋锁其实是忙等待,只有在占用锁的时间极短的状况下,使用自旋锁才是合理的自旋锁可能致使死锁:递归使用一个自旋锁或进程得到自旋锁后阻塞。

例子:

spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock);  //获取自旋锁,保护临界区

。。。。临界区

spin_unlock(&lock);//释放自旋锁

  自旋锁不关心锁定的临界区到底是如何执行的。不论是读操做仍是写操做,实际上,对共享资源进行读取的时候是应该能够容许多个执行单元同时访问的,那么这样的话,自旋锁就有了弊端。因而便衍生出来一个读写锁。它保留了自旋的特性,但在对操做上面能够容许有多个单元进程同时操做。固然,读和写的时候不能同时进行。

  如今又有问题了,若是我第一个进程写共享资源,第二个进程读的话,一旦写了,那么就读不到了,可能写的东西比较多,可是第二个进程读很小,那么能不能第一个进程写的同时,我第二个进程读呢?
固然能够,那么引出了顺序锁的概念。都是同样的操做。

 

  读写自旋锁(rwlock)容许读的并发。在写操做方面,只能最多有一个写进程,在读操做方面,同时能够有多个读执行单元。固然,读和写也不能同时进行。

// 定义和初始化读写自旋锁
rwlock_t my_rwlock = RW_LOCK_UNLOCKED;  // 静态初始化
rwlock_t my_rwlock;
rwlock)init(&my_rwlock);  // 动态初始化
 
// 读锁定:在对共享资源进行读取以前,应先调用读锁定函数,完成以后调用读解锁函数
void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);
 
// 读解锁
void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);
 
// 写锁定:在对共享资源进行写以前,应先调用写锁定函数,完成以后调用写解锁函数
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock);
 
// 写解锁
void write_unlock(rwlock_t *lock);
void write_unlock_irqsave(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);

  读写自旋锁通常用法:

rwlock_t lock;  // 定义rwlock
rwlock_init(&lock);  // 初始化rwlock
 
// 读时获取锁
read_lock(&lock);
...  // 临界资源
read_unlock(&lock);
 
// 写时获取锁
write_lock_irqsave(&lock, flags);
...  // 临界资源
write_unlock_irqrestore(&lock, flags);

  

顺序锁(seqlock):

  顺序锁是对读写锁的一种优化,若使用顺序锁,读与写操做不阻塞,只阻塞同种操做,即读与读/写与写操做。

  写执行单元的操做顺序以下:

//得到顺序锁
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)

  

  RCU(Read-Copy Update 读-拷贝-更新)可看做读写锁的高性能版本,既容许多个读执行单元同时访问被保护的数据,又容许多个读执行单元和多个写执行单元同时访问被保护的数据。可是RCU不能替代读写锁。由于若是写操做比较多时,对读执行单元的性能提升不能弥补写执行单元致使的损失。由于使用RCU时,写执行单元之间的同步开销会比较大,它须要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其余写执行单元的修改操做。

  具体操做:略

 

信号量的使用

    信号量(semaphore)与自旋锁相同,只有获得信号量才能执行临界区代码,但,当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。

相同点:只有获得信号量的进程才能执行临界区的代码。(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)用于同步时能够唤醒全部等待的执行单元。

 

 自旋锁与互斥锁的选择

  • 当锁 不能被获取到时,使用信号量的开销是进程上下文切换时间Tsw,使用自旋锁的开始是等待获取自旋锁的时间Tcs,若Tcs比较小,则应使用自旋锁,不然应使用信号量
  • 信号量锁保护的临界区能够包含引发阻塞的代码,而自旋锁则却对要避免使用包含阻塞的临界区代码,不然极可能引起锁陷阱
  • 信号量存在于进程上下文,所以,若是被保护的共享资源须要在中断或软中断状况下使用,则在信号量和自旋锁之间只能选择自旋锁。固然,若是必定要使用信号量,则只能经过down_trylock()方式进行,不能获取就当即返回以免阻塞。

 

  版权全部,转载请注明转载地址:http://www.cnblogs.com/lihuidashen/p/4435979.html

相关文章
相关标签/搜索