Linux 设备驱动中必须要解决的一个问题是多个进程对共享的资源的并发访问,并发的访问会导致竞态,即使是经验丰富的驱动工程师也常常设计出包含并发问题bug 的驱动程序。

一、基础概念

1、Linux 并发相关基础概念

a -- 并发(concurrency):并发指的是多个执行单元同时、并发被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(race condition);

b -- 竞态(race condition) :竞态简单的说就是两个或两个以上的进程同时访问一个资源,同时引起资源的错误;

c -- 临界区(Critical Section):每个进程中访问临界资源的那段代码称为临界区

d -- 临界资源 :一次仅允许一个进程使用的资源称为临界资源;多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用;

在宏观上并行或者真正意义上的并行(这里为什么是宏观意义的并行呢?我们应该知道“时间片”这个概念,微观上还是串行的,所以这里称为宏观上的并行),可能会导致竞争; 类似两条十字交叉的道路上运行的车。当他们同一时刻要经过共同的资源(交叉点)的时候,如果没有交通信号灯,就可能出现混乱。在linux 系统中也有可能存在这种情况:

2、并发产生的场合

a -- 对称多处理器(SMP)的多个CPU

       SMP 是一种共享存储的系统模型,它的特点是多个CPU使用共同的系统总线,因此可访问共同的外设和储存器,这里可以实现真正的并行

b -- 单CPU内进程与抢占它的进程

一个进程在内核执行的时候有可能被另一个高优先级进程打断;

c -- 中断和进程之间

中断可以打断正在执行的进程,如果中断处理函数程序访问进程正在访问的资源,则竞态也会发生;

3、解决竞态问题的途径

解决竞态问题的途径最重要的是保证对共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。

Linux 设备中提供了可采用的互斥途径来避免这种竞争。主要有原子操作信号量自旋锁

那么这三种有什么相同的地方,有什么区别呢?适用什么不同的场合呢?会带来什么边际效应?要彻底弄清楚这些问题,要从其所处的环境来进行细化分类处理。是UP(单CPU)还是SMP(多CPU);是抢占式内核还是非抢占式内核;是在中断上下文不是进程上下文。似交通信号灯一样的措施来避免这种竞争。

先看一下三种并发机制的简单概念:

 原子锁:原子操作不可能被其他的任务给调开,一切(包括中断),针对单个变量。

 自旋锁:使用忙等待锁来确保互斥锁的一种特别方法,针对是临界区。

信号量:包括一个变量及对它进行的两个原语操作,此变量就称之为信号量,针对是临界区。

二、并发处理途径详解

1、中断屏蔽

在单CPU范围内避免静态的一种简单而省事的方法是在进入临界区之前屏蔽系统的中断,这项功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,防止某些竞争条件的发生。具体而言

a -- 中断屏蔽将使得中断和进程之间的并发不再发生

b -- 由于Linux内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也得以避免

中断屏蔽的使用方法:

[cpp] view plaincopy
  1. local_irq_disable()
  2. local_irq_enable()
  3. 只能禁止和使能本地CPU的中断,所以不能解决多CPU引发的竞态
  4. local_irq_save(flags)
  5. local_irq_restore(flags)
  6. 除了能禁止和使能中断外,还保存和还原目前的CPU中断位信息
  7. local_bh_disable()
  8. local_bh_disable()
  9. 如果只是想禁止中断的底半部,这是个不错的选择。

但是要注意:

a -- 中断对系统正常运行很重要,长时间屏蔽很危险,有可能造成数据丢失乃至系统崩溃,所以中断屏蔽后应尽可能快的执行完毕。

b -- 宜与自旋锁联合使用。

所以,不建议使用中断屏蔽

2、原子操作

 原子操作(分为原子整型操作和原子位操作)就是绝不会在执行完毕前被任何其他任务和时间打断,不会执行一半,又去执行其他代码原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都在include/asm/atomic.h中,使用汇编语言实现。

  在linux中,原子变量的定义如下:

    typedef struct {volatile int counter;} atomic_t;

    关键字volatile用来暗示GCC不要对该类型做数据优化,所以对这个变量counte的访问都是基于内存的,不要将其缓冲到寄存器中。存储到寄存器中,可能导致内存中的数据已经改变,而寄存其中的数据没有改变。

原子整型操作:

1)定义atomic_t变量:

#define ATOMIC_INIT(i) ( (atomic_t) { (i) } )

