《Linux设备驱动开发详解》学习笔记 -- 并发控制

什么是并发?

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

例子:
假如两个进程A,B同时对一个内存单元进行操作。
内存中的某个单元 M 保存并初始化变量 i = 100;
A:对 i 进行 加 1操作 B:对 i 进行 减 1 操作 ;
那么: 最后 i的值,就变成不确定的值了。

更复杂、更混乱的并发大量地存在于设备驱动中,只要并发的多个执行单元存在对共享资源的访问,竞态就可能发生。


常见并发引起竞态的几种场景

1对称多处理器(SMP)的多个 CPU

SMP 是一种紧耦合、共享存储的系统模型,其体系结构下图所示,它的特点是多个 CPU 使用共同的系统总线,因此可访问共同的外设和储存器。

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

Linux 2.6 内核支持抢占调度,一个进程在内核执行的时候可能被另一高优先级进程打断,进程与抢占它的进程访问共享资源的情况类似于 SMP 的多 个 CPU。

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

中断可以打断正在执行的进程,如果中断处理程序访问进程正在访问的资源,则竞态也会发生。
此外,中断也有可能被新的更高优先级的中断打断,因此,多个中断之间本身也可能引起并发而导致竞态。
上述并发的发生情况除了 SMP 是真正的并行以外,其他的都是“宏观并行、微观串行”的,但其引发的实质问题和 SMP 相似。


逻辑关系

并发 — (引起)——>竞态 <——(解决)——互斥访问机制


解决竞态的几种互斥机制(手段)

解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。访问共享资源的代码区域称为临界区
临界区需要以下几种互斥机制加以保护
中断屏蔽
原子操作
自旋锁和
信号量

1中断屏蔽

2原子操作

原子操作指的是在执行过程中不会被别的代码路径所中断的操作。

Linux原子操作问题来源于中断、进程的抢占以及多核smp系统中程序的并发执行。对于临界区的操作可以加锁来保证原子性,对于全局变量或静态变量操作则需要依赖于硬件平台的原子变量操作。因此原子操作有两类:一类是各种临界区的锁,一类是操作原子变量的函数。
对于arm来说,单条汇编指令都是原子的,多核smp也是,因为有总线仲裁所以cpu可以单独占用总线直到指令结束,多核系统中的原子操作通常使用内存栅障(memory barrier)来实现,即一个CPU核在执行原子操作时,其他CPU核必须停止对内存操作或者不对指定的内存进行操作,这样才能避免数据竞争问题。但是对于load update store这个过程可能被中断、抢占,所以arm指令集有增加了ldrex/strex这样的实现load update store的原子指令
但是linux种对于c/c++程序(一条c编译成多条汇编),由于上述提到的原因不能保证原子性,因此linux提供了一套函数来操作全局变量或静态变量
这些函数又分为两类,分别针对位和整型变量进行原子操作。它们的共同点是在任何情况下操作都是原子的,内核代码可以安全地调用它们而不被打断。位和整型变量原子操作都依赖底层CPU 的原子操作来实现,因此所有这些函数都与 CPU 架构密切相关.

整型原子操作
初始化原子变量的值

void atomic_set(atomic_t *v, int i); //初始化原子变量v的值为 i
atomic_t v = ATOMIC_INIT(0); //定义原子变量 v 并初始化为 0

获取原子变量的值

atomic_read(atomic_t *v); //返回原子变量的值

原子变量加/减

void atomic_add(int i, atomic_t *v); //原子变量增加 i
void atomic_sub(int i, atomic_t *v); //原子变量减少 i

void atomic_inc(atomic_t *v); //原子变量增加 1
void atomic_dec(atomic_t *v); //原子变量减少 1

操作并测试

int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);

上述操作对原子变量执行自增、自减和减操作后(注意没有加)测试其是否为 0,为 0 则返回 true,否则返回 false。

操作并返回

int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);

上述操作对原子变量进行加/减和自增/自减操作,并返回新的值

位原子操作
代码清单 : 使用原子变量使设备只能被一个进程打开

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

在文件开头定义并初始化一个整形原子变量,初始化为1 。
open函数中,当第一个进程打开设备时,因为available原子变量的初值为1,atomic_dec_and_test做自减后测试为0,返回true,所以open就成功。
那第二个进程打开设备时,原子变量已经减到0了,所以atomic_dec_and_test返回false,所以open返回EBUSY。
不管是第二个进程打开失败,还是第一个进程执行结束,都不要忘了恢复原子变量的值.(atomic_inc(&scull_available);)

3自旋锁

自旋锁(spin lock)是一种对临界资源进行互斥手访问的典型手段,其名称来源于它的工作方式。为了获得一个自旋锁,在某 CPU 上运行的代码需先执行一个原子操作,该操作测试并设置(test-and-set)某个内存变量,由于它是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。
如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋” ,通俗地说就是“在原地打转” 。当自旋锁的持有者通过重置该变量释放这个自旋锁后,某个等待的“测试并设置”操作向其调用者报告锁已释放。理解自旋锁最简单的方法是把它作为一个变量看待,该变量把一个临界区或者标记为“我当前在运行,请稍等一会”或者标记为“我当前不在运行,可以被使用” 。
如果 A 执行单元首先进入例程,它将持有自旋锁;当 B 执行单元试图进入同一个例程时,将获知自旋锁已被持有,需等到 A 执行单元释放后才能进入

