6、设备驱动中的并发控制(一)

  在 Linux 设备驱动中必需要解决的一个问题是多个进程对共享资源的访问,并发的访问会致使竞态。编程

6.1 并发与竞态

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

  在 Linux 内核中,竞态主要发生于以下几种状况:架构

  • 对称多处理器(SMP)的多个 CPU
    • SMP 是一种耦合的、共享存储的系统模型,它的特色是多个 CPU 使用共同的系统总线,所以可访问共同的外设和存储器。
    • SMP 体系架构以下图:
    • 在 SMP 的状况下,两个核的竞态可能发生与 CPU0 的进程与 CPU1 的进程之间、CPU0 的进程与 CPU1 的中断之间以及 CPU0 的中断与 CPU1 的中断之间,下图中任何一条线链接的两个实体都有核间并发可能性。
  • 单 CPU 内进程与抢占它的进程
    • Linux 2.6 之后的内核支持内核抢占调度,一个进程在内核执行的时候可能耗完了本身的时间片,也可能被另外一个高优先级进程打断,进程与抢占它的进程访问共享资源的状况相似于 SMP 的多个 CPU。
  • 中断(硬中断、软中断、Tasklet、底半部)与进程之间
    • 中断能够打断正在执行的进程,若是中断服务程序访问进程正在访问的资源,则竞态会发生。
    • 中断可悲新的更高优先级的中断打断,多个中断之间自己也可能引发并发而致使竞态,但在 Linux 2.6.35 取消了中断嵌套。

  解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源的时候,其余的执行单元被禁止访问。并发

  访问共享资源的代码区域称为临界区,临界区须要被以某种互斥机制加以保护。中断屏蔽、原子操做、自旋锁、信号量、互斥体等是 Linux 设备驱动中可采用的互斥途径。函数

6.2 中断屏蔽

  中断屏蔽适用于单 CPU 范围内的竞态,即在进入临界区以前屏蔽系统的中断,可是在驱动编程中不值得推荐这么作,驱动一般要考虑平台特色而不假定本身在单核上运行。性能

  中断屏蔽使得中断与进程之间的并发再也不发生,并且因为 Linux 内核的进程调度等操做都依赖中断来实现,内核抢占进程之间的并发也能够避免了。测试

  中断屏蔽使用的函数:优化

  

   

   

  注意:不建议单独使用中断屏蔽,它适合与自旋锁配合使用。spa

6.3 原子操做

  原子操做可保证对一个整型数据的修改是排他性的。Linux 内核提供了一系列函数来实现内核中的原子操做,这些函数分为两类:分别针对位或整型进行原子操做。3d

  位和整型变量的原子操做都依赖于底层 CPU 的原子操做,这些函数都与 CPU 架构密切相关。

 6.3.1 整型原子操做

     

       

 6.3.2 位原子操做

    

     

       

 6.3.3 例子-使用原子操做

  

  原子变量初始化为 1,当打开设备的时候,断定原子变量减 1 以后,是否为 0,为 0 返回 TRUE(1),代表打开成功; 非0,返回 FALSE(0),表示当前设备正在打开状态。

 6.4 自旋锁

6.4.1 基本介绍

  自旋锁(Spin lock) 是一种典型的对临界资源进行互斥访问的手段,其名称来源于它的工做方式。为了得到一个自旋锁,在某 CPU 上执行的代码需先执行一个原子操做,该操做测试并设置某个内存变量。因为它是原子操做,因此在它的操做完成前其余执行单元不可能访问这个内存变量。若是测试结果代表锁已经空闲,则程序得到这个自旋锁并继续执行;若是测试结果代表锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”的操做,即进行所谓的“自旋”。当自旋锁的持有者经过重置该变量释放这个自旋锁后,某个等待的“测试并设置”操做向其调用者报告锁已释放。

  Linux 中与自旋锁相关的操做主要有如下四种:

 1 1.定义自旋锁
 2 spinlock_t lock;
 3 2.初始化自旋锁
 4 spin_lock_init(lock);    ///< 该宏用于动态初始化自旋锁
 5 3.得到自旋锁
 6 spin_lock(lock);        ///< 该宏用于得到自旋锁 lock, 
 7                         ///< 若是可以当即得到, 它就立刻返回
 8                         ///< 不然, 它将在那里自旋,直到该自旋锁的保持者释放
 9                         or
