首先我们来了解下为什么要对设备驱动中引入并发控制,这个问题简单的理解就是设备资源只有一个,但是在同时间内,有好多的进程需要访问它。对设备资源进行并发控制,让其在同一时间只能被一个进程访问,这样就不能造成访问的紊乱问题,解决了访问资源的竞态。
例如有一段内存空间,存储了数据,有多个进程能够对这段资源进行读写,当一个进程对资源进行写操作的时候,来了另一个进程对资源进行读操作,在没有写完这段内存的时候进行读访问,这种多个进程对资源的并发访问就导致了竞态的产生。
竞态发生的类型:
(1)多核处理器容易发生CPU之间的对内存和外设的并发访问。
(2)单内核处理的CPU的进程之间的访问共享资源。
(3)中断与进程之间的资源访问。
对访问的共享资源代码区域成为临界区(critical sections)。那么如果对这段临界区的访问进行保护,而避免产生竞态呢?途径有:
(1) 中断屏蔽
中断屏蔽的使用方法:
local_irq_disable(); // 屏蔽中断
....
critical section // 临界区
.....
local_irq_enable(); // 开中断
场合:保证正在执行的内核路径不被中断处理程序所抢占。是中断和进程之间能够产生的竞态使用的方法。
要求:临界区的代码应该非常短,时间长会导致其他中断不能相应,后果很严重。
注意:中断屏蔽智能禁止和使能本CPU的中断,多核的CPU不能解决竞态,单独使用时需要和自旋锁配合使用。
(2)原子操作
原子操作有位和整数原子操作两种类型。
原子操作有好几个函数,实现原子变量的设置、获取、加减、自加自减等操作。
在这里以实例的方式介绍:
/*这个例子是使用原子操作来实现设备只能被一个进程打开*/
// 首先定义一个原子变量
static atomic_t xxxx_available ATOMIC_INIT(1);
static int xxx_open(struct inode *inode,struct file*filp)
{ // atomic_dec_and_test() 函数是减去一时候为零,为零返回true,否则返回flase.
// 如果设备没有打开,测试返回TRUE,取反的话不执行if 语句下面的代码。返回打开成功
// 如果已经打开,返回false,取反为1,执行if
if(!atomic_dec_and_test(&xxx_available)){
atomic_inc(&xxx_available); 把减掉的1加上
return -EBUSY; // 已经被打开了
}
...
return 0; 打开成功
}
static int xxx_release(struct inode *inode, struct file *filp)
{
atomic_inc(&xxx_available); /* 释放设备 */
return 0;
}
(3)自旋锁
“自旋锁(Spin Lock)是一种典型的对临界资源进行互斥访问的手段,其名称来源于它的工作方式。为了获得一个自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试并设置(Test-And-Set)某个内存变量。由于它是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋”,通俗地说就是“在原地打转””--Linux驱动开发详解中所说。
自旋锁的使用:
/*
1)定义自旋锁:spinlock_t lock;
2) 初始化自旋锁: spin_lock_init(lock);
3) 获取自旋锁:spin_lock(lock);
spin_trylock(lock); 这个函数是获取是如果能立马获取的话返回 true ,否则false,是不等待,不原地旋转。
4) 释放自旋锁: spin_unlock(lock);
*/
使用模板:
/* 定义一个自旋锁 */
spinlock_t lock;
spin_lock_init(&lock);
spin_lock (&lock) ; /* 获取自旋锁,保护临界区 */
. . ./* 临界区 */
spin_unlock (&lock) ; /* 解锁 */
场合:针对多核和单核的抢占情况
注意:(1)自旋锁能够受到中断和底半部的影响。因此和中断有拓展的函数存在,形成整套的自旋锁机制。这里就不介绍了。
(2)自旋锁是忙等锁,如果占用锁的时间既短暂的话,使用合理,长时间的话等待时间长降低了系统的性能。
(3)递归使用自旋锁会导致锁死的情况发生。
(4)在自旋锁锁定期间不能调用可能引起进程调度的函数。例如copy_from_user(), copy_to_user(), kmalloc(), msleep()。
(5)衍生的读写自旋锁、顺序自旋锁。
(4)信号量
信号量是实现同步和互斥访问的重要手段,使用过程有定义信号量、初始化信号量、获取信号量及释放信号量。当能获取到信号量是进入临界保护区,当不能获取信号量是进入休眠状态。由此可见和自旋锁不同的是信号量不在原地旋转等待,而是直接进入休眠。
struct semaphore sem;
void sema_init(struct semaphore *sem, int val);
void down(struct semaphore * sem);
int down_interruptible(struct semaphore * sem);
int down_trylock(struct semaphore * sem);
void up(struct semaphore * sem);
场合:1)用于互斥,是对一段临界区实现互斥访问。
2)用于同步,两个进程之间的同步, 一个进程释放信号量后,另一个进程获取到信号量后再执行。(生产者和消费者问题)
注意:mutex也可以用于互斥,一般常使用信号量来同步,互斥倾向于mutex。
(5)互斥体
mutex 的使用和信号量基本相同,实现互斥访问的场合。
不同点: 互斥体是实现进程之间对资源的互斥访问,如果互斥体抢占失败的情况下会进行进程之间的切换,当前的进程进入到睡眠态。而自旋锁如果访问失败的话会在原地等待,自旋锁释放,不会进行进程切换,如果访问临界区时间短时,自旋锁不进行进程切换,比较方便。如果在中断中使用只能选择自旋锁。中断中不能上下文切换,阻塞。
(6)完成量
到最后说下完成量,完成量是当一个进程执行某段代码完成后,唤醒另一个进程执行的操作。
操作有定义完成量,初始化完成量,等待完成量和唤醒完成量。
使用过程是:
1)先定义,初始化 。。。。
2) 进程1 执行代码,执行完了 唤醒完成量,complete(&done)。
3)进程2 wait_for_completion(&done),如果等待完成,则执行进程2下面的代码。
在以后的笔记中会举个并发控制的设备驱动加深理解。