自旋锁操作
1.定义自旋锁
spinlock_t spin;
2.初始化自旋锁
spin_lock_init(lock)
该宏用于动态初始化自旋锁 lock
3.获得自旋锁
spin_lock(lock)
该宏用于获得自旋锁 lock,如果能够立即获得锁,它就马上返回,否则,它将自旋在那里,直到该自旋锁的保持者释放;
spin_trylock(lock)
该宏尝试获得自旋锁 lock,如果能立即获得锁,它获得锁并返回真,否则立即返回假,实际上不再“在原地打转” ;
4.释放自旋锁
spin_unlock(lock)
该宏释放自旋锁 lock,它与 spin_trylock 或 spin_lock 配对使用。

自旋锁的状态值为1表示解锁状态,说明有1个资源可用;0或负值表示加锁状态,0说明可用资源数为0。

自旋锁一般这样被使用:

/* 定义一个自旋锁 */
spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock);  /* 获取自旋锁,保护临界区 */

critical section  /临界区/

spin_unlock(&lock);  /* 解锁*/

下面是一个实例:

static spinlock_t lock;
static int flag = 1;
static int hello_open (struct inode *inode, struct file *filep)
{
  spin_lock(&lock);
  if(flag !=1)
  {
    spin_unlock(&lock);
    return -EBUSY;
  }
  flag = 0;
  spin_unlock(&lock);
  return 0;
}
static int hello_release (struct inode *inode, struct file *filep)
{
  flag = 1;
  return 0;
}

自旋锁的特点:

4信号量

信号量(semaphore)是用于保护临界区的一种常用方法,它的使用方式和自旋锁
类似。与自旋锁相同,只有得到信号量的进程才能执行临界区代码。但是,与自旋锁
不同的是,当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。

信号量的操作

1.定义信号量
下列代码定义名称为 sem 的信号量。

struct semaphore sem;

2.初始化信号量

void sema_init (struct semaphore *sem, int val);

该函数初始化信号量,并设置信号量 sem 的值为 val。尽管信号量可以被初始化为大于 1 的值从而成为一个计数信号量,但是它通常不被这样使用。

void init_MUTEX(struct semaphore *sem);

该函数用于初始化一个用于互斥的信号量,它把信号量 sem 的值设置为 1,等同于sema_init (struct semaphore *sem, 1)。

void init_MUTEX_LOCKED (struct semaphore *sem);

该函数也用于初始化一个信号量,但它把信号量 sem 的值设置为 0,等同于sema_init (struct semaphore *sem, 0)。

此外,下面两个宏是定义并初始化信号量的“快捷方式” 。

DECLARE_MUTEX(name)
DECLARE_MUTEX_LOCKED(name)

前者定义一个名为 name 的信号量并初始化为 1,后者定义一个名为 name 的信号量并初始化为 0。

3.获得信号量

void down(struct semaphore * sem);

该函数用于获得信号量 sem,它会导致睡眠,因此不能在中断上下文使用。

int down_interruptible(struct semaphore * sem);

该函数功能与 down()类似,不同之处为,因为 down()而进入睡眠状态的进程不能被信号打断,而因为down_interruptible()而进入睡眠状态的进程能被信号打断,信号也会导致该函数返回,这时候函数的返回值非 0。

int down_trylock(struct semaphore * sem);

该函数尝试获得信号量 sem,如果能够立刻获得,它就获得该信号量并返回 0,否则,返回非 0 值。它不会导致调用者睡眠,可以在中断上下文使用。在使用 down_interruptible()获取信号量时,对返回值一般会进行检查,如果非 0,通常立即返回-ERESTARTSYS,如:

if (down_interruptible(&sem))
{
return - ERESTARTSYS;
}

4.释放信号量

void up(struct semaphore * sem);

该函数释放信号量 sem,唤醒等待者。

信号量一般这样被使用,如下所示:

//定义信号量
DECLARE_MUTEX(mount_sem);
down(&mount_sem);//获取信号量,保护临界区

critical section //临界区

up(&mount_sem);//释放信号量

代码清单 7.3 使用信号量实现设备只能被一个进程打开

static DECLARE_MUTEX(xxx_lock);//定义互斥锁
static int xxx_open(struct inode *inode, struct file *filp)
{
     ...
     if (down_trylock(&xxx_lock)) //获得打开锁
     return - EBUSY; //设备忙
     ...
    return 0; /* 成功 */
}

>static int xxx_release(struct inode *inode, struct file *filp)
{
    up(&xxx_lock); //释放打开锁
    return 0;
 }

锁粒度问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值