Linux 设备驱动中的并发控制

首先我们来了解下为什么要对设备驱动中引入并发控制,这个问题简单的理解就是设备资源只有一个,但是在同时间内,有好多的进程需要访问它。对设备资源进行并发控制,让其在同一时间只能被一个进程访问,这样就不能造成访问的紊乱问题,解决了访问资源的竞态。

例如有一段内存空间,存储了数据,有多个进程能够对这段资源进行读写,当一个进程对资源进行写操作的时候,来了另一个进程对资源进行读操作,在没有写完这段内存的时候进行读访问,这种多个进程对资源的并发访问就导致了竞态的产生。

竞态发生的类型:

(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下面的代码。

在以后的笔记中会举个并发控制的设备驱动加深理解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章