前言

内核驱动的并发&竟态很容易理解,其解决方法也不能,看看例程就可以了。

对于API,看看内核源码和内核文档即可。

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

12. 并发&竞态

本章内容为驱动基石之一

驱动只提供功能,不提供策略

12.1 并发&竞态概念

并发

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

并发产生原因(大概):

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

竞态

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

12.2 竞态解决方法

需要解决竞态是因为要保护数据。

确保每个时刻都只有一个执行单元访问共享资源。

竞态解决方法有:

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

12.3 原子

参考文档:

  • Documentation\atomic_t.txt
  • Documentation\atomic_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) 重新初始化一个完成量

【linux】驱动-12-并发与竞态的更多相关文章

  1. Linux驱动设计——并发与竞态控制

    并发的概念:多个执行单元同时.并行被执行. 共享资源:硬件资源(IO/外设等),软件上的全局变量.静态变量等. 四种并发控制机制(对共享资源互斥的访问):原子操作.自旋锁(spinlock).信号量( ...

  2. Linux内核中的并发与竞态概述

    1.前言 众所周知,Linux系统是一个多任务的操作系统,当多个任务同时访问同一片内存区域的时候,这些任务可能会相互覆盖内存中数据,从而造成内存中的数据混乱,问题严重的话,还可能会导致系统崩溃. 2. ...

  3. 《Linux 设备驱动程序》读后感。 并发,竞态,死锁。

    1. 概念 并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行. 来源: 1. Linux ...

  4. Linux内核分析(七)----并发与竞态

    原文:Linux内核分析(七)----并发与竞态 Linux内核分析(七) 这两天家里的事好多,我们今天继续接着上一次的内容学习,上次我们完善了字符设备控制方法,并深入分析了系统调用的实质,今天我们主 ...

  5. 漫画|Linux 并发、竞态、互斥锁、自旋锁、信号量都是什么鬼?(转)

    知乎链接:https://zhuanlan.zhihu.com/p/57354304 1. 锁的由来? 学习linux的时候,肯定会遇到各种和锁相关的知识,有时候自己学好了一点,感觉半桶水的自己已经可 ...

  6. LDD3之并发和竞态-completion(完毕量)的学习和验证

    LDD3之并发和竞态-completion(完毕量)的学习和验证 首先说下測试环境: Linux2.6.32.2 Mini2440开发板 一開始难以理解书上的书面语言,这里<linux中同步样例 ...

  7. Linux 设备驱动--- 并发 与 竞态 --- atomic_t --- atomic_dec_and_test --- 原子操作

    并发: 多个执行单元同时被执行. 竞态: 并发的执行单元对资源 ( 硬件资源和软件上的全局变量等 ) 的访问导致的竞争状态. 并发的处理: 处理并发的常用技术是加锁或者互斥,即保证在任何时间只有一个执 ...

  8. Linux驱动开发4——并发和竞态

    Linux系统处于一个高并发的运行环境,不管是系统调用还是中断都要求可重入,但是有一些系统资源处于临界区,因此,必须保证临界区资源访问的原子性. 对于临界区资源被占用时,发起访问的进程,有三种处理方法 ...

  9. linux设备驱动程序之并发和竞态(二)

    事实上这blog都是阅读ldd3时的一些总结,巩固自己的学习.也方便后期的使用.大家也能够直接阅读ldd3原文. 锁陷阱         所谓的锁陷阱就是防止死锁.         不明白的规则:   ...

随机推荐

  1. SSM中事务的配置模板

    Spring-tx.xml 配置思路: 1. 声明事务管理器DataSourceTransactionManager,并注入数据源dataSource属性 2.配置事务增强<tx:advice& ...

  2. 将项目连接到远程仓库git

    方式一: git clone "git中的项目地址",此时会生成一个新的项目2.该步骤用于生成一个本地仓库 将需要提交的所有文件除了node_module.git以及输出文件dis ...

  3. 面向对象JML系列作业总结

    面向对象JML系列作业总结 一.综述 本单元作业,由简到难地迭代式实现了三种JML需求,主要学习了面向规格的编程方法. 第一次:实现Path类和PathContainer类 第二次:继承PathCon ...

  4. 结对项目:求交点pro

    [2020 BUAA 软件工程]结对项目作业 项目 内容 课程:北航2020春软件工程 博客园班级博客 作业:阅读并撰写博客回答问题 结对项目作业 我在这个课程的目标是 积累两人结对编程过程中的经验 ...

  5. Django(26)HttpResponse对象和JsonResponse对象

    HttpResponse对象 Django服务器接收到客户端发送过来的请求后,会将提交上来的这些数据封装成一个HttpRequest对象传给视图函数.那么视图函数在处理完相关的逻辑后,也需要返回一个响 ...

  6. [bug] IDEA:application context not configured for this file

    参考 https://blog.csdn.net/a772304419/article/details/79680833

  7. 联想INTEL X86台式机 用光驱启动 usb光驱启动

    联想INTEL X86台式机  用光驱启动 usb光驱启动 启动项顺序 都要调整 主要顺序 自动顺序 出错顺序 按下f10 f12

  8. 选择“保留window设置、个人文件及应用”或者“升级安装windows并保留文件设置和应用程序”的 处理干净以后用ghost备份

    个人经验 第一次装好以后 把所有常用软件什么的 还有系统的更新全部装好 删去乱七八糟的临时文件啊什么的 处理干净以后用ghost备份下次需要重装直接从ghost镜像恢复 然后更新软件 打补丁 再备份 ...

  9. Linux 部署 iSCSI 服务端

    Linux 部署 iSCSI 服务端 服务端实验环境 iSCSI-server :RHEL8 IP:192.168.121.10 一.服务端安装 target 服务和 targetcli 命令行工具 ...

  10. 使用 dd 命令进行硬盘 I/O 性能检测

    使用 dd 命令进行硬盘 I/O 性能检测 作者: Vivek Gite 译者: LCTT DongShuaike | 2015-08-28 07:30   评论: 1 收藏: 6 如何使用dd命令测 ...