10 spin_trylock(lock);        ///< 该宏尝试得到自旋锁 lock
11                         ///< 若是可以当即得到, 它得到锁并返回 true
12                         ///< 不然当即返回 false, 实际上再也不“原地打转”
13 4.释放自旋锁
14 spin_unlock(lock);        ///< 必须与 spin_lock 和 spin_trylock 配对使用

  自旋锁主要针对 SMP 或单 CPU 但内核可抢占的状况,对于单 CPU和内核不支持抢占的状况,自旋锁退化为空操做。

  自旋锁和中断结合使用:

1 spin_lock_irq() = spin_lock() + local_irq_disable();
2 spin_unlock_irq() = spin_unlock() + local_irq_enable();
3 spin_lock_irqsave() = spin_lock() + local_irq_save();
4 spin_unlock_irqrestore() = spin_unlock() + local_irq_restore();
5 spin_lock_bh() = spin_lock() + local_bh_disable();
6 spin_unlock_bh() = spin_unlock() + local_bh_enable();

  在多核编程中,若是进程和中断可能访问同一片临界资源,通常须要在进程上下文调用上面的自旋锁和中断结合使用的函数,在中断上下文再调用 spin_lock() 等函数,这是防止当前的核被其余核的中断打断,防止核间并发。

  • 自旋锁使用中须要注意的问题:
    • 自旋锁其实是忙等锁,当锁不可用时,CPU 一直循环执行 “测试并设置” 该锁直到可用而取得该锁,CPU 在等待自旋锁时不作任何有用的工做,仅仅是等待。所以,只有在占用锁的时间极短的时候,使用自旋锁才合理。若临界区很大,须要长时间占用锁,使用自旋锁会下降系统的性能。
    • 自旋锁可能致使系统死锁。这种状况通常都是递归使用一个自旋锁。
    • 在自旋锁锁按期间不能调用可能引发进程调度的函数。
    • 在单核状况下编程,也应该认为本身的 CPU 是多核的。

   使用例子:

 1 /** 定义文件打开次数计数 */
 2 int xxx_count = 0;
 3 
 4 static int xxx_open(........)
 5 {
 6     ...
 7     spin_lock(&xxx_lock);
 8     if(xxx_count) {    /** 已经打开 */
 9         spin_unlock(&xxx_lock);
10         return -EBUSY;
11     }
12     xxx_count++;    ///< 增长使用计数
13     spin_unlock(&xxx_lock);
14     ...
15 
16     return 0;   ///< 成功
17 }
18 
19 static int xxx_release(.........)
20 {
21     spin_lock(&xxx_lock);
22     xxx_count--;    ///< 减小使用计数
23     spin_unlock(&xxx_lock);
24     return 0;
25 }

6.4.2 读写自旋锁

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

 1 1.定义自旋锁
 2 rwlock_t lock;
 3 2.初始化自旋锁
 4 rwlock_init(lock);    ///< 该宏用于动态初始化自旋锁
 5 3.读锁定
 6 void read_lock(rwlock_t *lock);
 7 void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
 8 void read_lock_irq(rwlock_t *lock);
 9 void read_lock_bh(rwlock_t *lock);
10 4.读解锁
11 void read_unlock(rwlock_t *lock);
12 void read_unlock_irqsave(rwlock_t *lock, unsigned long flags);
13 void read_unlock_irq(rwlock_t *lock);
14 void read_unlock_bh(rwlock_t *lock);
15 5.写锁定
16 void write_lock(rwlock_t *lock);
17 void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
18 void write_lock_irq(rwlock_t *lock);
19 void write_lock_bh(rwlock_t *lock);
20 void write_trylock(rwlock_t *lock);
21 6.写解锁
22 void write_unlock(rwlock_t *lock);
23 void write_unlock_irqsave(rwlock_t *lock, unsigned long flags);
24 void write_unlock_irq(rwlock_t *lock);
25 void write_unlock_bh(rwlock_t *lock);

