Linux设备驱动开发详解总结(二)之并发与竞争

2023-11-14

转载地址:http://blog.csdn.net/lwj103862095/article/details/8548500
Linux设备驱动中必须解决一个问题是多个进程对共享资源的并发访问,并发的访问会导致竞态,在当今的Linux内核中,支持SMP与内核抢占的环境下,更是充满了并发与竞态。幸运的是,Linux 提供了多钟解决竞态问题的方式,这些方式适合不同的应用场景。例如:中断屏蔽、原子操作、自旋锁、信号量等等并发控制机制。
1.1 并发与竞态
并发是指多个执行单元同时、并发被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态。
临界区概念是为解决竞态条件问题而产生的,一个临界区是一个不允许多路访问的受保护的代码,这段代码可以操纵共享数据或共享服务。临界区操纵坚持互斥锁原则(当一个线程处于临界区中,其他所有线程都不能进入临界区)。然而,临界区中需要解决的一个问题是死锁。
1.2 中断屏蔽
在单CPU 范围内避免竞态的一种简单而省事的方法是进入临界区之前屏蔽系统的中断。CPU 一般都具有屏蔽中断和打开中断的功能,这个功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,有效的防止了某些竞态条件的发送,总之,中断屏蔽将使得中断与进程之间的并发不再发生。
中断屏蔽的使用方法:
local_irq_disable() /屏蔽本地CPU 中断/

critical section /临界区受保护的数据/

local_irq_enable() /打开本地CPU 中断/
由于Linux 的异步I/O、进程调度等很多重要操作都依赖于中断,中断对内核的运行非常重要,在屏蔽中断期间的所有中断都无法得到处理,因此长时间屏蔽中断是非常危险的,有可能造成数据的丢失,甚至系统崩溃的后果。这就要求在屏蔽了中断后,当前的内核执行路径要尽快地执行完临界区代码。
与local_irq_disable() 不同的是,local_irq_save(flags) 除了进行禁止中断的操作外,还保存当前CPU 的中断状态位信息;与local_irq_enable() 不同的是,local_irq_restore(flags) 除了打开中断的操作外,还恢复了CPU 被打断前的中断状态位信息。
1.3 原子操作
原子操作指的是在执行过程中不会被别的代码路径所中断的操作,Linux 内核提供了两类原子操作——位原子操作和整型原子操作。它们的共同点是在任何情况下都是原子的,内核代码可以安全地调用它们而不被打断。然而,位和整型变量原子操作都依赖于底层CPU 的原子操作来实现,因此这些函数的实现都与 CPU 架构密切相关。
1.3.1 整型原子操作
1、设置原子变量的值
void atomic_set(atomic *v,int i); /*设置原子变量的值为 i */
atomic_t v = ATOMIC_INIT(0); /*定义原子变量 v 并初始化为 0 */
2、获取原子变量的值
int atomic_read(atomic_t *v) /返回原子变量 v 的当前值/
3、原子变量加/减
void atomic_add(int i,atomic_t *v) /*原子变量增加 i */
void atomic_sub(int i,atomic_t *v) /*原子变量减少 i */
4、原子变量自增/自减
void atomic_inc(atomic_t *v) /*原子变量增加 1 */
void atomic_dec(atomic_t *v) /*原子变量减少 1 */
5、操作并测试
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
上述操作对原子变量执行自增、自减和减操作后测试其是否为 0 ,若为 0 返回true,否则返回false。注意:没有atomic_add_and_test(int i, atomic_t *v)。
6、操作并返回
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
上述操作对原子变量进行加/减和自增/自减操作,并返回新的值。
1.3.2 位原子操作
1、设置位
void set_bit(nr,void *addr);/*设置addr 指向的数据项的第 nr 位为1 */
2、清除位
void clear_bit(nr,void *addr)/*设置addr 指向的数据项的第 nr 位为0 */
3、取反位
void change_bit(nr,void *addr); /对addr 指向的数据项的第 nr 位取反操作/
4、测试位
test_bit(nr,void *addr);/返回addr 指向的数据项的第 nr位/
5、测试并操作位
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr,void *addr);
int test_amd_change_bit(nr,void *addr);
1.4 自旋锁
自旋锁(spin lock)是一种典型的对临界资源进行互斥访问的手段。为了获得一个自旋锁,在某CPU 上运行的代码需先执行一个原子操作,该操作测试并设置某个内存变量,由于它是原子操作,所以在该操作完成之前其他执行单元不能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,则程序将在一个小的循环里面重复这个“测试并设置” 操作,即进行所谓的“自旋”。
理解自旋锁最简单的方法是把它当做一个变量看待,该变量把一个临界区标记为“我在这运行了,你们都稍等一会”,或者标记为“我当前不在运行,可以被使用”。
Linux中与自旋锁相关操作有:
1、定义自旋锁
spinlock_t my_lock;
2、初始化自旋锁
spinlock_t my_lock = SPIN_LOCK_UNLOCKED; /静态初始化自旋锁/
void spin_lock_init(spinlock_t *lock); /动态初始化自旋锁/
3、获取自旋锁
/若获得锁立刻返回真,否则自旋在那里直到该锁保持者释放/
void spin_lock(spinlock_t *lock);
/若获得锁立刻返回真,否则立刻返回假,并不会自旋等待/
void spin_trylock(spinlock_t *lock)
4、释放自旋锁
void spin_unlock(spinlock_t *lock)
自旋锁的一般用法:
spinlock_t lock; /定义一个自旋锁/
spin_lock_init(&lock); /动态初始化一个自旋锁/

