Linux设备驱动中的并发控制
有两种可能的原因会造成程序出错,一种可能性是编译乱序,另一种可能性是执行乱序。
处理器为了解决多核间一个核的内存行为对另一个核可见的问题,引入了一些内存屏蔽的指令。ARM处理器的屏蔽指令包括:
DMB(数据内存屏障):在DMB之后的显式内存访问执行前,保证所有在DMB指令之前的内存访问完成;
DSB(数据同步屏障):等待所有在DSB指令之前的指令完成(位于此指令前的所有显式内存访问均完成,位于此指令前的所有缓存、跳转预测和TLB维护操作全部完成);
ISB(指令同步屏障):Flush流水线,使得所有ISB之后执行的指令都是从缓存或者内存中获得的。
Linux内核提供了一系列函数来实现内核中的原子操作,分为两类:整型原子操作和位原子操作。
使用原子变量使设备只能被一个进程打开。
例:
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_available);
return–EBUSY;
}
…
return 0;
}
static int xxx_release(struct inode *inode, struct file*filp)
{
atomic_inc(&xxx_available);
return 0;
}
自旋锁(Spin Lock的衍生、读写自旋锁、顺序锁)与RCU(Read-Copy-Update,读-复制-更新)
自旋锁,既保证排他性,也能处理好内存屏障。主要针对SMP或单CPU但内核可抢占的情况,对于单CPU和内核不支持抢占的系统,自旋锁退化为空操作。
在得到锁的代码路径在执行临界区的时候,还可能受到中断和底半部(BH)的影响。为了防止这种影响,就衍生除了一套自旋锁机制。
使用自旋锁需要注意的问题:
(1) 只有在占用锁的时间极短的情况下,使用自旋锁才是合理的。当临界区很大,或有共享设备的时候,需要较长时间占用锁,使用自旋锁会降低系统的性能。
(2) 自旋锁可能导致死锁。最常见的情况是递归使用一个自旋锁。
(3) 在自旋锁锁定期间不能调用可能引起进程调度的函数。如果进程获得自旋锁之后再阻塞,如调用copy_from_user()、kmalloc()、msleep()等函数,则可能导致内核的崩溃。
(4) 在强调跨平台的概念时,CPU视为多核,spin_lock_irpsave()不能屏蔽另一个核的中断,所以另一个核就可能造成并发问题。因此,无论如何,我们在中断服务程序里也应该调用spin_lock()。
RCU的机制跟自旋锁相反。读自旋锁是等所有进程或中断释放自旋锁后,再拿到自旋锁后修改数据。RCU是直接制造一个新的节点M,把N的内容复制给M,之后再修改M上的数据,并用M代替N原本在链表的位置。之后进程等待在链表前期已经存在的所有读端结束后(即宽限期,通过synchronize_rcu()API完成),在释放原来的N。
但是RCU不能代替读写锁,因为RCU写执行单元之间的同步开销比较大,它也必须使用某种锁机制来同步并发的其他写执行单元的修改操作。
函数call_rcu()也由RCU写执行单元调用,与synchronize_rcu()不同的是,它不会使写执行单元阻塞,因而可以在中断上下文或软中断使用。
信号量(semaphore)是操作系统中最典型的用于同步和互斥的手段,信号量的值可以是0、1或者n。信号量与操作系统中的经典概念PV操作对应。
互斥体。尽管信号量已经可以实现互斥的功能,但是“正宗”的mutex在Linux内核中还是真实地存在着。
总结自旋锁和互斥体选用的3项原则:
(1) 若临界区比较小,宜使用自旋锁,若临界区很大,应使用互斥体。
(2) 互斥体所保护的临界区可以包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区。
(3) 互斥体存在于进程上下文,因此,如果被保护的共享支援需要在中断或者软中断情况下使用,则在互斥体和自旋锁之间只能选择自旋锁。当然,如果一定要使用互斥体,则只能通过mutex_trylock()方式进行,不能获取就立即返回以避免阻塞。
Linux提供了完成量(Completion),它用于一个执行单元等待另一个执行单元执行完某事。
增加并发控制后的globalmem的设备驱动。
在globalmem()读写函数中,由于调用了copy_from_user()等可能导致阻塞的函数,因此不能使用自旋锁,宜使用互斥体。
驱动工程师习惯将某设备锁使用的自旋锁、互斥体等辅助手段也放在设备结构中。
struct globalmem_dev{
…
struct mutexmutex;
}
加载函数中添加互斥体初始化:
mutex_init(&globalmem_devp->mutex);
globalmem的读写操作中,在访问共享资源时,先获取互斥体,访问完成后,释放互斥体。
mutex_lock(&dev->mutex);
if(copy_from_user(buf, dev->mem + p, count)){
<span style="white-space:pre"></span>ret = -EFAULT;
}else
…
mutex_unlock(&dev->mutex);
如果在读写的同时,另一个执行单元执行MEM_CLEAR IO控制命令,也会到导致全局内存的混乱,因此globalmem_ioctl()修改如下:
…
case MEM_CLEAR:
mutex_lock(&dev->mutex);
memset(dev->mem,0 , GLOBALMEM_SIZE);
mutex_unlock(&dev->mutex);
…