并发和竞争发生在两类体系中:

  • 对称多处理器(SMP)的多个CPU
  • 内核可抢占的单CPU系统

  访问共享资源的代码区域称为临界区critical sections,临界区需要以某种互斥机制加以保护。在驱动程序中,当多个线程同时访问相同的资源critical sections时(驱动程序中的全局变量是一种典型的共享资源),可能会引发"竞态",因此我们必须对共享资源进行并发控制。Linux内核中解决并发控制的方法又中断屏蔽、原子操作、自旋锁、信号量。(后面为主要方式)

中断屏蔽:

  使用方法

local_irq_disable()  //屏蔽中断
...
critical section //临界区
...
local_irq_enable() //开中断

  local_irq_disable/enable只能禁止/使能本CPU内的中断,不能解决SMP多CPU引发的竞态,故不推荐使用,其适宜于自旋锁联合使用。

原子操作:  

  原子操作是一系列的不能被打断的操作。linux内核提供了一系列的函数来实现内核中的原子操作,这些函数分为2类,分别针对位和整型变量进行原子操作。

实现整型原子操作的步骤如下:

1.定义原子变量并设置变量值

void atomic_set(atomic_t *v , int i); //设置原子变量值为i
atomic_t v = ATOMIC_INIT(0); //定义原子变量v,初始化为0

2.获取原子变量的值

atomic_read(atomic_t *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.操作并测试:对原子变量执行自增、自减后(没有加)测试其是否为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);

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);

实现 位原子操作如下:

// 设置位
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)

下面来举一个实例,是原子变量使用实例,使设备只能被一个进程打开:

static atomic_t xxx_available = ATOMIC_INIT(1);  // 定义原子变量

static int xxx_open(struct inode *inode, struct file *filp)
{
...
if(!atomic_dec_and_test(&xxx_available))
{
atomic_inc(&xxx_availble);
return - EBUSY; // 已经打开
}
...
return 0; // 成功
} static int xxx_release(struct inode *inode, struct file *filp)
{
atomic_inc(&xxx_available); // 释放设备
return 0;
}

 

我要着重谈一下:

自旋锁VS信号量

  从严格意义上来说,信号量和自旋锁属于不同层次的互斥手段,前者的实现依赖于后者,在多CPU中需要自旋锁来互斥。信号量是进程级的,用于多个进程之间对资源的互斥,虽然也在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的。如果竞争失败,会切换到下个进程,而当前进程进入睡眠状态,因此,当进程占用资源时间较长时,用信号量是较好的选择。

当所要保护的临界访问时间比较短时,用自旋锁是非常方便的,因为它节省了上下文切换的时间。但是CPU得不到自旋锁是,CPU会原地打转,直到其他执行单元解锁为止,所以要求锁不能在临界区里停留时间过长。

  自旋锁的操作步骤:

1.定义自旋锁
spinlock_t lock;
2.初始化自旋锁
spin_lock_init(lock);这是个宏,它用于动态初始化自旋锁lock;
3.获得自旋锁
spin_lock(lock);该宏用于加锁,如果能够立即获得锁,它就能马上返回,否则,他将自旋在那里,直到该自旋锁的保持者释放。
spin_trylock(lock);能够获得,则返回真,否则返回假,实际上是不在原地打转而已。
4.释放自旋锁
spin_unlock(lock);

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

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()

 注意:自旋锁实际上是忙等待,只有在占用锁的时间极短的情况下,使用自旋锁才是合理的自旋锁可能导致死锁:递归使用一个自旋锁或进程获得自旋锁后阻塞。

例子:

spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock); //获取自旋锁,保护临界区 。。。。临界区 spin_unlock(&lock);//释放自旋锁

  自旋锁不关心锁定的临界区究竟是如何执行的。不管是读操作还是写操作,实际上,对共享资源进行读取的时候是应该可以允许多个执行单元同时访问的,那么这样的话,自旋锁就有了弊端。于是便衍生出来一个读写锁。它保留了自旋的特性,但在对操作上面可以允许有多个单元进程同时操作。当然,读和写的时候不能同时进行。

  现在又有问题了,如果我第一个进程写共享资源,第二个进程读的话,一旦写了,那么就读不到了,可能写的东西比较多,但是第二个进程读很小,那么能不能第一个进程写的同时,我第二个进程读呢?
当然可以,那么引出了顺序锁的概念。都是一样的操作。

  读写自旋锁(rwlock)允许读的并发。在写操作方面,只能最多有一个写进程,在读操作方面,同时可以有多个读执行单元。当然,读和写也不能同时进行。

