深入浅出:Linux设备驱动中的并发控制

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

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

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

中断屏蔽:

使用方法

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

原子操作:

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

实现整型原子操作的步骤如下:

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

2.获取原子变量的值

3.原子变量加减操作

4.原子变量自增/自减

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

6.操作并返回

实现 位原子操作如下:

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

我要着重谈一下:

自旋锁VS信号量

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

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

  自旋锁的操作步骤:

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

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

例子:

自旋锁不关心锁定的临界区究竟是如何执行的。不管是读操作还是写操作,实际上,对共享资源进行读取的时候是应该可以允许多个执行单元同时访问的,那么这样的话,自旋锁就有了弊端。于是便衍生出来一个读写锁。它保留了自旋的特性,但在对操作上面可以允许有多个单元进程同时操作。当然,读和写的时候不能同时进行。

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

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

 读写自旋锁一般用法:

顺序锁(seqlock):

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

写执行单元的操作顺序如下:

读执行单元的操作顺序如下:

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

具体操作:略

信号量的使用

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

相同点:只有得到信号量的进程才能执行临界区的代码。(linux自旋锁和信号量锁采用的都是“获得锁-访问临界区-释放锁”,可以称为“互斥三部曲”,实际存在于几乎所有多任务操作系统中)

不同点:当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。

信号量的操作:

信号量用于同步时只能唤醒一个执行单元,而完成量(completion)用于同步时可以唤醒所有等待的执行单元。

 自旋锁与互斥锁的选择

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

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部