Linux设备驱动-并发控制

10 篇文章 0 订阅

并发机制

多个执行单元对一个共享资源的访问,会导致竞态(访问结果出错)。

  • 多CPU访问一个共享资源
  • 单CPU多进程访问一个共享资源
  • 中断和进程访问一个共享资源
  • 多中断访问一个共享资源

一个典型的例子:
直接使用全局变量作为标志位。会在标志位更改前调度去其他任务,导致两个线程同时操作一个设备文件。

并发中的同步

避免竞态的方法是保证共享资源的互斥访问。

graph TD
a2(互斥机制)-->b2(原子操作 atomic)
a2(互斥机制)-->b3(互斥锁 mutex)
a2(互斥机制)-->b4(信号量 semaphore)
a2(互斥机制)-->b5(自旋锁 spinlock)

原子操作 atomic

include/linux/atomic.h

原子操作保证对一个数据的操作(一个操作,如修改原子变量有如下三步:读,±,写入)仅由当前执行单元进行。

atomic_t v = ATOMIC_INIT(1);/* 定义原子变量,赋初值1 */

/* 整型 */
void atomic_set(atomic_t *v, int i);/* 设置原子变量的值为i */
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);/* 原子变量自增 */
void atomic_dec(atomic_t *v);/* 原子变量减少 */
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);/* 减i并测试 */
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);

/* 位操作 */
void set_bit(nr, void *addr);
void clear_bit(nr, void *addr);
void change_bit(nr, void *addr);
int test_bit(nr, void *addr);
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);

下面操作定义了一个设备,仅能被一个进程打开

/* 定义 */
static atomic_t v_available = ATOMIC_INIT(1);

static int hello_open(struct inode *inode, struct file *filep)
{
    /* 申请资源,原子变量自减并测试是否为0 */
    if(!atomic_dec_and_test(&v))
    {
        //busy 原子变量自增
        atomic_inc(&v_available);
        return -EBUSY;
    }
    return 0;
}

static int hello_release(struct inode *inode, struct file *filep)
{
    /* 用完释放设备,原子变量自增 */
    atomic_inc(&v_available);
    return 0;   
}

原子变量用于要保护的资源比较简单的情况,开销较小。

信号量 semaphore

include/linux/semaphore.h

用于进程间共享资源的同步

static struct semaphore test_sem;

/* 初始化,可被2个任务占用 */
sema_init(&test_sem, 2);

/* 获得信号量,会导致休眠,不可在中断上下文中使用 */
down(&test_sem);
/* 和down()相比,down_interruptible休眠可被用户发送的信号中断, 而down()只会一直等待信号量可用 */
if(down_interruptible(&test_sem))
    return -ERESTARTSYS;
/* 释放信号量 */
up(&test_sem);

进程在临界区是可以睡眠的

在semaphore实现中,down里面有两段自旋锁,两段之间进行进程调度,用于等待信号量可用。
所以开销是挺大的,适用于临界区要保护的资源复杂或者临界区需要休眠的清空。

信号量有以下几个特点:

  • 用于进程和进程间同步
  • 允许多个进程进入临界区代码执行
  • 进程获取不到信号量会陷入休眠,让出CPU
  • 临界区允许休眠
  • 本质是基于进程调度器,UP(单核)和SMP(多核)下的实现无差异
  • 不支持进程和中断之间的同步

互斥锁 mutex

include/linux/mutex.h

DEFINE_MUTEX(test_mut);

mutex_lock(&test_mut);

mutex_unlock(&test_mut);
 

允许临界区阻塞,适用于临界区大的情况。

自旋锁 spinlock

include/linux/spinlock.h

等待自旋锁释放时,线程不休眠(无调度开销)。用于临界区访问耗时小,或者在中断等禁止睡眠的情况。

临界区禁止睡眠

spinlock_t lock;

spin_lock_init(&lock);

spin_lock (&lock) ;    /* 获取自旋锁,保护临界区*/

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

自旋锁用于在多处理器的环境下保护数据:
在单处理器(非抢占式内核)下,自旋锁不起作用
在单处理器(抢占式内核)下,自旋锁起到禁止抢占的作用

自旋锁有以下几个特点:

  • spinlock是一种死等的锁机制
  • 一次只能有一个执行单元进入临界区
  • 临界区需要执行时间短,临界区不能有阻塞操作(否则其他等待的核也会一直死等)
  • 可以用于中断上下文
函数使用场景
void spin_lock(spinlock_t *lock)进程和进程间同步
void spin_lock_bh(spinlock_t *lock)涉及到和本地软中断间的同步
void spin_lock_irq(spinlock_t *lock)涉及到和本地硬件中断间的同步
void spin_lock_irqsave(lock, flags)涉及到和本地硬件中断间的同步并保存本地中断状态
int spin_trylock(spinlock_t *lock)尝试获取锁,成功返回非0值
int spin_trylock_bh(spinlock_t *lock)
int spin_trylock_irq(spinlock_t *lock)
int spin_trylock_irqsave(lock, flags)
死锁
死锁原因

自旋锁一直自旋,无法获得释放

  • 使用自旋锁访问临界区时,突然产生的中断也要申请自旋锁
  • 自旋锁访问临界区时再次申请这个自旋锁(递归调用自旋锁)
  • 自旋锁访问临界区时产生阻塞,在阻塞未释放前,另一个线程申请自旋锁
死锁处理
  • 在临界区同时会被中断和进程访问时:
/* 进程中调用,需要失能中断 */
spin_lock_irqsave(&spin_cnt);
/* 临界区 */
spin_unlock_irqrestore(&spin_cnt);

/*  在中断中调用:
    spin_lock(..);

    spin_unlock(..);      */
  • 禁止递归调用自旋锁
  • 禁止调用可能引起进程调度的函数。如果进程获得自旋锁之后再阻塞,如调用copy_from_user()、copy_to_user()、kmalloc()和msleep()等函数,则可能导致内核的崩溃(锁定期间,不允许阻塞

在单核(UP)情况下编程的时候,也应该认为自己的CPU是多核(SMP) 的,驱动特别强调跨平台的概念

自旋锁原理

spinlock结构体包含两个16位的成员变量:owner, next。
当一个进程申请这个spinlock时,next=owner=0。
第二个进程申请这个spinlock时,next=next+1=1,第三个进程申请时next=next+1=2。
第一个进程释放spinlock时owner=owner+1=1,此时next=owner=1,第二个进程得到执行。。

读写锁 rw spinlock
  • 当临界区有一个write thread时,其他write 或者read thread都不能进入。
  • 当临界区有一个或多个read thread时,write thread不能进入,write必须等临界区没有read thread时才能进入。

适用于读优先的场景

顺序锁 seqlock
  • 当临界区有一个write thread时,其他write 或者read thread都不能进入。
  • 当临界区只有一个或多个read thread时,write thread可以立刻执行,不会等待。

适用于写优先的场景

spinlock的不足

多处理器分别对应多个一级缓存,当一个处理器改变了自己的缓存时,其他处理器会出现缓存未命中的情况,
需要从二级缓存或者mem中读取数据,mem的读写速度限制造成了处理器性能损耗。

RCU应运而生

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值