Linux 设备驱动的并发控制

 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
在CODE上查看代码片派生到我的代码片
  1.     local_irq_disable()  
  2.     local_irq_enable()  
  3. 只能禁止和使能本地CPU的中断,所以不能解决多CPU引发的竞态  
  4.   
  5.     local_irq_save(flags)  
  6.     local_irq_restore(flags)  
  7. 除了能禁止和使能中断外,还保存和还原目前的CPU中断位信息  
  8.   
  9.     local_bh_disable()  
  10.     local_bh_disable()  
  11. 如果只是想禁止中断的底半部,这是个不错的选择。  

但是要注意:

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
在CODE上查看代码片派生到我的代码片
  1. static atomic_t v=ATOMIC_INIT(1);  
  2.   
  3. static int hello_open (struct inode *inode, struct file *filep)  
  4. {  
  5.     if(!atomic_dec_and_test(&v))  
  6.     {  
  7.         atomic_inc(&v);  
  8.         return -EBUSY;  
  9.     }  
  10.     return 0;  
  11. }  
  12.   
  13. static int hello_release (struct inode *inode, struct file *filep)  
  14. {  
  15.     atomic_inc(&v);  
  16.     return 0;  
  17. }  

3、自旋锁

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

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

1)自旋锁的使用:

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

下面是一个实例:

[cpp] view plaincopy
在CODE上查看代码片派生到我的代码片
  1. static spinlock_t lock;  
  2. static int flag = 1;  
  3.   
  4. static int hello_open (struct inode *inode, struct file *filep)  
  5. {  
  6.     spin_lock(&lock);  
  7.   
  8.     if(flag !=1)  
  9.     {  
  10.         spin_unlock(&lock);  
  11.         return -EBUSY;  
  12.     }  
  13.   
  14.     flag = 0;  
  15.     spin_unlock(&lock);  
  16.     return 0;  
  17. }  
  18.   
  19. static int hello_release (struct inode *inode, struct file *filep)  
  20. {  
  21.     flag = 1;      
  22.     return 0;  
  23. }  

       自旋锁主要针对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并初始化1
DECLARE_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
在CODE上查看代码片派生到我的代码片
  1. //定义和初始化  
  2. static  struct semaphore sem;  
  3. sema_init(&sem,1);  
  4.   
  5. static int hello_open (struct inode *inode, struct file *filep)  
  6. {  
  7.     // p操作,获得信号量,保护临界区  
  8.     if(down_interruptible(&sem))  
  9.     {  
  10.     //没有获得信号量  
  11.         return -ERESTART;  
  12.     }  
  13.     return 0;  
  14. }  
  15.   
  16. static int hello_release (struct inode *inode, struct file *filep)  
  17. {  
  18.     //v操作,释放信号量  
  19.     up(&sem);  
  20.   
  21.     return 0;  
  22. }  

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

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

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


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/402209.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第三章:多坐标系

第一节&#xff1a;为什么要有多坐标系 当我们使用一个坐标系来描绘整个场景的时候&#xff0c;场景中的任意点都可以用该坐标系描述&#xff0c;此时如果有一只羊一遍摇动着耳朵&#xff0c;一边走&#xff0c;这个时候如果进行坐标的转换会发现异常的麻烦&#xff0c;此时如果…

Linux 设备驱动开发 —— 设备树在platform设备驱动中的使用

关与设备树的概念&#xff0c;我们在Exynos4412 内核移植&#xff08;六&#xff09;—— 设备树解析 里面已经学习过&#xff0c;下面看一下设备树在设备驱动开发中起到的作用 Device Tree是一种描述硬件的数据结构&#xff0c;设备树源(Device Tree Source)文件&#xff08;以…

Android 网络通信框架Volley简介(Google IO 2013)

1. 什么是Volley 在这之前&#xff0c;我们在程序中需要和网络通信的时候&#xff0c;大体使用的东西莫过于AsyncTaskLoader&#xff0c;HttpURLConnection&#xff0c;AsyncTask&#xff0c;HTTPClient&#xff08;Apache&#xff09;等&#xff0c;今年的Google I/O 2013上&…

Linux 设备驱动开发 —— platform设备驱动应用实例解析

前面我们已经学习了platform设备的理论知识Linux 设备驱动开发 —— platform 设备驱动 &#xff0c;下面将通过一个实例来深入我们的学习。 一、platform 驱动的工作过程 platform模型驱动编程&#xff0c;需要实现platform_device(设备)与platform_driver&#xff08;驱动&am…

Python爬虫入门四urllib库的高级用法

1.设置headers 有些网站不会同意程序直接用上面的方式进行访问&#xff0c;如果识别有问题&#xff0c;那么站点根本不会响应&#xff0c;所以为了完全模拟浏览器的工作&#xff0c;我们需要设置一些 Headers 的属性。 首先&#xff0c;打开我们的浏览器&#xff0c;调试浏览器…

进程上下文、中断上下文及原子上下文

谈论进程上下文 、中断上下文 、 原子上下文之前&#xff0c;有必要讨论下两个概念&#xff1a; a -- 上下文 上下文是从英文context翻译过来&#xff0c;指的是一种环境。相对于进程而言&#xff0c;就是进程执行时的环境&#xff1b; 具体来说就是各个变量和数据&#xff0c;…

Linux 文件系统与设备文件系统 (二)—— sysfs 文件系统与Linux设备模型