spin_lock(&lock); /获取自旋锁,保护临界区/
…/临界区/
spin_unlock(&lock); /解锁/
自旋锁主要针对SMP 或单CPU 但内核可抢占的情况,对于单CPU 且内核不支持抢占的系统,自旋锁退化为空操作。尽管用了自旋锁可以保证临界区不受别的CPU和本地CPU内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候,还可能受到中断和底半部(BH)的影响,为了防止这种影响,就需要用到自旋锁的衍生。
获取自旋锁的衍生函数:
void spin_lock_irq(spinlock_t *lock); /获取自旋锁之前禁止中断/
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);/获取自旋锁之前禁止中断,并且将先前的中断状态保存在flags 中/
void spin_lock_bh(spinlock_t *lock); /在获取锁之前禁止软中断,但不禁止硬件中断/
释放自旋锁的衍生函数:
void spin_unlock_irq(spinlock_t *lock)
void spin_unlock_irqrestore(spinlock_t *lock,unsigned long flags);
void spin_unlock_bh(spinlock_t lock);
解锁的时候注意要一一对应去解锁。
自旋锁注意点:
(1)自旋锁实际上是忙等待,因此,只有占用锁的时间极短的情况下,使用自旋锁才是合理的。
(2)自旋锁可能导致系统死锁。
(3)自旋锁锁定期间不能调用可能引起调度的函数。如:copy_from_user()、copy_to_user()、kmalloc()、msleep()等函数。
(4)拥有自旋锁的代码是不能休眠的。
1.4.2 读写自旋锁
它允许多个读进程并发执行,但是只允许一个写进程执行临界区代码,而且读写也是不能同时进行的。
1、定义和初始化读写自旋锁
rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /
静态初始化 /
rwlock_t my_rwlock;
rwlock_init(&my_rwlock); /
动态初始化 */
2、读锁定
void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);
3、读解锁
void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);
在对共享资源进行读取之前,应该先调用读锁定函数,完成之后调用读解锁函数。
4、写锁定
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
void write_trylock(rwlock_t *lock);
5、写解锁
void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);
在对共享资源进行写之前,应该先调用写锁定函数,完成之后应调用写解锁函数。
读写自旋锁的一般用法:
rwlock_t lock; /定义一个读写自旋锁 rwlock/
rwlock_init(&lock); /初始化/
read_lock(&lock); /读取前先获取锁/
…/临界区资源/
read_unlock(&lock); /读完后解锁/
write_lock_irqsave(&lock, flags); /写前先获取锁/
…/临界区资源/
write_unlock_irqrestore(&lock,flags); /写完后解锁/
1.4.3 顺序锁(sequence lock)
顺序锁是对读写锁的一种优化,读执行单元在写执行单元对被顺序锁保护的资源进行写操作时仍然可以继续读,而不必等地写执行单元完成写操作,写执行单元也不必等待所有读执行单元完成读操作才进去写操作。但是,写执行单元与写执行单元依然是互斥的。并且,在读执行单元读操作期间,写执行单元已经发生了写操作,那么读执行单元必须进行重读操作,以便确保读取的数据是完整的,这种锁对于读写同时进行概率比较小的情况,性能是非常好的。
顺序锁有个限制,它必须要求被保护的共享资源不包含有指针,因为写执行单元可能使得指针失效,但读执行单元如果正要访问该指针,就会导致oops。
1、初始化顺序锁
seqlock_t lock1 = SEQLOCK_UNLOCKED; /静态初始化/
seqlock lock2; /动态初始化/
seqlock_init(&lock2)
2、获取顺序锁
void write_seqlock(seqlock_t *s1);
void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags)
void write_seqlock_irq(seqlock_t *lock);
void write_seqlock_bh(seqlock_t *lock);
int write_tryseqlock(seqlock_t *s1);
3、释放顺序锁
void write_sequnlock(seqlock_t *s1);
void write_sequnlock_irqsave(seqlock_t *lock, unsigned long flags)
void write_sequnlock_irq(seqlock_t *lock);
void write_sequnlock_bh(seqlock_t *lock);
写执行单元使用顺序锁的模式如下:
write_seqlock(&seqlock_a);
/写操作代码/