atomic_t v = ATOMIC_INIT(0);    //定义原子变量v并初始化为0

2)设置原子变量的值:

#define atomic_set(v,i) ((v)->counter = (i))
void atomic_set(atomic_t *v, int i);//设置原子变量的值为i 

3)获取原子变量的值:

#define atomic_read(v) ((v)->counter + 0)
atomic_read(atomic_t *v);//返回原子变量的值

4)原子变量加/减:

static __inline__ void atomic_add(int i, atomic_t * v); //原子变量增加i static __inline__ void atomic_sub(int i, atomic_t * v); //原子变量减少i

5)原子变量自增/自减:

#define atomic_inc(v) atomic_add(1, v); //原子变量加1
#define atomic_dec(v) atomic_sub(1, v); //原子变量减1

6)操作并测试:

//这些操作对原子变量执行自增,自减,减操作后测试是否为0,是返回true,否则返回false #define atomic_inc_and_test(v) (atomic_add_return(1, (v)) == 0)
static inline int atomic_add_return(int i, atomic_t *v)

原子操作的优点编写简单;缺点是功能太简单,只能做计数操作,保护的东西太少。下面看一个实例:

[cpp] view plaincopy
  1. static atomic_t v=ATOMIC_INIT(1);
  2. static int hello_open (struct inode *inode, struct file *filep)
  3. {
  4. if(!atomic_dec_and_test(&v))
  5. {
  6. atomic_inc(&v);
  7. return -EBUSY;
  8. }
  9. return 0;
  10. }
  11. static int hello_release (struct inode *inode, struct file *filep)
  12. {
  13. atomic_inc(&v);
  14. return 0;
  15. }

3、自旋锁

  自旋锁是专为防止多处理器并发而引入的一种锁,它应用于中断处理等部分。对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁。

  自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环——旋转——等待锁重新可用(忙等待,即当一个进程位于其临界区内,任何试图进入其临界区的进程都必须在进入代码连续循环)。要是锁未被争用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。

1)自旋锁的使用:

spinlock_t spin; //定义自旋锁spin_lock_init(lock); //初始化自旋锁spin_lock(lock); //成功获得自旋锁立即返回,否则自旋在那里直到该自旋锁的保持者释放      spin_trylock(lock); //成功获得自旋锁立即返回真,否则返回假,而不是像上一个那样"在原地打转"spin_unlock(lock);//释放自旋锁

下面是一个实例:

[cpp] view plaincopy
  1. static spinlock_t lock;
  2. static int flag = 1;
  3. static int hello_open (struct inode *inode, struct file *filep)
  4. {
  5. spin_lock(&lock);
  6. if(flag !=1)
  7. {
  8. spin_unlock(&lock);
  9. return -EBUSY;
  10. }
  11. flag = 0;
  12. spin_unlock(&lock);
  13. return 0;
  14. }
  15. static int hello_release (struct inode *inode, struct file *filep)
  16. {
  17. flag = 1;
  18. return 0;
  19. }

       自旋锁主要针对SMP或单CPU但内核可抢占的情况,对于单CPU和内核不支持的抢占的系统,自旋锁退化为空操作(因为自旋锁本身就需进行内核抢占)在单CPU和内核可抢占的系统中,自旋锁持有期间内核的抢占将被禁止。由于内核可抢占的单CPU系统的行为实际很类似于SMP系统,因此,在这样的单CPU系统中使用自旋锁仍十分重要。

尽管用了自旋锁可以保证临界区不受别的CPU和本CPU内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候,还可能受到中断和底半部的影响。为了防止这种影响。为了防止影响,就需要用到自旋锁的衍生。


2)注意事项

a -- 自旋锁是一种忙等待。它是一种适合短时间锁定的轻量级的加锁机制。

b -- 自旋锁不能递归使用。自旋锁被设计成在不同线程或者函数之间同步。这是因为,如果一个线程在已经持有自旋锁时,其处于忙等待状态,则已经没有机会释放自己持有的锁了。如果这时再调用自身,则自旋锁永远没有执行的机会了,即造成“死锁”。

【自旋锁导致死锁的实例】

1)a进程拥有自旋锁,在内核态阻塞的,内核调度进程b,b也要或得自旋锁,b只能自旋,而此时抢占已经关闭了,a进程就不会调度到了,b进程永远自旋。

2)进程a拥有自旋锁,中断来了,cpu执行中断,中断处理函数也要获得锁访问共享资源,此时也获得不到锁,只能死锁。

