Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >linux设备驱动第五篇:驱动中的并发与竟态

linux设备驱动第五篇:驱动中的并发与竟态

作者头像
程序员互动联盟
发布于 2018-03-12 03:07:40
发布于 2018-03-12 03:07:40
1.8K00
代码可运行
举报
运行总次数:0
代码可运行

综述

在上一篇介绍了linux驱动的调试方法,这一篇介绍一下在驱动编程中会遇到的并发和竟态以及如何处理并发和竞争。

首先什么是并发与竟态呢?并发(concurrency)指的是多个执行单元同时、并行被执行。而并发的执行单元对共享资源(硬件资源和软件上的全局、静态变量)的访问则容易导致竞态(race conditions)。可能导致并发和竟态的情况有:

  • SMP(Symmetric Multi-Processing),对称多处理结构。SMP是一种紧耦合、共享存储的系统模型,它的特点是多个CPU使用共同的系统总线,因此可访问共同的外设和存储器。
  • 中断。中断可 打断正在执行的进程,若中断处理程序访问进程正在访问的资源,则竞态也会发生。中断也可能被新的更高优先级的中断打断,因此,多个中断之间也可能引起并发而导致竞态。
  • 内核进程的抢占。linux是可抢占的,所以一个内核进程可能被另一个高优先级的内核进程抢占。如果两个进程共同访问共享资源,就会出现竟态。

以上三种情况只有SMP是真正意义上的并行,而其他都是宏观上的并行,微观上的串行。但其都会引发对临界共享区的竞争问题。而解决竞态问题的途径是保证对共享资源的互斥访问,即一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。那么linux内核中如何做到对对共享资源的互斥访问呢?在linux驱动编程中,常用的解决并发与竟态的手段有信号量与互斥锁,Completions 机制,自旋锁(spin lock),以及一些其他的不使用锁的实现方式。下面一一介绍。

信号量与互斥锁

信号量其实就是一个整型值,其核心是一个想进入临界区的进程将在相关信号量上调用 P; 如果信号量的值大于零, 这个值递减 1 并且进程继续. 相反,,如果信号量的值是 0 ( 或更小 ), 进程必须等待直到别人释放信号量. 解锁一个信号量通过调用 V 完成; 这个函数递增信号量的值,,并且, 如果需要, 唤醒等待的进程。而当信号量的初始值为1的时候,就变成了互斥锁。

信号量的典型使用形式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//声明信号量
struct semaphore sem;

//初始化信号量
void sema_init(struct semaphore *sem, int val)
    //常用下面两种形式
#define init_MUTEX(sem) sema_init(sem, 1)
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0)
    //以下是初始化信号量的快捷方式,最常用的
DECLARE_MUTEX(name)    //初始化name的信号量为1
DECLARE_MUTEX_LOCKED(name) //初始化信号量为0

//常用操作
DECLARE_MUTEX(mount_sem);
down(&mount_sem); //获取信号量
...
critical section    //临界区
...
up(&mount_sem);    //释放信号量

常用的down操作还有

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 类似down(),因为down()而进入休眠的进程不能被信号打断,而因为down_interruptible()而进入休眠的进程能被信号打断, 
// 信号也会导致该函数返回,此时返回值非0
int down_interruptible(struct semaphore *sem);
// 尝试获得信号量sem,若立即获得,它就获得该信号量并返回0,否则,返回非0.它不会导致调用者睡眠,可在中断上下文使用
int down_trylock(struct semaphore *sem);

Completions 机制

完成量(completion)提供了一种比信号量更好的同步机制,它用于一个执行单元等待另一个执行单元执行完某事。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
</pre></div><div><pre name="code" class="cpp">// 定义完成量
struct completion my_completion;
 
// 初始化completion
init_completion(&my_completion);
 
// 定义和初始化快捷方式:
DECLEAR_COMPLETION(my_completion);
 
// 等待一个completion被唤醒
void wait_for_completion(struct completion *c);
 
// 唤醒完成量
void cmplete(struct completion *c);
void cmplete_all(struct completion *c);

自旋锁

若一个进程要访问临界资源,测试锁空闲,则进程获得这个锁并继续执行;若测试结果表明锁扔被占用,进程将在一个小的循环内重复“测试并设置”操作,进行所谓的“自旋”,等待自旋锁持有者释放这个锁。自旋锁与互斥锁类似,但是互斥锁不能用在可能睡眠的代码中,而自旋锁可以用在可睡眠的代码中,典型的应用是可以用在中断处理函数中。自旋锁的相关操作:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 定义自旋锁 
spinlock_t spin; 
 
// 初始化自旋锁
spin_lock_init(lock);
 
