《LINUX设备驱动开发详解》作者:华清远见第7章Linux设备驱动中的并发控制LinuxLinux7.17.27.57.6globalmem专业始于专注卓识源于远见 ‐ 2 ‐并发与竞态并发(concurrency)指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(raceconditions)。例如,对于globalmem设备,假设一个执行单元A对其写入3000个字符“a”,而另一个执行单元B对其写入4000个字符“b”,第三个执行单元C读取globalmem的所有字符。如果执行单元A、B的写操作如图7.1所示的顺序执行,执行单元C的读操作不会有问题。但是,如果执行单元A、B如图7.2所示的顺序执行,而执行单元C又“不合时宜”地读,则会读出3000个“b”。7.1比图7.2更复杂、更混乱的并发大量地存在于设备驱动中,只要并发的多个执行单元存在对共享资源的访问,竞态就可能发生。在Linux内核中,主要的竞态发生于如下几种情况。7.21.对称多处理器(SMP)的多个CPU SMP是一种紧耦合、共享存储的系统模型,其体系结构如图7.3所示,它的特点是多个CPU使用共同的系统总线,因此可访问共同的外设和储存器。2.单CPU内进程与抢占它的进程 Linux2.6内核支持抢占调度,一个进程在内核执行的时候可能被另一高优先级进程打断,进程与抢占它的进程访问共享资源的情况类似于SMP的多个CPU。3.中断(硬中断、软中断、Tasklet、底半部)与进程之间 中断可以打断正在执行的进程,如果中断处理程序访问进程正在访问的资源,则竞态也会发生。此外,中断也有可能被新的更高优先级的中断打断,因此,多个中断之间本身也可能引起并发而导致竞态。7.3SMP专业始于专注卓识源于远见 ‐ 3 ‐上述并发的发生情况除了SMP是真正的并行以外,其他的都是“宏观并行、微观串行”的,但其引发的实质问题和SMP相似。解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。访问共享资源的代码区域称为临界区(criticalsections),临界区需要以某种互斥机制加以保护。中断屏蔽、原子操作、自旋锁和信号量等是Linux设备驱动中可采用的互斥途径,7.2~7.5节将进行一一讲解。1.4中断屏蔽在单CPU范围内避免竞态的一种简单方法是在进入临界区之前屏蔽系统的中断。CPU一般都具备屏蔽中断和打开中断的功能,这项功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,防止某些竞态条件的发生。具体而言,中断屏蔽将使得中断与进程之间的并发不再发生,而且,由于Linux内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也就得以避免了。中断屏蔽的使用方法为:local_irq_disable()//屏蔽中断...criticalsection//临界区...local_irq_enable()//开中断由于Linux系统的异步I/O、进程调度等很多重要操作都依赖于中断,中断对于内核的运行非常重要,在屏蔽中断期间所有的中断都无法得到处理,因此长时间屏蔽中断是很危险的,有可能造成数据丢失甚至系统崩溃。这就要求在屏蔽了中断之后,当前的内核执行路径应当尽快地执行完临界区的代码。local_irq_disable()和local_irq_enable()都只能禁止和使能本CPU内的中断,因此,并不能解决SMP多CPU引发的竞态。因此,单独使用中断屏蔽通常不是一种值得推荐的避免竞态的方法,它适宜与自旋锁联合使用。与local_irq_disable()不同的是,local_irq_save(flags)除了进行禁止中断的操作以外,还保存目前CPU的中断位信息,local_irq_restore(flags)进行的是与local_irq_save(flags)相反的操作。如果只是想禁止中断的底半部,应使用local_bh_disable(),使能被local_bh_disable()禁止的底半部应该调用local_bh_enable()。1.4原子操作原子操作指的是在执行过程中不会被别的代码路径所中断的操作。Linux内核提供了一系列函数来实现内核中的原子操作,这些函数又分为两类,分别针对位和整型变量进行原子操作。它们的共同点是在任何情况下操作都是原子的,内核代码可以安全地调用它们而不被打断。位和整型变量原子操作都依赖底层CPU的原子操作来实现,因此所有这些函数都与CPU架构密切相关。7.3.1整型原子操作1.设置原子变量的值 voidatomic_set(atomic_t*v,inti);//设置原子变量的值为iatomic_tv=ATOMIC_INIT(0);//定义原子变量v并初始化为0专业始于专注卓识源于远见 ‐ 4 ‐2.获取原子变量的值 atomic_read(atomic_t*v);//返回原子变量的值3.原子变量加/减 voidatomic_add(inti,atomic_t*v);//原子变量增加ivoidatomic_sub(inti,atomic_t*v);//原子变量减少i4.原子变量自增/自减 voidatomic_inc(atomic_t*v);//原子变量增加1voidatomic_dec(atomic_t*v);//原子变量减少15.操作并测试 intatomic_inc_and_test(atomic_t*v);intatomic_dec_and_test(atomic_t*v);intatomic_sub_and_test(inti,atomic_t*v);上述操作对原子变量执行自增、自减和减操作后(注意没有加)测试其是否为0,为0则返回true,否则返回false。6.操作并返回 intatomic_add_return(inti,atomic_t*v);intatomic_sub_return(inti,atomic_t*v);intatomic_inc_return(atomic_t*v);intatomic_dec_return(atomic_t*v);上述操作对原子变量进行加/减和自增/自减操作,并返回新的值。7.3.2位原子操作1.设置位 voidset_bit(nr,void*addr);上述操作设置addr地址的第nr位,所谓设置位即将位写为1。2.清除位 voidclear_bit(nr,void*addr);上述操作清除addr地址的第nr位,所谓清除位即将位写为0。专业始于专注卓识源于远见 ‐ 5 ‐3.改变位 voidchange_bit(nr,void*addr);上述操作对addr地址的第nr位进行反置。4.测试位 test_bit(nr,void*addr);上述操作返回addr地址的第nr位。5.测试并操作位 inttest_and_set_bit(nr,void*addr);inttest_and_clear_bit(nr,void*addr);inttest_and_change_bit(nr,void*addr);上述test_and_xxx_bit(nr,void*addr)操作等同于执行test_bit(nr,void*addr)后再执行xxx_bit(nr,void*addr)。代码清单7.1给出了原子变量的使用实例,它用于使设备昀多只能被一个进程打开。代码清单7.1使用原子变量使设备只能被一个进程打开1staticatomic_txxx_available=ATOMIC_INIT(1);/*定义原子变量*/23staticintxxx_open(structinode*inode,structfile*filp)4{5...6if(!atomic_dec_and_test(&xxx_available))7{8atomic_inc(&xxx_available);9return-EBUSY;/*已经打开*/10}11...12return0;/*成功*/13}1415staticintxxx_release(structinode*inode,structfile*filp)16{17atomic_inc(&xxx_available);/*释放设备*/18return0;19}5自旋锁专业始于专注卓识源于远见 ‐ 6 ‐7.4.1自旋锁的使用自旋锁(spinlock)是一种对临界资源进行互斥手访问的典型手段,其名称来源于它的工作方式。为了获得一个自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试并设置(test-and-set)某个内存变量,由于它是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋”,通俗地说就是“在原地打转”。当自旋锁的持有者通过重置该变量释放这个自旋锁后,某个等待的“测试并设置”操作向其调用者报告锁已释放。理解自旋锁昀简单的方法是把它作为一个变量看待,该变量把一个临界区或者标记为“我当前在运行,请稍等一会”或者标记为“我当前不在运行,可以被使用”。如果A执行单元首先进入例程,它将持有自旋锁;当B执行单元试图进入同一个例程时,将获知自旋锁已被持有,需等到A执行单元释放后才能进入。Linux系统中与自旋锁相关的操作主要有如下4种。1.定义自旋锁 spinlock_tspin;2.初始化自旋锁 spin_lock_init(lock)该宏用于动态初始化自旋锁lock3.获得自旋锁 spin_lock(lock)该宏用于获得自旋锁lock,如果能够立即获得锁,它就马上返回,否则,它将自旋在那里,直到该自旋锁的保持者释放;spin_trylock(lock)该宏尝试获得自旋锁lock,如果能立即获得锁,它获得锁并返回真,否则立即返回假,实际上不再“在原地打转”;4.释放自旋锁 spin_unlock(lock)该宏释放自旋锁lock,它与spin_trylock或spin_lock配对使用。自旋锁一般这样被使用,如下所示://定义一个自旋锁spinlock_tlock;spin_lock_init(&lock);spin_lock(&lock);//获取自旋锁,保护临界区...//临界区spin_unlock(&lock);//解锁自旋锁主要针对SMP或单CPU但内核可抢占的情况,对于单CPU和内核不支持抢占的系统,自旋锁退化为空操作。在单CPU和内核可抢占的系统中,自旋锁持有期间内核的抢占将被禁止。由于内核可抢专业始于专注卓识源于远见 ‐ 7 ‐占的单CPU系统的行为实际很类似于SMP系统,因此,在这样的单CPU系统中使用自旋锁仍十分必要。尽管用了自旋锁可以保证临界区不受别的CPU和本CPU内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候还可能受到中断和底半部(BH)的影响。为了防止这种影响,就需要用到自旋锁的衍生。spin_lock()/spin_unlock()是自旋锁机制的基础,它们和关中断local_irq_disable()/开中断local_irq_enable()、关底半部local_bh_disable()/开底半部local_bh_enable()、关中断并保存状态字local_irq_save()/开中断并恢复状态local_irq_restore()结合就形成了整套自旋锁机制,关系如下所示:spin_lock_irq()=spin_lock()+local_irq_disable()spin_unlock_irq()=spin_unlock()+local_irq_enable()spin_lock_irqsave()=spin_unlock()+local_irq_save()spin_unlock