3)内核抢占

内核抢占是上面提到的一个概念,不管当前进程处于内核态还是用户态,都会调度优先级高的进程运行,停止当前进程;当我们使用自旋锁的时候,抢占是关闭的。

4)自旋锁有几个重要的特性:

a -- 被自旋锁保护的临界区代码执行时不能进入休眠。

b -- 被自旋锁保护的临界区代码执行时是不能被被其他中断中断。

c -- 被自旋锁保护的临界区代码执行时,内核不能被抢占。

从这几个特性可以归纳出一个共性:被自旋锁保护的临界区代码执行时,它不能因为任何原因放弃处理器。

4、信号量

  linux中,提供了两种信号量:一种用于内核程序中,一种用于应用程序中。这里只讲属前者

  信号量和自旋锁的使用方法基本一样。与自旋锁相比,信号量只有当得到信号量的进程或者线程时才能够进入临界区,执行临界代码。信号量和自旋锁的最大区别在于:当一个进程试图去获得一个已经锁定的信号量时,进程不会像自旋锁一样在远处忙等待。

  信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。

1)信号量的实现:

  在linux中,信号量的定义如下:

struct semaphore {spinlock_t        lock;      //用来对count变量起保护作用。    unsigned int        count;     //    大于0,资源空闲;等于0,资源忙,但没有进程等待这个保护的资源;小于0,资源不可用,并至少有一个进程等待资源。    struct list_head    wait_list; //存放等待队列链表的地址,当前等待资源的所有睡眠进程都会放在这个链表中。};

2)信号量的使用:

static inline void sema_init(struct semaphore *sem, int val); //设置sem为val
#define init_MUTEX(sem) sema_init(sem, 1) //初始化一个用户互斥的信号量sem设置为1
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0) //初始化一个用户互斥的信号量sem设置为0

定义和初始化可以一步完成:

DECLARE_MUTEX(name); //该宏定义信号量name并初始化1DECLARE_MUTEX_LOCKED(name); //该宏定义信号量name并初始化0

  当信号量用于互斥时(即避免多个进程同是在一个临界区运行),信号量的值应初始化为1。这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一个信号量有时也称为一个“互斥体(mutex)”,它是互斥(mutual exclusion)的简称。Linux内核中几乎所有的信号量均用于互斥。

  使用信号量,内核代码必须包含<asm/semaphore.h> 。

3)获取(锁定)信号量:

void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_killable(struct semaphore *sem);

4)释放信号量

void up(struct semaphore *sem);

下面看一个实例:

[cpp] view plaincopy
  1. //定义和初始化
  2. static  struct semaphore sem;
  3. sema_init(&sem,1);
  4. static int hello_open (struct inode *inode, struct file *filep)
  5. {
  6. // p操作,获得信号量,保护临界区
  7. if(down_interruptible(&sem))
  8. {
  9. //没有获得信号量
  10. return -ERESTART;
  11. }
  12. return 0;
  13. }
  14. static int hello_release (struct inode *inode, struct file *filep)
  15. {
  16. //v操作,释放信号量
  17. up(&sem);
  18. return 0;
  19. }

三、自旋锁与信号量的比较

  信号量 自旋锁
1、开销成本 进程上下文切换时间 忙等待获得自旋锁时间
2、特性 a -- 导致阻塞,产生睡眠
b -- 进程级的(内核是代表进程来争夺资源的)
a -- 忙等待,内核抢占关闭
b -- 主要是用于CPU同步的
3、应用场合 只能运行于进程上下文 还可以出现中断上下文
4、其他 还可以出现在用户进程中 只能在内核线程中使用

从以上的区别以及本身的定义可以推导出两都分别适应的场合。只考虑内核态

后记:除了上述几种广泛使用的的并发控制机制外,还有中断屏蔽、顺序锁(seqlock)、RCU(Read-Copy-Update)等等,做个简单总结如下图:

