并发与竞态

发布于:2024-10-18 ⋅ 阅读:(49) ⋅ 点赞:(0)

一、竞态发生原因

  1. 多核多CPU使用同一总线对共享资源的访问(并行)
  2. 多进程/多线程对共享资源的访问(并发)
  3. 中断对共享资源的访问

二、竞态的解决

1.中断屏蔽

local_irq_disable()
...
local_irq_enable()

只能禁止和使能本CPU内的中断,并不能解决多CPU引发的竞态。

2.原子操作

对于arm而言,底层使用LDREX核STREX指令,让总线监控两个指令之间有无其他存取该地址的实体,如果有则重新执行该段存取指令。

适用于单核/多核的竞态处理

int value = 0;

/*
 1. 整型原子操作
*/
atomic_t v = ATOMIC_INIT(0); // 初始化原子变量为0

atomic_set(&v, 1); // 设置原子变量值为1

atomic_add(1, &v); // 原子变量值+1
atomic_sub(1, &v); // 原子变量值-1

atomic_inc(&v); // 原子变量值自增
atomic_dec(&v); // 原子变量值自减

value = atomic_sub_and_test(1, &v); // 原子变量值-1, 并返回是否为0
value = atomic_inc_and_test(&v); // 原子变量值自增, 并返回是否为0
value = atomic_dec_and_test(&v); // 原子变量值自减, 并返回是否为0

value = atomic_add_return(1, &v); // 原子变量值+1, 并返回新值
value = atomic_sub_return(1, &v); // 原子变量值-1, 并返回新值
value = atomic_inc_return(&v); // 原子变量值自增, 并返回新值
value = atomic_dec_return(&v); // 原子变量值自减, 并返回新值

value = atomic_read(&v); // 读取原子变量的值

/*
 2. 位原子操作
*/

set_bit(2, &v); // 将对应地址的第2位置1

clear_bit(2, &v); // 将对应地址的第2位置0

change_bit(2, &v); // 将对应地址的第2位取反

value = test_bit(2, &v); // 返回对应地址的第2位

value = test_and_set_bit(2, &v); // 将对应地址的第2位置1, 并测试

value = test_and_clear_bit(2, &v); // 将对应地址的第2位置0, 并测试

value = test_and_change_bit(2, &v); // 将对应地址的第2位取反, 并测试

3.自旋锁

本质上是设置原子变量+测试,如果失败了则循环该过程,起到“自旋”的效果

注意事项:

  • 主要针对SMP或单CPU但内核可抢占的情况,对于但CPU和内核不支持抢占的系统,自旋锁退化为空操作。
  • 自旋锁不能递归使用,会造成死锁。
  • 一般只有在占用锁时间极短的情况下使用,太长会导致系统并发性能降低
  • 由于自旋锁在获取前会关闭进程抢占,为了防止其他进程无限自旋,在当前进程自旋锁定期间不能调用可能引起进程调度/阻塞的函数
spinlock_t lock;

void spin_lock_init(&lock); // 初始化

void spin_lock(&lock); // 获取失败则自旋等待

bool spin_trylock(&lock); // 获取失败返回false

void spin_unlock(&lock); // 释放锁

不受中断和底半部影响的自旋锁(自旋锁的衍生)

spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()

spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()

spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()

如果进程和中断可能访问同一片临界资源,一般在进程上下文中使用spin_lock_irqsave(),而在中断上下文中使用spin_lock()

3.1.读写自旋锁

允许读的并发,但是写时只能有一个进程(也不允许读)。

rwlock_t lock;
rwlock_init(&lock);

read_lock(&lock);
read_unlock();

write_lock_irqsave();
write_unlock_irqrestore();

//...

3.2.顺序自旋锁

读执行单元不会被写执行单元阻塞,写执行单元也不会被读执行单元阻塞。但是如果读操作期间,写执行单元已经完成勒操作,则必须重新读取数据。

/*写执行单元*/

write_seqlock(&seqlock_t);
//...
write_sequnlock(&seqlock_t);


/*读执行单元*/
do {
    seqnum = read_seqbegin(&seqlock_t);
} while(read_seqretry(&seqlock_t,seqnum));

3.3. 读-复制-更新(RCU)

读操作只是简单增加对共享数据的引用,而写操作需要拷贝副本,对副本进行更改,等到所有读引用都结束之后,再将原数据指向新的数据。

4.信号量

信号量与PV操作对应,主要用于进程/线程间同步

P(S): S = S - 1; 若 S >= 0, 则进程/线程继续执行, 否则进入等待队列

V(S): S = S + 1; 若 S >0, 唤醒等待队列中的进程/线程

struct semaphore sem;

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

void down(struct semaphore *sem); // 获取信号量

int down_interruptible(struct semaphore *sem); // 进入睡眠状态后能被信号打乱. 如果返回值非0, 通常立刻返回-ERESTARTSYS

int down_trylock(struct semaphore *sem); // 获取失败不会导致睡眠

void up(struct semaphore *sem);

5.互斥体

互斥体主要用于进程/线程间互斥

struct mutex my_mutex;
mutex_init(&my_mutex);

void mutex_lock(struct mutex* lock);

int mutex_lock_interruptible(struct mutex* lock);

int mutex_trylock(struct mutex *lock);

5.1.自旋锁与互斥体的选择

互斥体时进程级的,处于进程上下文之中,而自旋锁则是更底层的实现,本质上互斥体需要使用自旋锁。

1.当访问临界区的时间较长时,使用互斥体可以防止进程慢等待导致CPU空转

2.当访问临界区时间较短时,使用自旋锁可以省去上下文切换的开销

3.当临界区需要再中断或软中断的情况下使用,则只能选择自旋锁

5.2. 互斥锁与信号量的选择:

锁只有0,1两种状态,主要用于保证资源访问的唯一性和排他性;信号量则可以是非负整数,在保证互斥的前提下,可以用于执行单元之间的资源信息同步,使得资源被有序合理占有。

1.当某些执行单元提供临界区的资源,而另一些执行单元消费临界区资源的时候,使用信号量。

2.当临界区的访问具有唯一、排他性的时候,使用锁

6.完成量

用于一个执行单元等待另一个执行单元完成某事。

void init_completion(struct completion*);
void reinit_completion(struct completion*);

void wait_for_completion(struct completion*);

void complete(struct completion*);
void complete_all(struct completion*);


今日签到

点亮在社区的每一天
去签到