Linux设备驱动开发详解总结(二)之并发与竞争

       Linux设备驱动中必须解决一个问题是多个进程对共享资源的并发访问,并发的访问会导致竞态,在当今的Linux内核中,支持SMP与内核抢占的环境下,更是充满了并发与竞态。幸运的是,Linux 提供了多钟解决竞态问题的方式,这些方式适合不同的应用场景。例如:中断屏蔽、原子操作、自旋锁、信号量等等并发控制机制。

 1.1 并发与竞态

      并发是指多个执行单元同时、并发被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态。

      临界区概念是为解决竞态条件问题而产生的,一个临界区是一个不允许多路访问的受保护的代码,这段代码可以操纵共享数据或共享服务。临界区操纵坚持互斥锁原则(当一个线程处于临界区中,其他所有线程都不能进入临界区)。然而,临界区中需要解决的一个问题是死锁。

1.2 中断屏蔽

     在单CPU 范围内避免竞态的一种简单而省事的方法是进入临界区之前屏蔽系统的中断。CPU  一般都具有屏蔽中断和打开中断的功能,这个功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,有效的防止了某些竞态条件的发送,总之,中断屏蔽将使得中断与进程之间的并发不再发生。

中断屏蔽的使用方法:
local_irq_disable()  /*屏蔽本地CPU 中断*/

.....

critical section  /*临界区受保护的数据*/

.....

local_irq_enable()  /*打开本地CPU  中断*/

 

      由于Linux 的异步I/O、进程调度等很多重要操作都依赖于中断,中断对内核的运行非常重要,在屏蔽中断期间的所有中断都无法得到处理,因此长时间屏蔽中断是非常危险的,有可能造成数据的丢失,甚至系统崩溃的后果。这就要求在屏蔽了中断后,当前的内核执行路径要尽快地执行完临界区代码。

      与local_irq_disable() 不同的是,local_irq_save(flags) 除了进行禁止中断的操作外,还保存当前CPU 的中断状态位信息;与local_irq_enable() 不同的是,local_irq_restore(flags) 除了打开中断的操作外,还恢复了CPU 被打断前的中断状态位信息。

 

1.3  原子操作

      原子操作指的是在执行过程中不会被别的代码路径所中断的操作,Linux 内核提供了两类原子操作——位原子操作和整型原子操作。它们的共同点是在任何情况下都是原子的,内核代码可以安全地调用它们而不被打断。然而,位和整型变量原子操作都依赖于底层CPU 的原子操作来实现,因此这些函数的实现都与 CPU 架构密切相关。

1.3.1 整型原子操作

1、设置原子变量的值

void  atomic_set(atomic *v,int i);  /*设置原子变量的值为 i  */

atomic_t  v = ATOMIC_INIT(0);  /*定义原子变量 v 并初始化为 0  */

2、获取原子变量的值

int  atomic_read(atomic_t  *v)  /*返回原子变量 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、操作并测试

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);

      上述操作对原子变量执行自增、自减和减操作后测试其是否为 0 ,若为 0 返回true,否则返回false。注意:没有atomic_add_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);

      上述操作对原子变量进行加/减和自增/自减操作,并返回新的值。

 

 

1.3.2 位原子操作

1、设置位

void  set_bit(nr,void  *addr);/*设置addr 指向的数据项的第 nr 位为1 */

2、清除位

void clear_bit(nr,void  *addr)/*设置addr 指向的数据项的第 nr 位为0 */

3、取反位

void change_bit(nr,void *addr); /*对addr 指向的数据项的第 nr 位取反操作*/

4、测试位

test_bit(nr,void *addr);/*返回addr 指向的数据项的第 nr位*/

5、测试并操作位

int  test_and_set_bit(nr, void  *addr);

int  test_and_clear_bit(nr,void  *addr);

int  test_amd_change_bit(nr,void  *addr);

 

1.4 自旋锁

      自旋锁(spin lock)是一种典型的对临界资源进行互斥访问的手段。为了获得一个自旋锁,在某CPU 上运行的代码需先执行一个原子操作,该操作测试并设置某个内存变量,由于它是原子操作,所以在该操作完成之前其他执行单元不能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,则程序将在一个小的循环里面重复这个“测试并设置” 操作,即进行所谓的“自旋”。

      理解自旋锁最简单的方法是把它当做一个变量看待,该变量把一个临界区标记为“我在这运行了,你们都稍等一会”,或者标记为“我当前不在运行,可以被使用”。

 

Linux中与自旋锁相关操作有:

1、定义自旋锁

spinlock_t  my_lock;

2、初始化自旋锁

spinlock_t  my_lock  = SPIN_LOCK_UNLOCKED;  /*静态初始化自旋锁*/

void  spin_lock_init(spinlock_t  *lock);   /*动态初始化自旋锁*/

3、获取自旋锁

/*若获得锁立刻返回真,否则自旋在那里直到该锁保持者释放*/

void spin_lock(spinlock_t  *lock);