// 定义和初始化读写自旋锁
rwlock_t my_rwlock = RW_LOCK_UNLOCKED; // 静态初始化
rwlock_t my_rwlock;
rwlock)init(&my_rwlock); // 动态初始化 // 读锁定:在对共享资源进行读取之前,应先调用读锁定函数,完成之后调用读解锁函数
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); // 读解锁
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); // 写锁定:在对共享资源进行写之前,应先调用写锁定函数,完成之后调用写解锁函数
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);
int write_trylock(rwlock_t *lock); // 写解锁
void write_unlock(rwlock_t *lock);
void write_unlock_irqsave(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); // 初始化rwlock // 读时获取锁
read_lock(&lock);
... // 临界资源
read_unlock(&lock); // 写时获取锁
write_lock_irqsave(&lock, flags);
... // 临界资源
write_unlock_irqrestore(&lock, flags);

  

顺序锁(seqlock):

  顺序锁是对读写锁的一种优化,若使用顺序锁,读与写操作不阻塞,只阻塞同种操作,即读与读/写与写操作。

  写执行单元的操作顺序如下:

//获得顺序锁
void write_seqlock(seqlock_t *s1);
int write_tryseqlock(seqlock_t *s1);
write_seqlock_irqsave(lock, flags)
write_seqlock_irq(lock)
write_seqlock_bh(lock) //释放顺序锁
void write_sequnlock(seqlock_t *s1);
write_sequnlock_irqrestore(lock, flags)
write_sequnlock_irq(lock)
write_sequnlock_bh(lock)

  读执行单元的操作顺序如下:

//读开始
unsinged read_seqbegin(const seqlock_t *s1);
read_seqbegin_irqsave(lock, flags) //重读,读执行单元在访问完被顺序锁s1保护的共享资源后需要调用该函数来检查在读操作器件是否有写操作,如果有,读执行单元需要从新读一次。
int reead_seqretry(const seqlock_t *s1, unsigned iv);
read_seqretry_irqrestore(lock, iv, flags)

  

  RCU(Read-Copy Update 读-拷贝-更新)可看作读写锁的高性能版本,既允许多个读执行单元同时访问被保护的数据,又允许多个读执行单元和多个写执行单元同时访问被保护的数据。但是RCU不能替代读写锁。因为如果写操作比较多时,对读执行单元的性能提高不能弥补写执行单元导致的损失。因为使用RCU时,写执行单元之间的同步开销会比较大,它需要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其他写执行单元的修改操作。

  具体操作:略

信号量的使用

信号量(semaphore)与自旋锁相同,只有得到信号量才能执行临界区代码,但,当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。

相同点:只有得到信号量的进程才能执行临界区的代码。(linux自旋锁和信号量锁采用的都是“获得锁-访问临界区-释放锁”,可以称为“互斥三部曲”,实际存在于几乎所有多任务操作系统中)

不同点:当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。

信号量的操作:

//信号量的结构
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); //释放信号量

  信号量用于同步时只能唤醒一个执行单元,而完成量(completion)用于同步时可以唤醒所有等待的执行单元。

 自旋锁与互斥锁的选择

  • 当锁 不能被获取到时,使用信号量的开销是进程上下文切换时间Tsw,使用自旋锁的开始是等待获取自旋锁的时间Tcs,若Tcs比较小,则应使用自旋锁,否则应使用信号量
  • 信号量锁保护的临界区可以包含引起阻塞的代码,而自旋锁则却对要避免使用包含阻塞的临界区代码,否则很可能引发锁陷阱
  • 信号量存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在信号量和自旋锁之间只能选择自旋锁。当然,如果一定要使用信号量,则只能通过down_trylock()方式进行,不能获取就立即返回以避免阻塞。

  版权所有,转载请注明转载地址:http://www.cnblogs.com/lihuidashen/p/4435979.html

