【linux】驱动-12-并发与竞态
前言
内核驱动的并发&竟态很容易理解,其解决方法也不能,看看例程就可以了。
对于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-并发与竞态的更多相关文章
- Linux驱动设计——并发与竞态控制
并发的概念:多个执行单元同时.并行被执行. 共享资源:硬件资源(IO/外设等),软件上的全局变量.静态变量等. 四种并发控制机制(对共享资源互斥的访问):原子操作.自旋锁(spinlock).信号量( ...
- Linux内核中的并发与竞态概述
1.前言 众所周知,Linux系统是一个多任务的操作系统,当多个任务同时访问同一片内存区域的时候,这些任务可能会相互覆盖内存中数据,从而造成内存中的数据混乱,问题严重的话,还可能会导致系统崩溃. 2. ...
- 《Linux 设备驱动程序》读后感。 并发,竞态,死锁。
1. 概念 并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行. 来源: 1. Linux ...
- Linux内核分析(七)----并发与竞态
原文:Linux内核分析(七)----并发与竞态 Linux内核分析(七) 这两天家里的事好多,我们今天继续接着上一次的内容学习,上次我们完善了字符设备控制方法,并深入分析了系统调用的实质,今天我们主 ...
- 漫画|Linux 并发、竞态、互斥锁、自旋锁、信号量都是什么鬼?(转)
知乎链接:https://zhuanlan.zhihu.com/p/57354304 1. 锁的由来? 学习linux的时候,肯定会遇到各种和锁相关的知识,有时候自己学好了一点,感觉半桶水的自己已经可 ...
- LDD3之并发和竞态-completion(完毕量)的学习和验证
LDD3之并发和竞态-completion(完毕量)的学习和验证 首先说下測试环境: Linux2.6.32.2 Mini2440开发板 一開始难以理解书上的书面语言,这里<linux中同步样例 ...
- Linux 设备驱动--- 并发 与 竞态 --- atomic_t --- atomic_dec_and_test --- 原子操作
并发: 多个执行单元同时被执行. 竞态: 并发的执行单元对资源 ( 硬件资源和软件上的全局变量等 ) 的访问导致的竞争状态. 并发的处理: 处理并发的常用技术是加锁或者互斥,即保证在任何时间只有一个执 ...
- Linux驱动开发4——并发和竞态
Linux系统处于一个高并发的运行环境,不管是系统调用还是中断都要求可重入,但是有一些系统资源处于临界区,因此,必须保证临界区资源访问的原子性. 对于临界区资源被占用时,发起访问的进程,有三种处理方法 ...
- linux设备驱动程序之并发和竞态(二)
事实上这blog都是阅读ldd3时的一些总结,巩固自己的学习.也方便后期的使用.大家也能够直接阅读ldd3原文. 锁陷阱 所谓的锁陷阱就是防止死锁. 不明白的规则: ...
随机推荐
- 栈(Stack) --- C# 自定义和微软官方的区别
最近在学习算法基础,本篇文章作为一个记录,也算是一次实践和总结.(顺便也深入C#运行时学习一下) 目录 1. 栈是什么 2. Stack 自定义实现 3. Stack C#官方实现 4. 区别 5. ...
- 【一】kubernetes学习笔记-Pod概念
一.Pod 控制器类型 Pod概念 当一个 Pod 创建后,Pause 容器就会随着 Pod 启动,只要是有 Pod,Pause 容器就要被启动. 在同一个 Pod 里面的容器不能出现端口冲突,否则这 ...
- springboot使用jwt进行权限验证
springboot使用jwt进行权限验证 依赖准备 首先导入对应的依赖 <dependencies> <dependency> <groupId>org.apac ...
- Smss.exe加载win32k.sys过程总结
windows操作系统初始化 windows操作系统再初始化的过程中,当内核完全初始化而且各个组件也已经准备好后会加载一个个用户进程smss.exe(会话管理器),此进程会接着调用NtSetSyste ...
- python爬虫——《英雄联盟》英雄及皮肤图片
还记得那些年一起网吧开黑通宵的日子吗?<英雄联盟>绝对是大学时期的风靡游戏,即使毕业多年的大学同学相聚,难免不怀念一番当时一起玩<英雄联盟>的日子. 今天就给大家分享一下英雄及 ...
- Uva 642 - Word Amalgamation sort qsort
Word Amalgamation In millions of newspapers across the United States there is a word game called J ...
- auto_increment 自增长
auto_increment create table t20( id int primary key auto_increment, (自增长必须为键) name char(16)); insert ...
- Solon Aop 特色开发(1)注入或手动获取配置
常规操作,先启动 Solon public class App{ public void main(String[] args){ Solon.start(App.class, args); } } ...
- 要求用户输入若干员工信息,格式为: name,age,gender,salary,hiredate
package day06; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util. ...
- 阿里云上安装 OpenStack 是什么体验
阿里云上跑火车(安装 OpenStack Train 版本),猜猜最终花了多少钱? 前言 前面给大家提供了用虚拟机安装 OpenStack 的镜像,虽然已经很简便了,但还是略显笨重.一来镜像文件比较大 ...