write_sequnlock(&seqlock_a);
4、读开始
unsigned read_seqbegin(const seqlock_t *s1);
unsigned read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);
5、重读
int read_seqretry(const seqlock_t *s1, unsigned iv);
int read_seqretry_irqrestore(seqlock_t *lock,unsigned int seq,unsigned long flags);
读执行单元使用顺序锁的模式如下:
unsigned int seq;
do{
seq = read_seqbegin(&seqlock_a);
/读操作代码/

}while (read_seqretry(&seqlock_a, seq));
1.5 信号量
1.5.1 信号量的使用
信号量(semaphore)是用于保护临界区的一种最常用的办法,它的使用方法与自旋锁是类似的,但是,与自旋锁不同的是,当获取不到信号量的时候,进程不会自旋而是进入睡眠的等待状态。
1、定义信号量
struct semaphore sem;
2、初始化信号量
void sema_init(struct semaphore *sem, int val); /*初始化信号量的值为 val */
更常用的是下面这二个宏:
#define init_MUTEX(sem) sema_init(sem, 1)
#define init_MUTEX_LOCKED(sem) sem_init(sem, 0)
然而,下面这两个宏是定义并初始化信号量的“快捷方式”
DECLARE_MUTEX(name) /*一个称为name信号量变量被初始化为 1 */
DECLARE_MUTEX_LOCKED(name) /*一个称为name信号量变量被初始化为 0 */
3、获得信号量
/该函数用于获取信号量,若获取不成功则进入不可中断的睡眠状态/
void down(struct semaphore *sem);
/该函数用于获取信号量,若获取不成功则进入可中断的睡眠状态/
void down_interruptible(struct semaphore *sem);
/该函数用于获取信号量,若获取不成功立刻返回 -EBUSY/
int down_trylock(struct sempahore *sem);
4、释放信号量
void up(struct semaphore *sem); /释放信号量 sem ,并唤醒等待者/
信号量的一般用法:
DECLARE_MUTEX(mount_sem); /定义一个信号量mount_sem,并初始化为 1 /
down(&mount_sem); /
获取信号量,保护临界区
/

critical section /临界区/