提到 sysfs 文件系统 &#xff0c;必须先需要了解的是Linux设备模型&#xff0c;什么是Linux设备模型呢&#xff1f; 一、Linux 设备模型 1、设备模型概述 从2.6版本开始&#xff0c;Linux开发团队便为内核建立起一个统一的设备模型。在以前的内核中没有独立的数据结构用来让内…

Python爬虫入门七正则表达式

已经搞定了怎样获取页面的内容&#xff0c;不过还差一步&#xff0c;这么多杂乱的代码夹杂文字我们怎样把它提取出来整理呢&#xff1f;下面就开始介绍一个十分强大的工具&#xff0c;正则表达式 1.了解正则表达式 正则表达式是用来匹配字符串非常强大的工具&#xff0c;在其…

Linux 文件系统与设备文件系统 (一)—— udev 设备文件系统

一、什么是Linux设备文件系统 首先我们不看定义&#xff0c;定义总是太抽象很难理解&#xff0c;我们先看现象。当我们往开发板上移植了一个新的文件系统之后&#xff08;假如各种设备驱动也移植好了&#xff09;&#xff0c;启动开发板&#xff0c;我们用串口工具进入开发板&a…

情人节,教大家使用css画出一朵玫瑰花。

情人节到了&#xff0c;给大家来一朵高端的玫瑰花。 在网上看到的一个canvas实现的玫瑰花&#xff0c;效果很好&#xff0c;但是代码被压缩过&#xff0c;也没有注释&#xff0c;看的云里雾里的。 今天我教大脚用CSS来实现一朵玫瑰花。 先看效果 首先我们画出一个花瓣 1、画出一…

Linux 字符设备驱动开发基础(六)—— VFS 虚拟文件系统解析

一、VFS 虚拟文件系统基础概念 Linux 允许众多不同的文件系统共存&#xff0c;并支持跨文件系统的文件操作&#xff0c;这是因为有虚拟文件系统的存在。虚拟文件系统&#xff0c;即VFS&#xff08;Virtual File System&#xff09;是 Linux 内核中的一个软件抽象层。它通过一些…

vim使用—实现程序的自动补齐(C语言)

使用过Source Insight的人一定对它的自动补全功能印象深刻&#xff0c;在很多的集成开发环境中&#xff0c;也都支持自动补全。vim做为一个出色的编辑器&#xff0c;这样的功能当然少不了。至于如何实现程序自动补全&#xff0c;网上教程很多。这里&#xff0c;我将自己配置过程…

[C#]Attribute特性(3)——AttributeUsage特性和特性标识符

相关文章 [C#]Attribute特性 [C#]Attribute特性(2)——方法的特性及特性参数 AttributeUsage特性 除了可以定制自己的特性来注释常用的C#类型外&#xff0c;您可以用AttributeUsage特性来定义您想怎样使用这些特性。AttributeUsage特性采用如下的调用惯例&#xff1a; 1 [Attri…

Linux 命令 ——less命令

less 工具也是对文件或其它输出进行分页显示的工具&#xff0c;应该说是linux正统查看文件内容的工具&#xff0c;功能极其强大。less 的用法比起 more 更加的有弹性。在 more 的时候&#xff0c;我们并没有办法向前面翻&#xff0c; 只能往后面看&#xff0c;但若使用了 less …

android闹钟实现原理

闹钟的原理可用下面我自己画的一幅图来概括&#xff1a;&#xff08;不对的地方&#xff0c;尽管吐槽&#xff09; 我们来看看新建闹钟到闹钟响铃的步骤&#xff1a; 1、新建一个闹钟&#xff1a; ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22…

将openstack的Token认证信息存储在memcache中

公司线上的openstack环境运行了5个多月的时间&#xff0c;keystone库的token表已经增长到了31GB&#xff0c;这个数据量还是很大的&#xff0c;对于以后的数据库备份很不方便。每次管理openstack的时候&#xff0c;都会产生一个新的token验证&#xff0c;而历史token信息其实都…

Linux 下shell编程

什么是shell?Shell是一个命令解析器&#xff0c;是介于Linux操作系统的内核(kernel)与用户之间的一个绝缘层。shell脚本就是讲各类命令预先放入其中&#xff0c;方便一次性执行的一个程序文件&#xff0c;主要用于方便管理员进行设置或者管理。 序员的角度来看&#xff0c; Sh…

linux 目录/sys 解析

今天学习Linux目录时&#xff0c;遇到/sys这个目录&#xff0c;老师怎么讲的&#xff0c;不太清楚&#xff0c;先对/sys目录知识进行一个整理 首先&#xff0c;对 /sys目录下的各个子目录进行具体说明&#xff1a; /sys下的子目录 内容 /sys/devices 该目录下…

南下事业篇——深圳 深圳(回顾)

2019独角兽企业重金招聘Python工程师标准>>> 二0一二年三月二十三号记录了下面的一篇日志&#xff0c;现在回味一下觉得自己有点惭愧&#xff0c;但不后悔&#xff0c;知道的越多就越了解自己的无知&#xff0c;工作之后渐渐磨灭了许多锐气&#xff0c;变得平滑低调…

php中花括号的使用

一、界定变量名 注&#xff1a;花括号内若左侧出现空格&#xff0c;则会当做普通花括号来解析。 二、界定表达式 1.获取字符串中某个字符 如&#xff1a;$strabcdefg; echo $str{0};//a 效果等同于$str[0]; 2.作为表示下标的方法定义数组 如&#xff1a;$arr []; $arr{10}4;…