6.4.3 顺序锁

  顺序锁(seqlock)是对读写锁的一种优化,若使用顺序锁,读执行单元不会被写执行单元阻塞,即读执行单元在写执行单元对被顺序锁保护的共享资源进行写操做的时候仍然能够继续读,而没必要等待写执行单元完成写操做,写执行单元也不须要等待全部读执行单元完成读操做才去进行写操做。但,写执行单元之间是互斥的。

  若读执行单元在读操做期间,写执行单元已经发生了写操做,那么,读执行单元必须从新读取数据,以便确保获得的数据是完整的。所以,在这种状况下,读端可能反复读屡次一样的区域才能获取到完整的数据。

  在内核中,写执行单元涉及的顺序操做以下:

 1 1.得到顺序锁
 2 void write_seqlock(seqlock_t *sl);
 3 void write_seqlock_irqsave(seqlock_t *sl, unsigned long flags);
 4 void write_seqlock_bh(seqlock_t *sl);
 5 void write_seqlock_irq(seqlock_t *sl);
 6 2.释放顺序锁
 7 void write_sequnlock(seqlock_t *sl);
 8 void write_sequnlock_irqrestore(seqlock_t *sl, unsigned long flags);
 9 void write_sequnlock_bh(seqlock_t *sl);
10 void write_sequnlock_irq(seqlock_t *sl);

  读执行单元涉及的操做以下:

1 1.读开始
2 unsigned read_seqbegin(const seqlock_t *sl);
3 unsigned read_seqbegin_irqsave(const seqlock_t *sl, unsigned long flags); ///< 4.0 以后内核已舍弃
4 2.重读
5 unsigned read_seqretry(const seqlock_t *sl, unsigned start);
6 unsigned read_seqretry_irqsave(const seqlock_t *sl, unsigned long flags); ///< 4.0 以后内核已舍弃

6.4.4 读-复制-更新

   RCU(Read-Copy-Update,读-复制-更新),它容许多个读执行单元同时访问被保护的数据,又容许多个读执行单元和多个写执行单元同时访问被保护的数据。但RCU 不能替代读写锁,对读执行单元的

 

 11】读锁定
 2 void rcu_read_lock(void);
 3 void rcu_read_lock_bh(void);
 42】读解锁
 5 void rcu_read_unlock(void);
 6 void rcu_read_unlock_bh(void);
 7 /** 使用 RCU 进行读模式 */
 8 rcu_read_lock();
 9 ... ///< 读临界区
10 rcu_read_unlock();
113】同步 RCU
12 /** 
13  *    此函数由 RCU 写执行单元调用,它将阻塞写执行单元,
14  *    直到 CPU 上全部的已经存在的读执行单元完成读临界区,写执行单元才能够执行下一步 
15  *    此函数并不须要等待后续读临界区的完成
16  */
17 void synchronize_rcu(void);
184】挂接回调
19 /** 
20  *    此函数由 RCU 写执行单元调用,它不阻塞写执行单元,可在中断上下文和软中断中使用
21  *    该函数把 func 挂接到 RCU 的回调函数链上,而后当即返回
22  *    挂接回调函数会在全部的已经存在的读执行单元完成读临界区后被执行
23  */
24 void call_rcu(struct rcu_head *head, rcu_callback_t func);
25 
26 /** 给 RCU 保护的指针赋一个新值 */
27 #define rcu_assign_pointer(p, v);
28 
29 /**
30  *    读端使用此宏获取一个 RCU 保护的指针,以后既能够安全的引用它
31  *     通常须要在 rcu_read_lock 和 rcu_read_unlock 保护的区间引用这个指针
32  */
33 #define rcu_dereference(p);
34 
35 /**
36  *    读端使用此宏获取一个 RCU 保护的指针,以后并不引用它
37  *     只关心指针的值,不关心指针指向的内容
38  *     例如可使用此宏判断指针是否为 NULL
39  */
40 #define rcu_access_pointer(p);
相关文章
相关标签/搜索