/*若获得锁立刻返回真,否则立刻返回假,并不会自旋等待*/

void spin_trylock(spinlock_t  *lock)

4、释放自旋锁

void spin_unlock(spinlock_t  *lock)

 

自旋锁的一般用法:

spinlock_t   lock;   /*定义一个自旋锁*/

spin_lock_init(&lock);  /*动态初始化一个自旋锁*/

......

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

......./*临界区*/

spin_unlock(&lock);  /*解锁*/

      自旋锁主要针对SMP 或单CPU 但内核可抢占的情况,对于单CPU 且内核不支持抢占的系统,自旋锁退化为空操作。尽管用了自旋锁可以保证临界区不受别的CPU 好本地CPU  内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候,还可能受到中断和底半部(BH)的影响,为了防止这种影响,就需要用到自旋锁的衍生。

获取自旋锁的衍生函数:

void spin_lock_irq(spinlock_t  *lock); /*获取自旋锁之前禁止中断*/

void spin_lock_irqsave(spinlock_t  *lock, unsigned long flags);/*获取自旋锁之前禁止中断,并且将先前的中断状态保存在flags 中*/

void spin_lock_bh(spinlock_t  *lock); /*在获取锁之前禁止软中断,但不禁止硬件中断*/

 

释放自旋锁的衍生函数:
void spin_unlock_irq(spinlock_t  *lock)

void spin_unlock_irqrestore(spinlock_t  *lock,unsigned long  flags);

void spin_unlock_bh(spinlock_t  *lock);

解锁的时候注意要一一对应去解锁。

自旋锁注意点:
(1)自旋锁实际上是忙等待,因此,只有占用锁的时间极短的情况下,使用自旋锁才是合理的。

(2)自旋锁可能导致系统死锁。

(3)自旋锁锁定期间不能调用可能引起调度的函数。如:copy_from_user()、copy_to_user()、kmalloc()、msleep()等函数。

(4)拥有自旋锁的代码是不能休眠的。

 

 

1.4.2  读写自旋锁

        它允许多个读进程并发执行,但是只允许一个写进程执行临界区代码,而且读写也是不能同时进行的。

1、定义和初始化读写自旋锁

rwlock_t  my_rwlock = RW_LOCK_UNLOCKED;  /* 静态初始化 */

rwlock_t  my_rwlock;

rwlock_init(&my_rwlock);  /* 动态初始化 */

2、读锁定

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);

3、读解锁

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);

        在对共享资源进行读取之前,应该先调用读锁定函数,完成之后调用读解锁函数。

4、写锁定

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);

void   write_trylock(rwlock_t  *lock);

5、写解锁

void  write_unlock(rwlock_t  *lock);

void  write_unlock_irqrestore(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);  /*初始化*/

 

read_lock(&lock);  /*读取前先获取锁*/

...../*临界区资源*/

read_unlock(&lock); /*读完后解锁*/

 

write_lock_irqsave(&lock, flags);  /*写前先获取锁*/

...../*临界区资源*/

write_unlock_irqrestore(&lock,flags); /*写完后解锁*/

 

 

1.4.3  顺序锁(sequence lock)

       顺序锁是对读写锁的一种优化,读执行单元在写执行单元对被顺序锁保护的资源进行写操作时仍然可以继续读,而不必等地写执行单元完成写操作,写执行单元也不必等待所有读执行单元完成读操作才进去写操作。但是,写执行单元与写执行单元依然是互斥的。并且,在读执行单元读操作期间,写执行单元已经发生了写操作,那么读执行单元必须进行重读操作,以便确保读取的数据是完整的,这种锁对于读写同时进行概率比较小的情况,性能是非常好的。

       顺序锁有个限制,它必须要求被保护的共享资源不包含有指针,因为写执行单元可能使得指针失效,但读执行单元如果正要访问该指针,就会导致oops。

1、初始化顺序锁

seqlock_t  lock1 = SEQLOCK_UNLOCKED; /*静态初始化*/

 

seqlock  lock2;  /*动态初始化*/

seqlock_init(&lock2)

 

2、获取顺序锁

void  write_seqlock(seqlock_t  *s1);

void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags)

void write_seqlock_irq(seqlock_t  *lock);

void write_seqlock_bh(seqlock_t *lock);

int  write_tryseqlock(seqlock_t  *s1);

 

3、释放顺序锁

void  write_sequnlock(seqlock_t  *s1);

void write_sequnlock_irqsave(seqlock_t *lock, unsigned long flags)

void write_sequnlock_irq(seqlock_t  *lock);

void write_sequnlock_bh(seqlock_t *lock);

 

写执行单元使用顺序锁的模式如下:

write_seqlock(&seqlock_a);

/*写操作代码*/

........

write_sequnlock(&seqlock_a);

 

 

4、读开始

unsigned read_seqbegin(const  seqlock_t  *s1);

unsigned read_seqbegin_irqsave(seqlock_t  *lock, unsigned  long  flags);

 

5、重读

int  read_seqretry(const  seqlock_t   *s1,  unsigned  iv);

