《驱动学习 - 同步互斥阻塞》

1.同步互斥阻塞的概念

并发与竞态:

  并发(concurrency)指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(race conditions)。

在linux中,主要的竞态发生在如下几种情况:

  1、对称多处理器(SMP)多个CPU                特点是多个CPU使用共同的系统总线,因此可访问共同的外设和存储器。

  2、单CPU内进程与抢占它的进程

  3、中断(硬中断、软中断、Tasklet、底半部)与进程之间

  只要并发的多个执行单元存在对共享资源的访问,竞态就有可能发生。

  如果中断处理程序访问进程正在访问的资源,则竞态也会会发生。

  多个中断之间本身也可能引起并发而导致竞态(中断被更高优先级的中断打断)。 

 

解决竞态问题的途径:

  是保证对共享资源的互斥访问,所谓互斥访问就是指一个执行单元在访问共享资源的时候,其他的执行单元都被禁止访问。 

  访问共享资源的代码区域被称为临界区,临界区需要以某种互斥机制加以保护,如:中断屏蔽,原子操作,自旋锁,和信号量都是linux设备驱动中可采用的互斥途径。 

临界区和竞争条件:

  所谓临界区(critical regions)就是访问和操作共享数据的代码段,为了避免在临界区中并发访问,编程者必须保证这些代码原子地执行——也就是说,代码在执行结束前不可被打断,就如同整个临界区是一个不可分割的指令一样,如果两个执行线程有可能处于同一个临界区中,那么就是程序包含一个bug,如果这种情况发生了,我们就称之为竞争条件(race conditions),避免并发和防止竞争条件被称为同步。 

阻塞与非阻塞:

  阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程,而是直接返回。

 

2.为什么使用原子操作

原子操作:

  顾名思义,就是说像原子一样不可再细分不可被中途打断。一个操作是原子操作,意思就是说这个操作是以原子的方式被执行,要一口气执行完,执行过程不能够被OS的其他行为打断,是一个整体的过程,在其执行过程中,OS的其它行为是插不进来的。

为什么要使用原子操作?

static int canopen = 1;

static int drv_open(略)
{
    if(--canopen != 0)
    {
         canopen++;
         return -EBUSY;
    }  
}

static int drv_close(略)
{
    canopen++;    
}

  根据上面的代码,一旦应用程序A调用open,那么canopen就会调用驱动程序drv_open,canopen自减等于0,if判断不成立,就接下来去执行其他的操作。当应用程序B调用open的时候,canopen自减等于-1,if判断成立,然后返回。驱动程序看起来就具备了每次只能有一个进程使用该设备的现象。

  但是实际不是这样。

  canopen的自减在汇编层面分为三个步骤:

  1.读出canopen的值

  2.canopen的值减1

  3.将canopen的值写回去

  因为linux是多任务系统,那么如果在应用程序A调用open的时候,canopen=1被读出来,这时候应用程序B也调用open,并且中断了应用程序A,应用程序B自减完,将canopen=0写回去。这时候应用程序A后继续上一步执行,将canopen=1自减,然后写回去。这样A和B都成功执行了open操作。

  因此需要原子操作来确保执行这些代码时不会被其他打断。

 

static atomic_t canopen = ATOMIC_INIT(1);

static int drv_open(略)
{
    if(!atomic_dec_and_test(&canopen))
    {
        atomic_inc(&canopen);
        return -EBUSY;
    }  
}

static int drv_close(略)
{
    atomic_inc(&canopen);
}

  将canopen定义为原子变量,初始值为1

  atomic_dec_and_test(&canopen)将canopen进行减1后判断它是否为0,如果为0,则返回true,否则flase。

  atomic_inc就是自增。

原子操作的函数在arch/arm/include/asm/atomic.h中有定义,基本根据函数名就知道什么意思,这边就不列举了。

 

3.信号量

  信号量(semaphore)是用于保护临界区的一种常用方法,只有得到信号量的进程才能执行临界区代码。当获取不到信号量时,进程进入休眠等待状态。

//定义信号量
struct semaphore sem;

//初始化信号量
void sema_init(struct semaphore *sem, int val);
void init_MUTEX(struct semaphore *sem);   //初始化为0

static DECLARE_MUTEX(button_lock);    //定义互斥锁,里面包含了定义和初始化。等价于上面两个函数

//获取信号量
void down(struct semaphore *sem); //获取信号量,获取不到就休眠
int down_interruptible(struct semaphore *sem);//这是获取不到信号量就去休眠,休眠可以被打断
int down_trylock(struct semaphore *sem);//尝试获取信号量,获取不到立即返回

//释放信号量
void up(struct semaphore *sem)

相关函数可以在include/linux/semaphore.h中可以查看到

 

 

4.阻塞与非阻塞模式

  阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。

  非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程,而是直接返回。

  open函数:用来打开一个设备,他返回的是一个整型变量,是文件的标识符。如果这个值等于-1,说明打开文件出现错误。

  open("/dev/xxx", O_RDWR );

  这是以读写方式打开某种设备文件,默认是阻塞操作。

  open("/dev/xxx", O_RDWR | O_NONBLOCK);

  O_NONBLOCK 如果pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选择项为此文件的本次打开操作和后续的I/O操作设置非阻塞方式。

  这时候在驱动程序中也应该添加对非阻塞的处理。

static DECLARE_MUTEX(button_lock);     //定义互斥锁
static
int sixth_drv_open(struct inode *inode, struct file *file) { if (file->f_flags & O_NONBLOCK) { if (down_trylock(&button_lock)) return -EBUSY; } else { /* 获取信号量 */ down(&button_lock); }
  return 1; }

int sixth_drv_close(struct inode *inode, struct file *file)
{
   up(&button_lock);
  return 0;
}

  获取file的标志位,判断是否为非阻塞。如果为非阻塞,就尝试获取信号量,没用获取到信号量则立即返回。否则为阻塞模式,没用获取到信号量进行休眠,直到获取信号量。

 

posted @ 2019-10-10 09:33  一个不知道干嘛的小萌新  阅读(201)  评论(0编辑  收藏  举报