up(&mount_sem); /释放信号量/
1.5.2 读写信号量
读写信号量可能引起进程阻塞,但是它允许多个读执行单元同时访问共享资源,但最多只能有一个写执行单元。
1、定义和初始化读写信号量
struct rw_semaphore my_rws; /定义读写信号量/
void init_rwsem(struct rw_semaphore *sem); /初始化读写信号量/
2、读信号量获取
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
3、读信号量释放
void up_read(struct rw_semaphore *sem);
4、写信号量获取
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
5、写信号量释放
void up_write(struct rw_semaphore *sem);
1.5.3 completion
完成量(completion)用于一个执行单元等待另外一个执行单元执行完某事。
1、定义完成量
struct completion my_completion;
2、初始化完成量
init_completion(&my_completion);
3、定义并初始化的“快捷方式”
DECLARE_COMPLETION(my_completion)
4、等待完成量
void wait_for_completion(struct completion *c); /等待一个 completion 被唤醒/
5、唤醒完成量
void complete(struct completion *c); /只唤醒一个等待执行单元/
void complete(struct completion *c); /唤醒全部等待执行单元/
1.5.4 自旋锁VS信号量
信号量是进程级的,用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的。如果竞争失败,会发送进程上下文切换,当前进程进入睡眠状态,CPU 将运行其他进程。鉴于开销比较大,只有当进程资源时间较长时,选用信号量才是比较合适的选择。然而,当所要保护的临界区访问时间比较短时,用自旋锁是比较方便的。
总结:
解决并发与竞态的方法有(按本文顺序):
(1)中断屏蔽
(2)原子操作(包括位和整型原子)
(3)自旋锁
(4)读写自旋锁
(5)顺序锁(读写自旋锁的进化)
(6)信号量
(7)读写信号量
(8)完成量
其中,中断屏蔽很少单独被使用,原子操作只能针对整数进行,因此自旋锁和信号量应用最为广泛。自旋锁会导致死循环,锁定期间内不允许阻塞,因此要求锁定的临界区小;信号量允许临界区阻塞,可以适用于临界区大的情况。读写自旋锁和读写信号量分别是放宽了条件的自旋锁 信号量,它们允许多个执行单元对共享资源的并发读。
结束语:
本文比较多的API,不过只要耐心阅读,可以发现很多是相似,其实在驱动当中运用锁机制没有想象的那么恐怖,熟悉掌握之后会发现,其实很多是可以套用的。下一篇,我们分析并发的高级字符驱动程序。最后,祝大家学习愉快大笑。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux设备驱动开发详解总结(二)之并发与竞争 的相关文章