// 获得自旋锁:若能立即获得锁,它获得锁并返回,否则,自旋,直到该锁持有者释放
spin_lock(lock); 
 
// 尝试获得自旋锁:若能立即获得锁,它获得并返回真,否则立即返回假,不再自旋
spin_trylock(lock); 
 
// 释放自旋锁: 与spin_lock(lock)和spin_trylock(lock)配对使用
spin_unlock(lock); 
 
  自旋锁的使用:
// 定义一个自旋锁
spinlock_t lock;
spin_lock_init(&lock);
 
spin_lock(&lock);  // 获取自旋锁,保护临界区
...  // 临界区
spin_unlock();  // 解锁

自旋锁持有期间内核的抢占将被禁止。自旋锁可以保证临界区不受别的CPU和本CPU内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候还可能受到中断和底半部(BH)的影响。为防止这种影响,需要用到自旋锁的衍生:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()

其他的一些选择

以上是linux驱动编程中经常用到的锁机制,下面讲一些内核中其他的一些实现。

不加锁算法

有时, 你可以重新打造你的算法来完全避免加锁的需要.。许多读者/写者情况 -- 如果只有一个写者 -- 常常能够在这个方式下工作.。如果写者小心使数据结构,由读者所见的,是一直一致的,,有可能创建一个不加锁的数据结构。在linux内核中就有一个通用的无锁的环形缓冲实现,具体内容参考<linux/kfifo.h>。

原子变量与位操作

原子操作指的是在执行过程中不会被别的代码路径所中断的操作。原子变量与位操作都是原子操作。以下是其相关操作介绍。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 设置原子变量的值
void atomic_set(atomic_t *v, int i);  // 设置原子变量的值为i
atomic_t v = ATOMIC_INIT(0);  // 定义原子变量v,并初始化为0
 
// 获取原子变量的值
atomic_read(atomic_t *v);  // 返回原子变量的值
 
// 原子变量加/减
void atomic_add(int i, atomic_t *v);  // 原子变量加i
void atomic_sub(int i, atomic_t *v);  // 原子变量减i
 
// 原子变量自增/自减
void atomic_inc(atomic_t *v);  // 原子变量增加1
void atomic_dec(atomic_t *v);  // 原子变量减少1
 
// 操作并测试:对原子变量进行自增、自减和减操作后(没有加)测试其是否为0,为0则返回true,否则返回false
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);
 
// 操作并返回: 对原子变量进行加/减和自增/自减操作,并返回新的值
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);
  位原子操作:
// 设置位
void set_bit(nr, void *addr);  // 设置addr地址的第nr位,即将位写1
 
// 清除位
void clear_bit(nr, void *addr);  // 清除addr地址的第nr位,即将位写0
 
// 改变位
void change_bit(nr, void *addr);  // 对addr地址的第nr位取反
 
// 测试位
test_bit(nr, void *addr); // 返回addr地址的第nr位
 
// 测试并操作:等同于执行test_bit(nr, void *addr)后再执行xxx_bit(nr, void *addr)
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);
seqlock(顺序锁)

使用seqlock锁,读执行单元不会被写执行单元阻塞,即读执行单元可以在写执行单元对被seqlock锁保护的共享资源进行写操作时仍然可以继续读,而不必等待写执行单元完成写操作,写执行单元也不需要等待所有读执行单元完成读操作才去进行写操作。写执行单元之间仍是互斥的。若读操作期间,发生了写操作,必须重新读取数据。seqlock锁必须要求被保护的共享资源不含有指针。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 获得顺序锁
void write_seqlock(seqlock_t *sl);
int write_tryseqlock(seqlock_t *sl);
write_seqlock_irqsave(lock, flags)
write_seqlock_irq(lock)
write_seqlock_bh()
 
// 释放顺序锁
void write_sequnlock(seqlock_t *sl);
write_sequnlock_irqrestore(lock, flags)
write_sequnlock_irq(lock)
write_sequnlock_bh()
 
// 写执行单元使用顺序锁的模式如下:
write_seqlock(&seqlock_a);
...  // 写操作代码块
write_sequnlock(&seqlock_a);
  读执行单元操作:
// 读开始:返回顺序锁sl当前顺序号
unsigned read_seqbegin(const seqlock_t *sl);
read_seqbegin_irqsave(lock, flags)
 
// 重读:读执行单元在访问完被顺序锁sl保护的共享资源后需要调用该函数来检查,在读访问期间是否有写操作。若有写操作,重读
int read_seqretry(const seqlock_t *sl, unsigned iv);
read_seqretry_irqrestore(lock, iv, flags)
 
