Linux并发控制

作者: Sophisticated
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Sophisticated_/article/details/83017371

并发及其管理

正在运行的多个用户空间进程可能以一种惊讶的组合方式来访问我们的代码。内核代码是可抢占的,因此,我们驱动程序的代码可能在任何时候丢失对处理器的独占,而拥有处理器的进程可能正在调用驱动程序代码。设备中断是异步事件,也会导致代码并发执行。

只要可能,就应该避免资源的共享,如果没有并发的访问,也就不会有竞态产生,这种思想最明显的应用就是避免使用全局变量。但事情的本质是,这种类型的共享通常是必需的,硬件资源本质上就是共享的,而软件资源经常需要对其他线程可用,而且全局变量并不是共享数据的唯一途径,只要我们的代码将一个指针传递给内核其他部分,一个新的共享就有可能建立。

信号量

#include<asm/semaphore.h>

定义信号量及其操作的包含文件

DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);

用于声明和初始化用在互斥模式中的信号量的两个宏

void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);

这两个函数可以在运行时初始化信号量

void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
void up(struct semaphore *sem);

锁定和解锁信号量。
down可能会将调用进程置于不可中断的休眠状态,然后等待信号量变得可用。
down_interruptible操作是可中断的,运行等待在某个信号量上的用户空间进程可被用户中断,可中断版本几乎始终我们需要的版本,因为非中断操作是建立不可杀进程的好方法,down_interruptible应该始终检查返回值。
down_trylock永远不会休眠,在信号量不可用时立即返回一个非零值。
调用up后,调用者不再拥有该信号量。

struct rw_semaphore;
init_rwsem(struct rw_semaphore *sem);

信号量的读取者、写入者版本以及用来初始化这种信号量的函数

void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);

获取并释放读取者、写入者信号量的读取访问函数

void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);

对读取者、写入者信号量的写入访问管理函数

Completion

内核编程常见的一种模式,当前线程之外初始化某个活动,然后等待该活动的结束。

我们可以用信号量来同步这两个任务:

struct semaphore sem;
init_MUTEX_LOCKED(&sem);
start_external_task(&sem);
down(&sem);

当外部任务完成时将调用up(&sem);

如果存在针对该信号量的严重竞争,性能将受到影响。

#include<linux/completion.h>
DECLARE_COMPLETION(name);
init_completion(struct completion *c);
INIT_COMPLETION(struct completion c);

初始化completion的常用方法,INIT_COMPLETION只能对已经使用过的completion重新初始化。

void wait_for_completion(struct completion *c);

等待一个completion事件的发生

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

发出completion事件信号,complete只能唤醒一个等待者,而complete_all会唤醒所有等待者

自旋锁

自旋锁可在不能休眠的代码中使用,比如中断处理例程。所有自旋锁在本质上都是不可中断的,一旦调用了spin_lock,在获得锁之前将一直处于自旋状态。

适用于自旋锁的核心原则:
任何拥有自旋锁的代码都必须是原子的,它不能休眠,不能因为任何原因放弃处理器。自旋锁必须在可能的最短时间内拥有,拥有自旋锁的时间越长,其他处理器不得不自旋以等待释放该自旋锁的时间越长。

#include<linux/spinlock.h>
spinlock_t lock = SPIN_LOCK_UNLOCKED;
spin_lock_init(spinlock_t *lock);

初始化自旋锁的两个方式

void spin_lock(spinlock_t *lock);
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_bh(spinlock_t *lock);

spin_lock_irqsave会在获得自旋锁之前禁止中断,先前的中断状态保存在flags中
若我们确保没有任何其他代码禁止本地中断,或者说我们能确保在释放自旋锁时应该启用中断,则可以使用 spin_lock_irq,而不用跟踪标志
spin_lock_bh在获得自旋锁之前禁止软中断,但会让硬中断打开

int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);

非阻塞的自旋锁操作,在获得自旋锁时返回非零值,否则返回零

void spin_unlock(spinlock_t *lock);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);

释放自旋锁的相应途径

原子变量

类似于java中的Atomic类,执行原子操作

#include<asm/atomic.h>
atomic_t v = ATOMIC_INIT(value);
void atomic_set(atomic_t *v, int i);
int atomic_read(atomic_t *v);
void atomic_add(int i, atomic_t *v);
void atomic_sub(int i, atomic_t *v);
void atomic_inc(atomic_t *v);
void atomic_dec(atomic_t *v);

