zoukankan      html  css  js  c++  java
  • 【linux】驱动-12-并发与竞态


    前言

    内核驱动的并发&竟态很容易理解,其解决方法也不能,看看例程就可以了。
    对于API,看看内核源码和内核文档即可。

    原文链接https://www.cnblogs.com/lizhuming/p/14907262.html

    12. 并发&竞态

    本章内容为驱动基石之一
    驱动只提供功能,不提供策略

    12.1 并发&竞态概念

    并发

    • 指多个单元同时、并行执行。
    • 但是并发执行的单元对共享资源的访问容易产生竞态
    • 单核的并发可以参考 MCU RTOS 多任务原理。看似并行,实质串行。不过也存在竞态

    并发产生原因(大概):

    • 多线程并发访问。
    • 抢占式并发访问。(linux2.6及高版本的内核为抢占式内核
    • 中断程序并发访问。
    • 多核(SMP)核间并发访问。

    竞态

    • 指并发的执行单元对共享资源的访问。
    • 竞态产生的条件:
      • 存在共享资源。
      • 对共享资源进行竞争访问。

    12.2 竞态解决方法

    需要解决竞态是因为要保护数据。
    确保每个时刻都只有一个执行单元访问共享资源。

    竞态解决方法有:

    • 原子操作
    • 自旋锁操作
    • 信号量操作
    • 互斥体操作

    12.3 原子

    参考文档:

    • Documentationatomic_t.txt
    • Documentationatomic_bitops.txt

    12.3.1 原子介绍

    都知道,在 C 的世界里,a = 10; 这样一个简单的赋值,到了汇编的世界就不止一条语句啦。若此时多线程往变量 a 的地址赋值,就可能会产生数据错误。

    原子操作就是不可分割操作。
    注意:原子操作只能对 整型变量位操作 具有保护功能。

    12.3.2 原子操作步骤

    原子操作

    • 定义原子变量&设置初始值。
    • 设置原子变量的值。
    • 获取原子变量的值。
    • 原子变量的 加/减。
    • 原子变量的 自加/自减。
    • 原子变量的 加/减 及返回值。
    • 原子变量测试函数。

    12.3.3 原子 API

    由于函数容易理解,所以就不像以前的笔记一样详细列出。

    整型原子的操作需要个 atomic_t 结构体。
    bit原子的操作只需要一个地址即可,是直接对内存操作。

    atomic_t 32bit 整型原子变量结构体

    //atomic_t类型结构体
    typedef struct 
    {
       int counter;
    }atomic_t;
    

    atomic64_t 64bit 整型原子变量结构体

    //atomic64_t 类型结构体
    typedef struct 
    {
       long long  counter;
    }atomic64_t;
    

    整型原子 API 汇总

    API 描述
    ATOMIC_INIT(int i) 定义原子变量时候的初始值
    void atomic_set(atomic_t *v, int i) 向 v 写入 i
    void atomic_read(atomic_t *v) 读取 v 的值
    void atomic_add(int i, atomic_t *v) v 加 i
    void atomic_sub(int i, atomic_t *v) v 减 i
    void atomic_inc(atomic_t *v) v 加 1
    void atomic_dec(atomic_t *v) v 减 1
    int atomic_add_return(int i, atomic_t *v) v 加 i ,返回 v 的结果
    int atomic_sub_return(int i, atomic_t *v) v 减 i ,返回 v 的结果
    int atomic_inc_return(int i, atomic_t *v) v 加 1 ,返回 v 的结果
    int atomic_dec_return(int i, atomic_t *v) v 减 1 ,返回 v 的结果
    int atomic_sub_and_test(int i, atomic_t *v) v 减 i 后是否为 0
    int atomic_inc_and_test(atomic_t *v) v 加 1 后是否为 0
    int atomic_dec_and_test(atomic_t *v) v 减 1 后是否为 0
    int atomic_add_negative(int i, atomic_t *v) v 加 i 后是否为 负数

    更多 API(如atomic_dec_unless_positive()、atomic_inc_unless_negative()) 请参考内核源码和推荐的文档。

    bit原子的操作不需要 atomic_t 结构体,它是直接对 内存 操作的。

    bit 原子 API 汇总

    API 描述
    void set_bit(int nr, void *p) 对地址 p 的第 nr 位置 1
    void clear_bit(int nr, void *p) 对地址 p 的第 nr 位置 0
    void change_bit(int nr, void *p) 对地址 p 的第 nr 位翻转
    int test_bit(int nr, void *p) 返回地址 p 的第 nr 位的值
    void test_and_set_bit(int nr, void *p) 对地址 p 的第 nr 位置 1,并返回原来的 nr 位值
    void test_and_clear_bit(int nr, void *p) 对地址 p 的第 nr 位置 0,并返回原来的 nr 位值
    void test_and_change_bit(int nr, void *p) 对地址 p 的第 nr 位翻转,并返回原来的 nr 位值

    12.4 自旋锁

    12.4.1 自旋锁介绍

    原子操作只能对整型变量或者bit进行保护。而自旋锁能对一个单元进行保护,是给代码段添加一把锁。

    自旋锁是实现互斥访问的常用手段。
    获取自旋锁后再运行代码才能被保护起来。

    自旋锁特点

    • 当使用自旋锁获取锁失败时(即需要访问的代码段被锁住了),线程不休眠,做死循环检测锁状态,直至自旋锁被释放。
    • 简单,不休眠,可在中断中使用。
    • 使用不当会导致死锁。如:
      • 递归获取锁:第一次获取锁成功,在自旋锁保护的代码段内进行获取锁,那便永远等不到解锁,导致死锁。

    自旋锁缺点

    • 死循环检测,占用系统资源。
    • 递归获取锁后会导致死锁。
    • 同一线程不能连续两次获取自旋锁,必须一获取一释放。
    • 自旋锁在锁定期间不能调用引起进程调度的函数,否则可能导致系统崩溃。

    12.4.2 自旋锁操作步骤

    自旋锁操作

    • 定义自旋锁。
    • 初始化自旋锁。
    • 获取自旋锁。
    • 释放自旋锁。

    自旋锁使用注意事项

    • 锁的持有时间要短。因为自旋锁是不会休眠的,以免其它线程获取锁等待太久,降低系统性能。
    • 自旋锁保护的临界区内不能调用引起线程休眠的 API 函数,否则可能引起死锁。
    • 不能递归获取自旋锁,否则会导致死锁。
    • 按多核思想编程。提高系统可移植性。

    12.4.3 自旋锁 API

    spinlock_t 结构体

    typedef struct
    {
       struct lock_impl internal_lock;
    }spinlock_t;
    

    自旋锁 API 汇总

    API 描述
    DEFINE_SPINLOCK(spinlock_t lock) 定义、初始化一个自选变量
    void spin_lock_init(spinlock_t *lock) 初始化一个自旋锁
    void spin_lock(spinlock_t *lock) 加锁,即是获取一个自旋锁
    int spin_trylock(spinlock_t *lock) 尝试获取自旋锁,不等待,成功返回 true,失败返回 false
    void spin_unlock(spinlock_t *lock) 释放自旋锁
    int spin_is_locked(spinlock_t *lock) 检查指定自旋锁是否已经被获取。若没有,则返回非0;否则返回 0
    void spin_lock_irq(spinlock_t *lock) 获取自旋锁并关中断(防止中断打断
    void spin_unlock_irq(spinlock_t *lock) 释放自旋锁并开中断
    spin_lock_irqsave(lock, flags) 获取自旋锁,并保存中断状态到flags。锁返回时,之前开的中断,之后也是开的;之前关,之后也是关
    spin_unlock_irqrestore(lock, flags) 释放自旋锁,并恢复中断状态,即是把 flags 值赋值给中断状态寄存器。

    12.4.4 读写自旋锁

    普通的自旋锁是一刀切的,不管访问者对临界区的操作是读还是写。
    但是实际上,很多共享资源都允许多个执行单元同时读,这是不影响数据的。

    所以,读写自旋锁 允许 读并发,但是不允许 写并发,且不允许读写同时出现。
    即有允许以下情景:

    • 多读。
    • 一写。

    读写自旋锁 结构体

    typedef struct
    {
       arch_rwlock_t raw_lock;
    }rwlock_t;
    

    读写自旋锁 API

    • 定义&初始化
    API 描述
    DEFINE_RWLOCK(rwlock_t lock) 定义、初始化一个自选变量
    void rwlock_init(rwlock_t *lock) 初始化一个自旋锁
    • 读锁 API
    API 描述
    void read_lock(rwlock_t *lock) 加锁,即是获取一个读自旋锁
    void read_unlock(rwlock_t *lock) 释放读自旋锁
    void read_lock_irq(rwlock_t *lock) 禁止本地中断,且加锁,即是获取一个读自旋锁
    void read_unlock_irq(rwlock_t *lock) 打开本地中断,释放读自旋锁
    void read_lock_irqsave(rwlock_t *lock, unsigned long flags) 保存本地中断状态,禁止本地中断,且加锁,即是获取一个读自旋锁
    void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags) 回复本地中断状态,且激活本地中断,释放读自旋锁
    void read_lock_bh(rwlock_t *lock) 关闭下半部,加锁,即是获取一个读自旋锁
    void read_unlock_bh(rwlock_t *lock) 打开下半部,释放读自旋锁
    • 写锁
      • 把前面读锁的前缀 read_ 改为 write_,即可。

    12.4.5 顺序锁

    顺序锁读写锁 的一个优化。

    读写锁 不允许同时出现。有以下前景:

    • 多读
    • 一写

    顺序锁 允许同时出现,但是只能出现一个写。有以下前景:

    • 多读
    • 一写
    • 多读一写

    顺序自旋锁 结构体

    typedef struct
    {
       struct seqcount seqcount;
       spinlock_t lock;
    }seqlock_t;
    

    顺序自旋锁 API

    • 定义&初始化
    API 描述
    DEFINE_SEQLOCK(seqlock_t sl) 定义、初始化一个自选变量
    void seqlock_init(seqlock_t *sl) 初始化一个自旋锁
    • 读锁 API
      • 需要注意的是,写操作的顺序锁,会对顺序号加1-2。若 read_seqretry() 检测到顺序号不一致,则请重新读去数据。
    API 描述
    unsigned read_seqbegin(const seqlock_t *sl) 加锁,并返回获取到的顺序锁的顺序号
    unsigned read_seqretry(const seqlock_t *sl) 读结束后调用该函数。用于检查在读的过程中是否有对资源进行写操作,若有,则返回1,建议重新读去数据。
    • 写锁 API
    API 描述
    void write_seqlock(seqlock_t *sl) 加锁,即是获取一个读自旋锁
    void write_sequnlock(seqlock_t *sl) 释放读自旋锁
    void write_seqlock_irq(seqlock_t *sl) 禁止本地中断,且加锁,即是获取一个读自旋锁
    void write_sequnlock_irq(seqlock_t *sl) 打开本地中断,释放读自旋锁
    void write_seqlock_irqsave(seqlock_t *sl, unsigned long flags) 保存本地中断状态,禁止本地中断,且加锁,即是获取一个读自旋锁
    void write_sequnlock_irqrestore(seqlock_t *sl, unsigned long flags) 回复本地中断状态,且激活本地中断,释放读自旋锁
    void write_seqlock_bh(seqlock_t *sl) 关闭下半部,加锁,即是获取一个读自旋锁
    void write_sequnlock_bh(seqlock_t *sl) 打开下半部,释放读自旋锁

    12.5 信号量

    12.5.1 信号量概念

    学过 RTOS 的都知道信号量了。可以看做一个全局计数器。

    信号量常用于同步和互斥

    信号量的获取失败后,线程可引入休眠,当信号量可用时,系统会通知其退出休眠。

    12.5.2 信号量操作

    信号量操作

    • 定义信号量。
    • 初始化信号量。
    • 尝试获取信号量。
    • 获取信号量。
    • 释放信号量。

    信号量使用注意事项

    • 适用于占用资源较长时间的情景。因为信号量可以引起休眠,占用系统资源少。若占用资源时间少的,建议使用 自旋锁 ,因为不用切换线程,系统开销小。
    • 不能用于中断。同样是因为信号量可以引起休眠。不过可以使用 down_interruptible() 函数。
    • 保护的临界区内可调用引起阻塞的 API

    12.5.3 信号量 API

    semaphore 结构体

    struct semaphore 
    {
        raw_spinlock_t    lock;
        unsigned int      count;
        struct list_head  wait_list;
    };
    
    API 描述
    DEFINE_SEMAPHORE(name) 定义一个信号量,并置为 1
    void sema_init(struct semaphore *sem, int val) 初始化信号量,并置为 val
    void down(struct semaphore *sem) 获取信号量。因为信号量会导致休眠,且不能被信号打断,因此不能在中断中使用该函数
    int down_trylock(struct semaphore *sem) 尝试获取信号量,不休眠。成功返回 0,失败返回 非0
    void down_interruptible(struct semaphore *sem) 获取信号量。就算导致休眠后,也能被信号打断,因此该函数可以在中断中使用
    void up(struct semaphore *sem) 释放信号量

    12.6 互斥体

    12.6.1 互斥体概念

    互斥体 的占用其实和 信号量量值为 1 的效果是一样的。
    但是互斥体的执行效率更高,毕竟,专业的API做专业的事嘛。

    12.6.2 互斥体操作

    互斥体执行操作

    • 定义互斥体。
    • 初始化互斥体。
    • 尝试获取互斥体。
    • 获取互斥体。
    • 释放互斥体。

    互斥体使用注意事项

    • 不能在中断中使用。因为 mutex 会导致休眠。除非使用函数 int mutex_lock_interruptible
    • 必须由 mutex 持有者释放。因为一次只有一条线程持有。
    • 保护的临界区内可调用引起阻塞的 API

    12.6.3 互斥体 API

    API 描述
    DEFINE_MUTEX(name) 定义并初始化一个 mutex 变量
    void mutex_init(mutex *lock) 初始化 mutex
    void mutex_lock(struct mutex *lock) 加锁,获取 mutex
    void mutex_unlock(struct mutex *lock) 释放 mutex
    int mutex_trylock(struct mutex *lock) 尝试获取 mutex。成功返回 1,失败返回 0
    int mutex_is_locked(struct mutex *lock) 判断 mutex 是否被上锁了。是返回 1,否返回 0
    void mutex_lock_interruptible(struct mutex *lock) 加锁,获取 mutex。获取失败进入休眠后,依然能被信号打断。支持在中断中使用。

    12.7 完成量

    12.7.1 完成量概念

    完成量(completion)。

    完成量用于一个执行单元等待另一个执行单元。

    12.7.2 完成量操作

    完成量操作

    • 定义完成量。
    • 初始化完成量。
    • 等待完成量。
    • 唤醒完成量。

    12.7.3 完成量 API

    完成量结构体

    struct completion {
    	unsigned int done;
    	wait_queue_head_t wait;
    };
    
    API 描述
    void complete(struct completion *x) 唤醒一个等待完成量 x 的线程
    void complete_all(struct completion *x) 唤醒所有等待完成量 x 的线程
    void wait_for_completion(struct completion *x) 等待一个完成量 x
    unsigned long wait_for_completion_timeout(struct completion *x, unsigned long timeout) 限时等待一个完成量 x
    void init_completion(struct completion *c) 初始化一个完成量
    void reinit_completion(struct completion *c) 重新初始化一个完成量
  • 相关阅读:
    Javaweb实现表单数据和多文件上传
    Java一般命名规范
    基于微信公众号的答题投票系统——项目开发心得体会记录
    C语言实现对二叉树的操作
    C语言使用链表实现学生信息管理系统
    C语言实现对队列的基本操作
    C语言使用顺序表实现对学生信息的管理系统
    PHP实现周和月起止时间计算方法
    IOC容器和注入方式
    Spring简介+HelloWorld
  • 原文地址:https://www.cnblogs.com/lizhuming/p/14907262.html
Copyright © 2011-2022 走看看