Linux 设备驱动的并发控制相关推荐

  1. linux 两个驱动 竞态,第7章 Linux设备驱动中的并发控制之一(并发与竞态)

    本章导读 Linux设备驱动中必须解决的一个问题是多个进程对共享资源的并发访问,并发的访问会导致竞态(竞争状态). Linux提供了多种解决竞态问题的方式,这些方式适合不同的应用场景. 7.1讲解了并 ...

  2. linux 驱动器发送信号,Linux设备驱动并发控制详解(自旋锁,信号量)

    转发:Linux设备驱动并发控制详解(自旋锁,信号量) 作者:jinhaijun 提交日期:2008-3-12 14:08:00 | 分类: | 访问量:144 link:http://www.emb ...

  3. Linux设备驱动中的并发控制总结

    并发(concurrency)指的是多个执行单元同时.并行被执行.而并发的执行单元对共享资源(硬件资源和软件上的全局.静态变量)的访问则容易导致竞态(race conditions).   SMP是一 ...

  4. Linux设备驱动开发详解:第7章 Linux设备驱动中的并发控制

    7.1并发与竞态 (1).竞态的发生场景:CPU0的进程与CPU1的进程之间.CPU0的中断与CPU1的进程之间.CPU0的中断与CPU1的中断之间: (2).解决竞态问题的途径是保证对共享资源的互斥 ...

  5. Linux设备驱动开发概述

    作者:宋宝华 email:author@linuxdriver.cn 在过去这些年,Linux已经成功应用于服务器和桌面系统,而近年来,随着嵌入式系统应用的持续升温,Linux也开始广泛应用于嵌入式领 ...

  6. linux设备驱动开发详解源码,linux设备驱动开发详解光盘源码.rar

    压缩包 : linux设备驱动开发详解光盘源码.rar 列表 19/busybox源代码/busybox-1.2.1.tar.bz2 19/MTD工具/mtd-utils-1.0.0.tar.gz 1 ...

  7. Linux设备驱动与整个软硬件系统的关系

    Table of Contents 编写Linux设备驱动的技术基础 编写Linux设备驱动的技术基础 编写Linux 设备驱动要求工程师具有良好的硬件基础,懂得SRAM.Flash.SDRAM.磁盘 ...

  8. 《Linux 设备驱动开发详解(第2版)》——1.4 Linux设备驱动

    本节书摘来自异步社区<Linux 设备驱动开发详解(第2版)>一书中的第1章,第1.1节,作者:宋宝华著,更多章节内容可以访问云栖社区"异步社区"公众号查看 1.4 L ...

  9. Linux设备驱动开发基础

    1.驱动概述和开发环境搭建 1.1驱动设备的作用 对设备驱动最通俗的解释就是"驱动硬件设备行动".驱动与底层硬件直接打交道,按照硬件设备的具体工作方式,读写设备的寄存器,完成设备的 ...

最新文章

  1. vmware esxi 查看网卡、Raid卡驱动
  2. 详解JavaScript中void语句的使用
  3. 前端学习(485):css
  4. C++中用frugally-deep调用keras的模型并进行预测
  5. 【网络信息安全】网络安全基础
  6. vsoce-video
  7. MNIST数据集格式ubyte转png
  8. u盘 计算机管理 没有就绪,U盘无法识别先别着急扔!这五步操作还能挽救一下...
  9. JavaEE | 集合之HashMap与ConcurrentHashMap(看完多线程后补充)
  10. 常用工具:用yEd工具画流程图实用步骤
  11. 智能世界的罗马是怎样建成的?
  12. 数据特征分析 - 帕累托分析法
  13. 2023年软考时间流程安排:
  14. python函数返回值可以有多个吗_Python函数中如何返回多个值?(代码示例)
  15. 10分钟搞懂蚁群算法
  16. IOS 将文字写绘制成图片并转换为像素数据
  17. 工业互联网企业蘑菇物联获数千万元A轮融资,元禾原点资本领投...
  18. 杭州一公司开20万月薪抢AIGC算法工程师;SpaceX「星舰」发射任务失败;华为宣布实现ERP自主可控,突破封锁|极客头条
  19. 支持OneNote for Window10代码高亮工具
  20. css·HTML知识梳理

热门文章

  1. JavaWeb重要知识点总结
  2. 交换机应用之端口模式(access、trunk和hybird)、是否标记(tag、untag)、端口缺省vlan(pvid、native id)...
  3. 单线程写多线程读安全的结构体
  4. WinForm UI设计与开发思路(转)
  5. 机器学习实践五---支持向量机(SVM)
  6. 怎么样用System.out.println在控制台打印出颜色
  7. 重学TCP协议(7) Timestamps 选项
  8. leetcode 127. 单词接龙(bfs)
  9. chrome麦克风权限_如何在Chrome扩展程序中处理麦克风输入权限和语音识别
  10. freecodecamp_freeCodeCamp.org隐私权政策:问题与解答