驱动中的并发控制相关概念

转载请注明原文地址:http://blog.csdn.net/ts_dchs/article/details/49851255

驱动中的并发控制相关概念

1 并发概念

并发concurrency,多个单元对共享资源访问导致竞态race conditions。
竞态发生的集中情况:

  1. SMP(对称多处理器)的多个CPU
    SMP多个CPU共同使用总线。所以可以同时访问共同的外设和存储设备。
  2. 单CPU调度,进程抢占
    linux 2.6支持抢占调度。可能被高优先级打断。
  3. 中断(硬中断,软中断,Tasklet,底半部)打断进程访问共同资源。

解决方法:资源的互斥访问。共享资源的代码区域成为critical sections,临界区。
Methods:中断屏蔽,原子操作,自旋锁,信号量。

补充知识

中断机制:中断(IRQ, Interrupt Request)是一种电信号,事件发生将电信号发给中断控制器,如果总线激活,中断控制器再发送电信号给处理器特定引脚,让处理器暂时停止当前工作处理中断。如果对应的中断有handler就处理event,处理完后恢复原来状态。
硬中断:由外设产生的中断。通知OS外设变化,来进行相应。在Linux下可以嵌套硬中断。
硬中断处理程序要确保它能快速地完成任务,这样程序执行时才不会等待较长时间,称为顶半部
软中断:softirq, 中断需要尽快完成,一般让硬中断处理短时间可以完成的工作,软中断处理较长的工作,由指令产生,无需中断控制器。硬中断可以被屏蔽,软中断不可以。Linux软中断不能嵌套,但可以多核执行。
软中断处理硬中断未完成的工作,是一种推后执行的机制,属于底半部
顶半部|底半部是对于中断来说的,理论上中断应该消耗时间很短,但实际上中断往往要处理很多事情,所以将中断分为上下半部分来处理:顶半部用来简单的处理中断,比如消中断,登记中断(也就是注册底半部),执行完顶半部后内核会在适当的时机执行耗时较长的底半部。

2 中断屏蔽

Linux的调度依赖中断实现,所以也能避免抢占发生。

(1) 硬中断的开关
简单禁止和激活当前处理器上的本地中断:

local_irq_disable();
local_irq_enable();

//保存本地中断系统状态下的禁止和激活:
unsigned long flags;
local_irq_save(flags);
local_irq_restore(flags);

(2) 软中断的开关
禁止底半部,如softirq、tasklet和workqueue等:

local_bh_disable();
local_bh_enable();

需要注意的是,禁止顶底部时仍然可以被硬中断抢占。

(3) 判断中断状态

    #define in_interrupt() (irq_count()) // 是否处于中断状态(硬中断或软中断)
    #define in_irq() (hardirq_count()) // 是否处于硬中断
    #define in_softirq() (softirq_count()) // 是否处于软中断

但是过长时间的中断终止是有危险的。而且操作只能禁止本CPU,不能解决SMP多CPU问题。所以一般和自旋锁联合使用。

3 原子操作

在执行过程中不会被别的代码路径所中断的操作。
分两类:针对bit, 针对int
用CPU的原子操作完成,所以和CPU架构密切相关。

如一个原子sub和dec指令实际上是由多条指令组合成的,用原子指令会一次完成。

4 自旋锁

即spin lock,用CPU的机制利用原子操作设定某个内存变量,利用锁的获取和释放来确保资源互斥。自旋就是比较快的一直在测试-设定,不断检查spin lock的释放情况,直到被释放持有lock。

spin lock对于SMP或单CPU+内核抢占有效,如果是单CPU,不支持抢占,spin lock退化为空。

由于在使用spin lock后仍然可能受到中断底半部的影响,所以结合中断开关机制,形成整套自旋锁机制,如:

spin_lock_irq() = spin_lock() + local_irq_disable() //获得自旋锁并关闭硬中断
spin_unlock_irq() = spin_unlock() + local_irq_enable()  //释放自旋锁并打开硬中断
//相应的对于local_irq_save, local_bh_disable也有两组

这将为自旋锁的使用时避免中断出现。

注意

  1. spin lock是忙等锁,不是用语临界区大的情况,共享设备。会降低性能。
  2. 获得锁的进程二次递归做spin_lock()会导致锁死。
  3. lock期间不能调用阻塞函数,可能会导致内核崩溃。
读写自旋锁

因为spin lock不关心进行什么操作。有的时候多个读同时进行是不影响的。所以衍生出rwlock来允许读并发。

顺序锁

seqlock,是对读写锁的一种优化。执行读不会被执行写所阻塞。执行写不需要等待所有读都执行完毕。如果读期间发生写,需要重读。提升了并发效率。

RCU

Read-Copy Update, 在Linux 2.6中被正式包含。读单元不需要锁,因而不会导致竞争。数据的写入是写单元先拷贝副本,对副本修改,再用回调在适当时机把原来的指针重定向。这个时机是所有引用该数据的CPU都推出对共享数据操作的时候。
所以读没有同步开销,写开销取决于写的同步机制。
所以RCU的read_lock只是禁止内核抢占调度,而不是资源互斥。
写单元调用synchronize_rcu(),阻塞写执行单元。等待所有读完成读临界(grace period),写单元再继续。

5 信号量

semephore,与spin lock不同的是,获得是信号量失败不会在原地spin。而是进入休眠等待来减少不必要的check。

注意由于down()获取信号量会导致睡眠,所以不能在中断上下文中使用。down_interruptible()在睡眠时可以被信号打断,可以导致该函数return。down_trylock()相当于非阻塞版。

之所以不用一直check是因为释放sem的时候,up()函数会主动激活正在等待的down()

注:mutex和sem的使用是一致的。mutex定义的是互斥体,相当于sem个数为1.

完成量?

completion, 用于一个执行单元等待另一个执行单元完成某件事。相当于提取了semaphore的激活部分,专门设定一个机制用于等待激活和激活。

6 spin lock VS. semaphore

两者的选择依据临界区的性质和系统的特点。
信号量和自旋锁其实是不同层次的,信号量的实现依赖于自旋锁。信号量属于进程级别,多进程资源互斥。由于上下文切换的开销较大,所以在进程占用资源时间较长的时候应该利用信号量,减少因为任务短而多次上下文切换。
自旋锁适用于critical section需要时间较短的情况。不需要切换,节省上下文切换的时间。但CPU会空转,知道其他单元解锁,所以要求所不能在临界区长时间停留。否则CPU一直空转会降低效率。

另外,spin lock要绝对避免 critical section中有阻塞。阻塞会导致上下文切换,如果切换到的进程也要获取锁,就会死锁。
如果要保护共享资源要在中断,或者软中断下使用,则应该选择自旋锁。

7 semaphore在项目中的使用

在自己封装的结构体中加入信号量struct semaphore sem;用于对设备的并发控制。

  • 在init函数中进行semaphore的初始化。
  • copy_to_usercopy_from_user前后加入信号量的获取与释放。down_interruptible(&dev->sem)up(&dev->sem)
  • 在对于共享内存区域控制的前后加入信号量获取与释放。

reference

[1] http://blog.csdn.net/zhangskd/article/details/21992933#

notification

source: 《Linux设备驱动开发详解》(第二版),内容为读书笔记和网络资料,有些资料原始来源不详,分享为了方便自己和他人查阅。如有侵权请及时告知,对于带来的不便非常抱歉。转载请注明来源。Terrence Zhou.
转载请注明原文地址:http://blog.csdn.net/ts_dchs/article/details/49851255

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值