// 读执行单元使用顺序锁的模式如下:
do{
    seqnum = read_seqbegin(&seqlock_a);
    // 读操作代码块 
    ...
}while(read_seqretry(&seqlock_a, seqnum));
读取-拷贝-更新(RCU)

读取-拷贝-更新(RCU) 是一个高级的互斥方法,在合适的时候可以取得非常高的效率。RCU可以看作读写锁的高性能版本,相比读写锁,RCU的优点在于既允许多个读执行单元同时访问被保护的数据,又允许多个读执行单元和多个写执行单元同时访问被保护的数据。但是RCU不能替代读写锁,因为如果写比较多时,对读执行单元的性能提高不能弥补写执行单元导致的损失。由于平时应用较少,所以不做多说。

小结

以上就是linux驱动编程中涉及的并发与竞态的内容,下面做一个简单的小结。

现在的处理器基本上都是SMP类型的,而且在新的内核版本中,基本上都支持抢占式的操作,在linux中很多程序都是可重入的,要保护这些数据,就得使用不同的锁机制。而锁机制的基本操作过程其实大同小异的,声明变量,上锁,执行临界区代码,然后再解锁。不同点在于,可以重入的限制不同,有的可以无限制重入,有的只允许异种操作重入,而有的是不允许重入操作的,有的可以在可睡眠代码中使用,有的不可以在可睡眠代码中使用。而在考虑不同的锁机制的使用时,也要考虑CPU处理的效率问题,对于不同的代码长度,不同的代码执行时间,选择一个好的锁对CPU的良好使用有很大的影响,否则将造成浪费。

之前在linux设备驱动第三篇:写一个简单的字符设备驱动中介绍了简单的字符设备驱动,下一篇将介绍一些字符设备驱动中得高级操作。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2015-04-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员互动联盟 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Linux设备驱动程序(五)——并发和竞态
并发相关的缺陷是最容易制造的,也是最难找到的,为了响应现代硬件和应用程序的需求,Linux 内核已经发展到同时处理更多事情的时代。这种变革使得内核性能及伸缩性得到了相当大的提高,然而也极大提高了内核编程的复杂性。
Gnep@97
2023/08/10
5670
Linux设备驱动程序(五)——并发和竞态
面试官让你讲讲Linux内核的竞争与并发,你该如何回答?
  在早期的 Linux内核中,并发的来源相对较少。早期内核不支持对称多处理( symmetric multi processing,SMP),因此,导致并发执行的唯一原因是对硬件中断的服务。这种情况处理起来较为简单,但并不适用于为获得更好的性能而使用更多处理器且强调快速响应事件的系统。