Seqlock

顺序锁的设计思想是:对某一个共享数据读取的时候不加锁,写的时候加锁。
同时为了保证读取的过程中因为写进程修改了共享区的数据,导致读进程读取数据错误。在读取者和写入者之间引入了一个整形变量sequence,读取者在读取之前读取sequence, 读取之后再次读取此值,如果不相同,则说明本次读取操作过程中数据发生了更新,需要重新读取。而对于写进程在写入数据的时候就需要更新sequence的值。

顺序锁(seqlock)是对读写锁的一种优化,提高了读锁和写锁的独立性。写锁不会被读锁阻塞,读锁也不会被写锁阻塞。写锁会被写锁阻塞。

顺序锁有一个限制:它必须要求被保护的共享资源中不能含有指针;因为写执行单元可能会使指针失效,当读执行单元如果正要访问该指针时,系统就会崩溃。

seqlock的实现思路是:
用一个递增的整型数表示sequence。写操作进入临界区时,sequence++;退出临界区时,sequence再++。
写操作还需要获得一个锁(比如mutex),这个锁仅用于写写互斥,以保证同一时间最多只有一个正在进行的写操作。
当sequence为奇数时,表示有写操作正在进行,这时读操作要进入临界区需要等待,直到sequence变为偶数。
读操作进入临界区时,需要记录下当前sequence的值,等它退出临界区的时候用记录的sequence与当前sequence做比较,不相等则表示在读操作进入临界区期间发生了写操作,这时候读操作读到的东西是无效的,需要返回重试。

#include<linux/seqlock.h>
seqlock_t lock = SEQLOCK_UNLOCKED;
seqlock_init(seqlock_t *lock);

unsigned int read_seqbegin(seqlock_t *lock);
unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);
int read_seqretry(seqlock_t *lock, unsigned int seq);
int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq, unsigned long flags);

用于获取受seqlock保护资源的读取访问函数

void write_seqlock(seqlock_t *lock);
void wirte_seqlock_irqsave(seqlock_t *lock, unsigned long flags);
void wirte_seqlock_irq(seqlock_t *lock);
void wirte_seqlock_bh(seqlock_t *lock);
void wirte_tyrseqlock(seqlock_t *lock);

用于获取受seqlock保护的资源的写入访问函数

void write_sequnlock(seqlock_t *lock);
void wirte_sequnlock_irqrestore(seqlock_t *lock, unsigned long flags);
void wirte_sequnlock_irq(seqlock_t *lock);
void wirte_sequnlock_bh(seqlock_t *lock);

用于释放受seqlock保护的资源的写入访问函数

读取-复制-更新(RCU)

对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。这个时机就是所有引用该数据的CPU都退出对共享数据的操作。

#include<linux/rcupdate.h>
void rcu_read_lock;
void rcu_read_unlock;

获取对受RCU保护资源的读取访问的宏,rcu_read_lock调用非常快,它会禁止内核抢占。

void call_rcu(struct rcu_head *head, void *(func)(void *arg), void *arg);

准备用于安全释放受RCU保护资源的回调函数,该函数将在所有的处理器被调度后运行。


总结

信号量自旋锁
开销成本进程上下文切换时间忙等待获得自旋锁时间
特性导致阻塞,产生睡眠忙等待,内核抢占关闭
应用场合只能运行于进程上下文可出现在中断上下文
其他可出现在用户进程中只能出现在内核线程中

在这里插入图片描述

除了信号量和自旋锁,还有seqlock,RCU等,总结如下图:

类型机制应用场合
spinlock使用忙等待,进程不挂起多处理器共享数据、可抢占内核共享数据、在任何上下文使用,适用于保持时间非常短
semaphore阻塞时等待,进程挂起共享区保持较长时间、只能用于进程上下文
atomic数据的原子访问共享简单数据类型:整型、适合高效率
rwlock特殊自旋锁允许同时读共享资源,但只有一个写。读优先于写,读写不能同时
seqlock免锁机制,基于访问计数允许同时读共享资源,但只有一个写。写优先于读,读写不能同时
RCU通过副本的免锁访问读占主要场合提供高性能、读访问不必获取锁,不必执行原子操作或禁止中断
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值