int read_seqretry_irqrestore(seqlock_t  *lock,unsigned  int  seq,unsigned  long  flags);

读执行单元使用顺序锁的模式如下:

unsigned int seq;

do{

       seq = read_seqbegin(&seqlock_a);

       /*读操作代码*/

       .......

}while (read_seqretry(&seqlock_a,   seq));

 

1.5  信号量

1.5.1  信号量的使用

        信号量(semaphore)是用于保护临界区的一种最常用的办法,它的使用方法与自旋锁是类似的,但是,与自旋锁不同的是,当获取不到信号量的时候,进程不会自旋而是进入睡眠的等待状态。

1、定义信号量

struct  semaphore  sem;

2、初始化信号量

void  sema_init(struct  semaphore  *sem,  int  val); /*初始化信号量的值为 val */

更常用的是下面这二个宏:

#define  init_MUTEX(sem)    sema_init(sem, 1)

#define  init_MUTEX_LOCKED(sem)  sem_init(sem, 0)

然而,下面这两个宏是定义并初始化信号量的“快捷方式”

DECLARE_MUTEX(name)   /*一个称为name信号量变量被初始化为 1 */

DECLARE_MUTEX_LOCKED(name)  /*一个称为name信号量变量被初始化为 0 */

 

3、获得信号量

/*该函数用于获取信号量,若获取不成功则进入不可中断的睡眠状态*/

void  down(struct semaphore  *sem);

/*该函数用于获取信号量,若获取不成功则进入可中断的睡眠状态*/

void  down_interruptible(struct  semaphore  *sem);

/*该函数用于获取信号量,若获取不成功立刻返回 -EBUSY*/

int  down_trylock(struct  sempahore  *sem);

 

4、释放信号量

void  up(struct  semaphore  *sem);  /*释放信号量 sem ,并唤醒等待者*/

信号量的一般用法:

DECLARE_MUTEX(mount_sem); /*定义一个信号量mount_sem,并初始化为 1 */

down(&mount_sem);  /* 获取信号量,保护临界区*/

 

.....

critical section /*临界区*/

.....

 

up(&mount_sem);  /*释放信号量*/

 

1.5.2 读写信号量

      读写信号量可能引起进程阻塞,但是它允许多个读执行单元同时访问共享资源,但最多只能有一个写执行单元。

1、定义和初始化读写信号量

struct  rw_semaphore  my_rws;  /*定义读写信号量*/

void   init_rwsem(struct  rw_semaphore  *sem);  /*初始化读写信号量*/

2、读信号量获取

void  down_read(struct  rw_semaphore  *sem);

int  down_read_trylock(struct  rw_semaphore  *sem);

3、读信号量释放

void  up_read(struct  rw_semaphore  *sem);

4、写信号量获取

void  down_write(struct  rw_semaphore  *sem);

int  down_write_trylock(struct   rw_semaphore  *sem);

5、写信号量释放

void up_write(struct  rw_semaphore  *sem);

 

1.5.3 completion

      完成量(completion)用于一个执行单元等待另外一个执行单元执行完某事。

1、定义完成量

struct  completion  my_completion;

2、初始化完成量

init_completion(&my_completion);

3、定义并初始化的“快捷方式”

DECLARE_COMPLETION(my_completion)

4、等待完成量

void wait_for_completion(struct  completion *c); /*等待一个 completion 被唤醒*/

5、唤醒完成量

void  complete(struct  completion  *c);  /*只唤醒一个等待执行单元*/

void  complete(struct  completion  *c);  /*唤醒全部等待执行单元*/

 

1.5.4  自旋锁VS信号量

       信号量是进程级的,用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的。如果竞争失败,会发送进程上下文切换,当前进程进入睡眠状态,CPU 将运行其他进程。鉴于开销比较大,只有当进程资源时间较长时,选用信号量才是比较合适的选择。然而,当所要保护的临界区访问时间比较短时,用自旋锁是比较方便的。

      

总结:

        解决并发与竞态的方法有(按本文顺序):

(1)中断屏蔽

(2)原子操作(包括位和整型原子)

(3)自旋锁

(4)读写自旋锁

(5)顺序锁(读写自旋锁的进化)

(6)信号量

(7)读写信号量

(8)完成量

        其中,中断屏蔽很少单独被使用,原子操作只能针对整数进行,因此自旋锁和信号量应用最为广泛。自旋锁会导致死循环,锁定期间内不允许阻塞,因此要求锁定的临界区小;信号量允许临界区阻塞,可以适用于临界区大的情况。读写自旋锁和读写信号量分别是放宽了条件的自旋锁 信号量,它们允许多个执行单元对共享资源的并发读。

 

结束语:

            本文比较多的API,不过只要耐心阅读,可以发现很多是相似,其实在驱动当中运用锁机制没有想象的那么恐怖,熟悉掌握之后会发现,其实很多是可以套用的。下一篇,我们分析并发的高级字符驱动程序。最后,祝大家学习愉快大笑

 

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值