嵌入式与Linux那些事
2021/05/20
8000
Linux内核同步原理学习笔记
在多年前,linux还没有支持对称多处理器SMP的时候,避免并发数据访问相对简单。
杨源鑫
2019/07/04
1.3K0
Linux驱动同步与互斥
要深入理解Linux内核中的同步与互斥的实现,需要先了解一下内联汇编:在C函数中使用汇编代码。
韦东山
2021/12/08
2.4K0
Linux驱动同步与互斥
深度剖析Linux内核同步机制:实现高效可靠的并发编程
前言:非常早之前就接触过同步这个概念了,可是一直都非常模糊。没有深入地学习了解过,最近有时间了,就花时间研习了一下《linux内核标准教程》和《深入linux设备驱动程序内核机制》这两本书的相关章节。趁刚看完,就把相关的内容总结一下。
嵌入式Linux内核
2023/08/08
1.1K0
深度剖析Linux内核同步机制:实现高效可靠的并发编程
linux 内核同步机制使用
Linux 内核中的同步机制:原子操作、信号量、读写信号量、自旋锁的API、大内核锁、读写锁、大读者锁、RCU和顺序锁。 1、介绍 在现代操作系统里,同一时间可能有多个内核执行流在执行,即使单CPU内核也需要一些同步机制来同步不同执行单元对共享的数据的访问。 主流的Linux内核中的同步机制包括: 原子操作 信号量(semaphore) 读写信号量(rw_semaphore) 自旋锁spinlock 大内核锁BKL(Big Kernel Lock) 读写锁rwlock、 brlock(只包含在2.4内核中
李海彬
2018/03/22
2.4K0
Linux kernel同步机制(上篇)
在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实像多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问,尤其是在多处理器系统上,更需要一些同步机制来同步不同处理器上的执行单元对共享的数据的访问。在主流的Linux内核中包含了如下这些同步机制包括:
Linux阅码场
2020/08/25
2.6K0
Linux kernel同步机制(上篇)
一文搞懂 | Linux 同步管理(上)
因为现代操作系统是多处理器计算的架构,必然更容易遇到多个进程,多个线程访问共享数据的情况,如下图所示:
刘盼
2021/10/21
6040
一文搞懂 | Linux 同步管理(上)
吐血整理 | 肝翻 Linux 同步管理所有知识点
因为现代操作系统是多处理器计算的架构,必然更容易遇到多个进程,多个线程访问共享数据的情况,如下图所示:
刘盼
2022/01/27
8870
吐血整理 | 肝翻 Linux 同步管理所有知识点
Linux内核中的各种锁:信号量/互斥锁/读写锁/原子锁/自旋锁/内存屏障等
既然是锁CPU,那就都是针对多核处理器或多CPU处理器。单核的话,只有发生中断会使任务被抢占,那么可以进入临界区之前先关中断,但是对多核CPU光关中断就不够了,因为对当前CPU关了中断只能使得当前CPU不会运行其它要进入临界区的程序,但其它CPU还是可能执行进入临界区的程序。
Linux阅码场
2024/02/21
1.9K0
Linux内核中的各种锁:信号量/互斥锁/读写锁/原子锁/自旋锁/内存屏障等
Linux 同步管理
对于基础类型操作,使用原子变量就可以做到线程安全,那原子操作是如何保证线程安全的呢?linux中的原子变量如下:
一只小虾米
2023/03/20
1.7K0
Linux 同步管理
深入Linux并发同步
并发 是指在某一时间段内能够处理多个任务的能力,而 并行 是指同一时间能够处理多个任务的能力。并发和并行看起来很像,但实际上是有区别的,如下图(图片来源于网络):
用户7686797
2020/08/25
1.6K0
Linux内核37-内核数据的同步访问
每一种技术的出现必然是因为某种需求。正因为人的本性是贪婪的,所以科技的创新才能日新月异。
Tupelo
2022/08/15
9560
信号量、互斥锁、自旋锁、原子操作
linux内核中有多种内核锁,内核锁的作用是: 多核处理器下,会存在多个进程处于内核态的情况,而在内核态下,进程是可以访问所有内核数据的,因此要对共享数据进行保护,即互斥处理; linux内核锁机制有信号量、互斥锁、自旋锁还有原子操作。 一、信号量(struct semaphore): 是用来解决进程/线程之间的同步和互斥问题的一种通信机制,是用来保证两个或多个关键代码不被并发调用。 信号量(Saphore)由一个值和一个指针组成,指针指向等待该信号量的进程。信号量的值表示相应资源的使用情况。信号量S>=0
233333
2018/03/07
3.2K0
信号量、互斥锁、自旋锁、原子操作
自旋锁的衍生锁有哪些?
自旋锁主要用来解决SMP和调度引发的竞态问题,但是普通的自旋锁并不关心临界区在执行什么操作,对读和写都一视同仁,这样就会存在一些弊端!
董哥聊技术
2023/09/26
2930
自旋锁的衍生锁有哪些?
顺序锁(seqlock)
前面学习了spin_lock可以知道,spin_lock对于临界区是不做区分的。而读写锁是对临界区做读写区分,并且度进程进入临界区的几率比较大,因为写进程进入时需要等待读进程退出临界区。而有没有一种方法,可以保护写进程的优先权,使得写进程可以更快的获得锁? 答案是有的,就是顺序锁。
DragonKingZhu
2020/03/24
1.7K0
Linux内核33-信号量
对于信号量我们并不陌生。信号量在计算机科学中是一个很容易理解的概念。本质上,信号量就是一个简单的整数,对其进行的操作称为PV操作。进入某段临界代码段就会调用相关信号量的P操作;如果信号量的值大于0,该值会减1,进程继续执行。相反,如果信号量的值等于0,该进程就会等待,直到有其它程序释放该信号量。释放信号量的过程就称为V操作,通过增加信号量的值,唤醒正在等待的进程。
Tupelo
2022/08/15
1.7K0
一文读懂|内核顺序锁
Linux 内核有非常多的锁机制,如:自旋锁、读写锁、信号量和 RCU 锁等。本文介绍一种和读写锁比较相似的锁机制:顺序锁(seqlock)。
用户7686797
2023/09/14
3980
一文读懂|内核顺序锁
spin_lock &amp; mutex_lock的差别?
多核处理器下,会存在多个进程处于内核态的情况,而在内核态下,进程是能够訪问全部内核数据的,因此要对共享数据进行保护,即相互排斥处理
全栈程序员站长
2022/07/13
4520
10.按键之互斥、阻塞机制(详解)
本文介绍了如何通过按键驱动程序实现按键事件和防抖,并对代码和测试效果进行了详细说明。
诺谦
2018/01/03
1K0
10.按键之互斥、阻塞机制(详解)
相关推荐
Linux设备驱动程序(五)——并发和竞态
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验