蜕变成蝶~Linux设备驱动中的并发控制的更多相关文章

  1. 蜕变成蝶~Linux设备驱动中的阻塞和非阻塞I/O

    今天意外收到一个消息,真是惊呆我了,博客轩给我发了信息,说是俺的博客文章有特色可以出本书,,这简直让我受宠若惊,俺只是个大三的技术宅,写的博客也是自己所学的一些见解和在网上看到我一些博文以及帖子里综合 ...

  2. 蜕变成蝶~Linux设备驱动之CPU与内存和I/O

    那是世上最远的距离 思念让我无法去呼吸 你的一动和一举 占据我心里 陪我每个孤独无尽的夜里 用我心中盛放的画笔 描绘你微笑时的绚丽 爱让人痛彻心底 我却不怀疑 你的存在是我生命的奇迹 感受你的每一次的 ...

  3. 蜕变成蝶~Linux设备驱动之异步通知和异步I/O

    在设备驱动中使用异步通知可以使得对设备的访问可进行时,由驱动主动通知应用程序进行访问.因此,使用无阻塞I/O的应用程序无需轮询设备是否可访问,而阻塞访问也可以被类似“中断”的异步通知所取代.异步通知类 ...

  4. 蜕变成蝶~Linux设备驱动之字符设备驱动

    一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据.字符设备是面向流 ...

  5. 蜕变成蝶~Linux设备驱动之中断与定时器

    “我叮咛你的 你说 不会遗忘 你告诉我的 我也全部珍藏 对于我们来说 记忆是飘不落的日子 永远不会发黄 相聚的时候 总是很短 期待的时候 总是很长 岁月的溪水边 捡拾起多少闪亮的诗行 如果你要想念我  ...

  6. 蜕变成蝶~Linux设备驱动之按键设备驱动

    在上述的驱动系列博客中,我们已经了解了关于阻塞和非阻塞.异步通知.轮询.内存和I/O口访问.并发控制等知识,按键设备驱动相对来说是比较简单的,本章内容可以加深我们对字符设备驱动架构.阻塞与非阻塞.中断 ...

  7. 蜕变成蝶~Linux设备驱动之DMA

    如果不曾相逢 也许 心绪永远不会沉重 如果真的失之交臂 恐怕一生也不得轻松 一个眼神 便足以让心海 掠过飓风 在贫瘠的土地上 更深地懂得风景 一次远行 便足以憔悴了一颗 羸弱的心 每望一眼秋水微澜 便 ...

  8. 蜕变成蝶~Linux设备驱动之watchdog设备驱动

    看门狗(watchdog )分硬件看门狗和软件看门狗.硬件看门狗是利用一个定时器 电路,其定时输出连接到电路的复位端,程序在一定时间范围内对定时器清零 (俗称 “喂狗”),如果程序出现故障,不在定时周 ...

  9. Linux设备驱动中的并发控制

    1.并发是指多个执行单元同时.并行的执行.并发的执行单元对共享资源的访问很容易导致竞态. 在 Linux 内核中,主要的竞态发生于如下几种情况: ①对称多处理器(SMP)的多个 CPU ②单CPU内进 ...

随机推荐

  1. bzoj3255 一个关于序列的游戏

    题意是啥 给你一个数列,可以任意删去一段,记其长度为$s$,得到$val_s$的价值,问你最大价值和为多少.. 其中这一段数要满足成一个上凸且相邻数差为$1$ 显然,删掉一段数后剩下的左右会相邻.. ...

  2. Flask请求request

    Flask中的request是一个公共变量,需要导入request from flask import Flask,request 接收url中的参数 @app.route("/req&qu ...

  3. 自定义simple_tag和filter在html中渲染出来的联系和区别

    关于 simple_tag: 1,在app下创建一个(templatetags)目录,(被引用的模块必须放在该目录下,且目录名称不可更改): 2,创建任意py文件: 3,创建template对象: f ...

  4. vue中引用vux

    官网看不懂,网上搜了下,以备不时之需 这是官网说明文档,看不懂的啊. Vux使用教程: 1,在项目里安装vux cnpm install vux --save 2,在项目里安装vux-loader(这 ...

  5. 阿里云ACE深圳同城会 开始报名

    大家好,阿里云 ACE深圳同城会本周末第一活动,主要大家一起聚聚,互相认识和熟悉,未来一起玩儿一起进步~ 通知大家一个好消息,当前凡是加入深圳ACE同城会组织(群)的朋友,通过跟群主报名申请,将有机会 ...

  6. 在图像上增加文字 C#

    using (Image i = Image.FromFile(inputPath)) { using (Graphics g = Graphics.FromImage(i)) { g.DrawStr ...

  7. CSS魔法堂:稍稍深入伪类选择器

    前言  过去零零星星地了解和使用:link.::after和content等伪类.伪元素选择器,最近看书时发现这方面有所欠缺,于是决定稍微深入学习一下,以下为伪类部分的整理. 伪类  伪类选择器实质上 ...

  8. .Net转Java.03.受查异常和非受查异常

    转到Java以后发现一个很妖的事情,为啥有些方法后边有个 throws XXXXException 比如下面的代码 @Override public <T> ResponseEntity& ...

  9. VuePress

    VuePress 这篇文章主要是记录自己在使用VuePress过程中所遇到的问题以及如何一步一步的解决问题. 安装vuepress前,请确保你的 Node.js 版本 >= 8 全局安装 # 安 ...

  10. 发现一个“佛系记账本”

    因为这是一款微信小程序,张小龙大力推崇的"用完即走"完美地适合记账应用. 不用下载.不用安装.不用注册.不用各种授权,只要从微信进入,就能记账,账本只与微信关联. 换手机.换PAD ...