linux设备驱动开发详解(基于4.0内核)_读书笔记二

时间:2021-10-19 10:52:57

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);