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,一经查实,立即删除!

相关文章

Python爬虫入门一综述

网络爬虫是一种自动抓取万维网信息的程序。 学习python爬虫&#xff0c;需要学习以下知识&#xff1a; python基础python中的urllib和urllib2库的用法python正则表达式python爬虫框架scrapypython爬虫高级功能 1.python基础 廖雪峰python教程 2.python urllib和urllib2库使…

Python爬虫学习二爬虫基础了解

1.什么是爬虫 爬虫就是进入网页自动获取数据的程序。当它进入一个网页时&#xff0c;将网页上需要的数据下载下来&#xff0c;并跟踪网页上的其他链接&#xff0c;进入新的页面下载数据&#xff0c;并继续跟踪链接下载数据。 2.URL URL&#xff0c;即统一资源定位符&#xf…

第三章:多坐标系

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

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

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

Python爬虫入门三urllib库基本使用

urllib是一个收集了多个涉及了URL的模块的包&#xff1a; URL获取网页 urllibtest.pyimport urllib2 response urllib2.urlopen(http://www.baidu.com) print(response.read())运行结果&#xff1a; C:\Python27\python.exe H:/spiderexercise/spidertest/urllibtest.py &l…

使用老毛桃U盘重装Windows10系统

使用老毛桃U盘启动盘重装Windows10系统&#xff0c;这个方法很常用&#xff0c;相比较一些别的方法去重装系统&#xff0c;这个方法更方便更简单&#xff0c;容易入手和掌握。 在重装系统前&#xff0c;先去微软的官网把Windows10的ISO镜像文件下载下来&#xff0c;去别的网站下…

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使用proxy

使用urllib proxies {http: http://host:port} resp urllib.urlopen(url, proxiesproxies) content resp.read()使用urllib2 import urllib2enable_proxy True proxy_handler urllib2.ProxyHandler({"http" : your_proxy}) null_proxy_handler urllib2.Proxy…

There is no public key available for the following key IDs:3B4FE6ACC0B21F32

ubuntu 运行完sudo apt-get update之后&#xff0c;提示 W: There is no public key available for the following key IDs: 3B4FE6ACC0B21F32 解决方法是执行: sudo apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 3B4FE6ACC0B21F32 参考 linux下apt-get出现“no …

Unichar, char, wchar_t

之前总结了一些关于字符表示&#xff0c;以及字符串的知识。 现在在看看一些关于编译器支持的知识。 L"" Prefix 几乎所有的编译器都支持L“” prefix&#xff0c;一个字符串如果带有L“”prefix&#xff0c;意味着这个字符串中的字符都被作为wide char存储&#xf…

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

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

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

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

HandlerThread用法

区分Handler和HandlerThreadHandler实例可以在主线程创建&#xff0c;也可以在子线程创建。在子线程中创建时通过Looper&#xff0c;以下示例&#xff1a; public class MainActivity extends AppCompatActivity { HandlerThread handlerThread; Override protected v…

PHP快速排序及其时间复杂度

<?phpfunction quickSort(&$arr, $l, $r) {if (count($arr)<2 || $l>$r) return;$tmp_l $l;$tmp_r $r;$privot $arr[$r];while($tmp_l<$tmp_r) {while($arr[$tmp_l] < $privot && $tmp_l<$tmp_r) $tmp_l; //内部没有$tmp_l <$tmp_r的判断…

Python爬虫入门五URLError异常处理

本节在这里主要说的是 URLError 还有 HTTPError&#xff0c;以及对它们的一些处理 1.URLError 首先解释下 URLError 可能产生的原因&#xff1a; 网络无连接&#xff0c;即本机无法上网连接不到特定的服务器服务器不存在 在代码中&#xff0c;我们需要用 try-except 语句来…

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

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

Python爬虫入门六Cookie的使用

为什么要使用 Cookie 呢&#xff1f; Cookie&#xff0c;指某些网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据&#xff08;通常经过加密&#xff09; 比如说有些网站需要登录后才能访问某个页面&#xff0c;在登录之前&#xff0c;你想抓取某个页面内容…

有得必有失

上次也说道Ruby on Rails 是一个很不错的框架。本人也想去研究一翻&#xff0c;我已经不再替别人写程序了。 当编程变成一门兴趣的时候&#xff0c;而不是吃饭的依据。我觉得我需要跟着我的心走。 1&#xff1a;我现在的工作环境是在LINUX下&#xff0c;我一次次的折腾mono好吧…

mybatis下log4j使用

1. log4j jar包&#xff08;mvnrepository&#xff09; 2. log4j.properties文件 log4j.properties内容&#xff1a; log4j.rootLoggerDEBUG, stdout, logfile    log4j.appender.stdoutorg.apache.log4j.ConsoleAppender    log4j.appender.stdout.layoutorg.apache…