文章目录
并发机制
多个执行单元对一个共享资源的访问,会导致竞态(访问结果出错)。
- 多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应运而生