随机推荐

  • 解决Windows server 2003不认U盘

    解决Windows server 2003不认U盘或移动硬盘 2009 05 18 23 08 47 标签 电脑 usb接口 移动硬盘 盘符 it 分类 电脑网络 答案一 快速解决办法 一 U盘以及移动硬盘自动装载也是一样的 WINDOWS
  • Exception in thread “main“ java.lang.ArrayIndexOutOfBoundsException: 6问题

    今天在java中出现了一个这样的问题 Exception in thread main java lang ArrayIndexOutOfBoundsException 6 at com wyt demo3 Role attack Role
  • valgrind android编译、安装

    valgrind android编译 安装 参见valgrind 3 12 0 tar bz2中的README android文件 以下步骤 遵循README android说明 注意VALGRIND LIB是程序内部环境变量 export
  • static在C和C++中的区别

    以下内容参考博客 https www cnblogs com Manual Linux p 8870038 html static在C语言中的区别 1 static修饰变量的时候 静态局部变量只被执行一次 延长了整个局部变量的生命周期 直到
  • linux下RDP客户端及服务器

    tsclient redsktop remmina gt 对ubuntu支持的非常不错 XRdp 集合vnc作为rdp服务器端使用
  • Java注释及分隔符 基础知识

    一 用于单行注释 用于多行注释 文档注释 文档注释属于多行注释的一种 二 空白符 空格 制表符 t 走页换页 f 回车 r 换行 n
  • 使用python指定个数随机生成一组混合字符集

    今天做测试想生成混合的id或者密码 思考了有很多方式 比如可以加入datetime库 然后截取一部分 或者随机生成一部分 进行替换 添加 这里采取一种简单易懂的方式 一 运行结果示范 就是这种效果 生成多少位数 18 ry3Gu aVr8V
  • STM32 基础系列教程 30 - 文件系统

    前言 学习stm32中FATFS 文件系统的基础使用 学会文件的打开及读写删除等基本操作 理解文件系统基本概念 示例详解 基于硬件平台 STM32F10C8T6最小系统板 MCU 的型号是 STM32F103c8t6 使用stm32cube
  • 学习HC-SR04超声波测距模块,代码附带卡尔曼滤波

    硬件引脚 VCC 供5V的电压 一定要是5v GND 接地 Trig HC SR04超声波测距模块上的触发引脚 用于向模块发送一个10微秒的高电平触发信号 触发模块开始进行距离测量 Echo 用于接收超声波回波信号的引脚 工作原理 使用HC
  • js根据本地文件路径上传文件(流上传)

    最近使用vue做了个项目 把本地指定url下的png图片上传 废话不多说 直接上代码 var fs require fs 需要引入nodejs中的文件操作部分 var http require http 需要引入nodejs中http请求部
  • 软件自动化测试工具/平台的挑战

    今天在微信读书偶然读到 高效自动化测试平台 设计与开发实战 作者徐德晨和茹炳晟 该书1 2章节详细讲述了软件自动化测试工具 平台的七个挑战 下面结合一站式开源持续测试平台项目MeterSphere详解这七个挑战 GitHub metersp
  • linux系统编程-1、基础知识

    前言 Linux系统编程的基础系列文章 随着不断学习会将一些知识点进行更新 前期主要是简单了解和学习 文章目录 shell bash 命令和路经补齐 历史记录 目录和文件 类Unix系统目录结构 用户目录 ls cd which pwd m
  • 请用美丽欢呼-------Day38

    周末 双休 疯了两天 敲了寥寥的代码 却没少看了相关的文章 这电子书大行于世的年代 对工具的漠然简直就是对生命的亵渎 颠簸的公交车上算是告别了YY的惬意 这生活 感觉傻了点 可真够味 原本只是想写篇 html的发展历程 的 可XHTML 2
  • Java并发编程:并发容器之CopyOnWriteArrayList(转载)

    http www cnblogs com dolphin0520 p 3938914 html 原文链接 http ifeve com java copy on write Copy On Write简称COW 是一种用于程序设计中的优化策
  • 基于YOLOv8模型和CrowdHuman数据集的行人检测系统(PyTorch+Pyside6+YOLOv8模型)

    摘要 基于YOLOv8模型和CrowdHuman数据集的行人目标检测系统可用于日常生活中检测与定位行人 Human 利用深度学习算法可实现图片 视频 摄像头等方式的目标检测 另外本系统还支持图片 视频等格式的结果可视化与结果导出 本系统采用
  • python 利用表格批量修改文件夹(包括子文件夹)下所有文件名

    首先是获得需要修改文件的路径放入xlsx中 我一般直接在系统的搜索框中搜索 然后全选复制路径 偷个小懒 也可以再写个自动遍历所有文件获取地址 点击这个复制路径即可复制全部选中文件的路径 直接复制在表格第一列即可 便于读取 然后按照自己实际的
  • SQL调优的几个方法

    1 为什么调优 好处是什么 SQL语句在编写之后 对于数据量较少的表基本没有什么性能上的需求 但是如果考虑到性能方面的话 SQL语句优化就是必须的 2 如何调优 调有点方法有哪些 1 对查询进行优化 应尽量避免全表扫描 首先考虑在where
  • node 版本管理器 --- Volta

    鲸腾FE 来自恒生鲸腾网络 是一支专注于 web 前端的开发团队 并在 web 前端领域积累了多年疑难问题解决经验 崇尚高效 优质 成长 自由 快乐 前言 在我们的日常开发中经常会遇到这种情况 手上有好几个项目 每个项目的需求不同 然而不同
  • uniapp 原生安卓开发插件(module),以及android环境本地调试(一)

    uniapp 原生安卓开发插件 module 以及android环境本地调试 1 开发前景 由于uniapp 框架的局限先 有很多功能不能如原生android开发使用顺畅 因此 需要使用插件进行辅助 再由uniapp引入插件 使得功能完善
  • Linux设备驱动开发详解总结(二)之并发与竞争

    转载地址 http blog csdn net lwj103862095 article details 8548500 Linux设备驱动中必须解决一个问题是多个进程对共享资源的并发访问 并发的访问会导致竞态 在当今的Linux内核中 支