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


      在 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 内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也可以避免了。

      中断屏蔽使用的函数:

      

       

       

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

    6.3 原子操作

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

      位和整型变量的原子操作都依赖于底层 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);

      

  • 相关阅读:
    Java 发送http请求(get、post)
    java 压缩成zip文件、解压zip文件(可设置密码)
    bootstrap日历控件datetimepicker基本用法
    js/java 获取、添加、修改、删除cookie(最全)
    阿里云centos中mysql的安装及一些常识知识
    HDOJ 4869 Turn the pokers
    J2ee高并发情况下监听器
    硬盘杀手!Windows版Redis疯狂占用C盘空间!
    指针数组、数组指针、函数指针、指针函数总结
    STM32学习笔记之EXTI(外部中断)
  • 原文地址:https://www.cnblogs.com/kele-dad/p/11663039.html
Copyright © 2020-2023  润新知