linux驱动学习---竞争与并发(原子操作与各种锁)

原子操作

Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变

量来代替整形变量,此结构体定义在 include/linux/types.h 文件中,定义如下:

c 复制代码
typedef struct {
    int counter;
} atomic_t;
函数原型 描述
ATOMIC_INIT(int i) 定义原子变量时初始化为 i
int atomic_read(atomic_t *v) 读取原子变量 v 的当前值并返回
void atomic_set(atomic_t *v, int i) 将原子变量 v 的值设置为 i
void atomic_add(int i, atomic_t *v) 给原子变量 v 增加 i
void atomic_sub(int i, atomic_t *v) 从原子变量 v 减去 i
void atomic_inc(atomic_t *v) 对原子变量 v 自增 1
void atomic_dec(atomic_t *v) 对原子变量 v 自减 1
int atomic_dec_return(atomic_t *v) 对原子变量 v 自减 1,并返回更新后的值
int atomic_inc_return(atomic_t *v) 对原子变量 v 自增 1,并返回更新后的值
int atomic_sub_and_test(int i, atomic_t *v) v 减去 i,若结果为 0 则返回真,否则返回假
int atomic_dec_and_test(atomic_t *v) v 自减 1,若结果为 0 则返回真,否则返回假
int atomic_inc_and_test(atomic_t *v) v 自增 1,若结果为 0 则返回真,否则返回假
int atomic_add_negative(int i, atomic_t *v) v 增加 i,若结果为负数则返回真,否则返回假

如果使用 64 位的 SOC 的话,就要用到 64 位的原子变量,Linux 内核也定义了 64 位原子

结构体,如下所示:

c 复制代码
typedef struct {
    long long counter;
} atomic64_t;

相应的也提供了 64 位原子变量的操作 API 函数,只是将"atomic_"前缀换为"atomic64_",将 int 换为 long long。如果使用的是 64 位的 SOC,那么就要使用 64 位的原子操作函数

原子位操作 API 函数

位操作也是很常用的操作,Linux 内核也提供了一系列的原子位操作 API 函数,只不过原子位操作不像原子整形变量那样有个 atomic_t 的数据结构,原子位操作是直接对内存进行操作

函数原型 功能描述
void set_bit(int nr, void *p) p 地址所指内存的第 nr 位设置为 1
void clear_bit(int nr, void *p) p 地址所指内存的第 nr 位设置为 0
void change_bit(int nr, void *p) p 地址所指内存的第 nr 位取反(翻转)
int test_bit(int nr, void *p) 获取 p 地址所指内存的第 nr 位的当前值
int test_and_set_bit(int nr, void *p) p 地址所指内存的第 nr 位置 1,并保存原值
int test_and_clear_bit(int nr, void *p) p 地址所指内存的第 nr 位清 0,并保存原值
int test_and_change_bit(int nr, void *p) p 地址所指内存的第 nr 位翻转,并保存原值

自旋锁

如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线

程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里"转圈圈"的等待锁可用。

函数原型 功能描述
DEFINE_SPINLOCK(spinlock_t lock) 定义并初始化一个自旋锁变量(通常在声明时使用)。
int spin_lock_init(spinlock_t *lock) 初始化一个自旋锁。
void spin_lock(spinlock_t *lock) 获取指定的自旋锁,如果锁不可用则忙等待(自旋),直到获取锁为止。
void spin_unlock(spinlock_t *lock) 释放指定的自旋锁。
int spin_trylock(spinlock_t *lock) 尝试获取指定的自旋锁,如果锁不可用则立即返回,不会自旋等待。
int spin_is_locked(spinlock_t *lock) 检查指定的自旋锁是否已经被其他线程/进程持有。

自旋锁 API 函数适用于 SMP 或支持抢占的单 CPU 下线程之间的并发访问,也就是用于线程与线程之间,被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数,否则的话会可能会导致死锁现象的发生。

自旋锁会自动禁止抢占,也就说当线程 A得到锁以后会暂时禁止内核抢占。如果线程 A 在持有锁期间进入了休眠状态,那么线程 A 会自动放弃 CPU 使用权。线程 B 开始运行,线程 B 也想要获取锁,但是此时锁被 A 线程持有,而且内核抢占还被禁止了!线程 B 无法被调度出去,那么线程 A 就无法运行,锁也就无法释放,好了,死锁发生了!

在中断里面使用自旋锁之前,一定要禁止本地中断,否则可能锁死,防止"线程持锁 → 被中断打断 → 中断再抢同一把锁"这种自锁死场景。

获取自旋锁并处理中断的函数

函数原型 功能说明
void spin_lock_irq(spinlock_t *lock) 禁止本地中断,然后获取指定的自旋锁。适用于已知中断未被禁止的情况。
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags) 保存当前中断状态到 flags 中,然后禁止本地中断并获取自旋锁。适用于中断可能已经关闭的情况,保证恢复时状态一致。
void spin_unlock_irq(spinlock_t *lock) 释放自旋锁,并重新启用本地中断。适用于之前使用 spin_lock_irq 获取锁的情况。
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) 释放自旋锁,并将中断状态恢复为 flags 所保存的状态。适用于之前使用 spin_lock_irqsave 获取锁的情况。

使用 spin_lock_irq/spin_unlock_irq 的时候需要用户能够确定加锁之前的中断状态,但实际上内核很庞大,运行也是"千变万化",我们是很难确定某个时刻的中断状态,因此不推荐使用spin_lock_irq/spin_unlock_irq。建议使用 spin_lock_irqsave/ spin_unlock_irqrestore,因为这一组函数会保存中断状态,在释放锁的时候会恢复中断状态。一般在线程中使用 spin_lock_irqsave/spin_unlock_irqrestore,在中断中使用 spin_lock/spin_unlock。

下半部(BH)也会竞争共享资源

函数名 行为描述
spin_lock_bh 关闭下半部,获取指定的自旋锁
spin_unlock_bh 释放指定的自旋锁,打开下半部

读写自旋锁

允许多个线程同时读,读的时候无法写,写的时候无法读。只允许一个在写。

读写锁操作 API 函数分为两部分,一个是给读使用的,一个是给写使用的

函数 描述
DEFINE_RWLOCK(rwlock_t lock) 定义并初始化一个读写锁
void rwlock_init(rwlock_t *lock) 初始化读写锁
读锁
void read_lock(rwlock_t *lock) 获取读锁
void read_unlock(rwlock_t *lock) 释放读锁
void read_lock_irq(rwlock_t *lock) 禁止本地中断,并且获取读锁
void read_unlock_irq(rwlock_t *lock) 打开本地中断,并且释放读锁
void read_lock_irqsave(rwlock_t *lock, unsigned long flags) 保存中断状态,禁止本地中断,并获取读锁
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags) 恢复中断状态,激活本地中断,并释放读锁
void read_lock_bh(rwlock_t *lock) 关闭下半部,并获取读锁
void read_unlock_bh(rwlock_t *lock) 打开下半部,并释放读锁
写锁
void write_lock(rwlock_t *lock) 获取写锁
void write_unlock(rwlock_t *lock) 释放写锁
void write_lock_irq(rwlock_t *lock) 禁止本地中断,并且获取写锁
void write_unlock_irq(rwlock_t *lock) 打开本地中断,并且释放写锁
void write_lock_irqsave(rwlock_t *lock, unsigned long flags) 保存中断状态,禁止本地中断,并获取写锁
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags) 恢复中断状态,激活本地中断,并释放写锁
void write_lock_bh(rwlock_t *lock) 关闭下半部,并获取写锁
void write_unlock_bh(rwlock_t *lock) 打开下半部,并释放写锁

顺序锁

在读写自旋锁的基础上,允许写的时候进行读,但是需要注意在写后重新读,保证数据完整性

注意:顺序锁保护的资源不能是指针

函数 描述
DEFINE_SEQLOCK(seqlock_t sl) 定义并初始化顺序锁
void seqlock_init(seqlock_t *sl) 初始化顺序锁
void write_seqlock(seqlock_t *sl) 获取写顺序锁
void write_sequnlock(seqlock_t *sl) 释放写顺序锁
void write_seqlock_irq(seqlock_t *sl) 禁止本地中断,并且获取写顺序锁
void write_sequnlock_irq(seqlock_t *sl) 打开本地中断,并且释放写顺序锁
void write_seqlock_irqsave(seqlock_t *sl, unsigned long flags) 保存中断状态,禁止本地中断,并获取写顺序锁
void write_sequnlock_irqrestore(seqlock_t *sl, unsigned long flags) 恢复中断状态,激活本地中断,释放写顺序锁
void write_seqlock_bh(seqlock_t *sl) 关闭下半部,并获取写顺序锁
void write_sequnlock_bh(seqlock_t *sl) 打开下半部,并释放写顺序锁
unsigned read_seqbegin(const seqlock_t *sl) 读单元访问共享资源时调用,返回顺序号
unsigned read_seqretry(const seqlock_t *sl, unsigned start) 检查在读过程中是否有写操作,如有则需重读
c 复制代码
unsigned int seq;
do {
    seq = read_seqbegin(&lock);

    /* 读取共享数据 */
    a = data->a;
    b = data->b;

} while (read_seqretry(&lock, seq));

信号量

信号量会让获取不到的线程进行睡眠,等到占用的线程退出后,唤醒该线程。

①、因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。

②、因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。

③、如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。

函数名 功能描述
DEFINE_SEMAPHORE(name) 定义一个信号量,并将其初始值设置为 1。
void sema_init(struct semaphore *sem, int val) 初始化指定的信号量 sem,将其值设置为 val
void down(struct semaphore *sem) 获取信号量。如果信号量不可用,则进入不可中断休眠,直到获取成功。不能在中断上下文中使用。
int down_trylock(struct semaphore *sem) 尝试获取信号量。如果成功返回 0;如果失败返回非 0 值,不会休眠。
int down_interruptible(struct semaphore *sem) 获取信号量。如果信号量不可用则进入可中断休眠状态(可被信号打断)。
void up(struct semaphore *sem) 释放信号量,唤醒等待该信号量的任务(如果有)。
c 复制代码
struct semaphore sem;/* 定义信号量*/

sema_init(&sem, 1);/* 初始化信号量 */
down(&sem);        /* 申请信号量*/
/* 临界区 */
up(&sem);          /* 释放信号量*/

耗费时间比自旋锁高:

当线程睡眠时会发生:

  1. 保存当前寄存器状态

  2. 进入调度器

  3. 选择下一个线程

  4. 切换栈

  5. 切换页表

  6. 切换 cache 相关状态

这个叫:上下文切换(context switch)

在 ARM 多核系统上:

一次切换大概是:几微秒级别(甚至更多)

而 spinlock 等待可能只是:几十纳秒~几百纳秒

互斥体

使用 mutex 的时候要注意如下几点:

①、mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。

②、和信号量一样,mutex 保护的临界区可以调用引起阻塞的 API 函数。

③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并

且 mutex 不能递归上锁和解锁。

这里要注意:

mutex 是"线程拥有型锁"

它关心:

  1. 谁持有

  2. 是否优先级继承

  3. 是否睡眠


spinlock 是"CPU 排他锁"

它关心:

  1. 共享数据是否被占用

  2. 不关心线程身份


seqlock 是"版本控制锁"

它关心:

  1. 数据是否被修改

  2. 读根本没有持有概念


函数原型 功能说明
DEFINE_MUTEX(name) 定义并初始化一个名为 name 的 mutex 变量(静态初始化)。
void mutex_init(mutex *lock) 动态初始化一个 mutex 变量。在使用 mutex 前必须调用此函数进行初始化。
void mutex_lock(struct mutex *lock) 获取 mutex 锁。如果锁已被占用,进程进入不可中断睡眠等待。
void mutex_unlock(struct mutex *lock) 释放 mutex 锁。只有持有锁的任务才能调用此函数解锁。
int mutex_trylock(struct mutex *lock) 尝试获取 mutex 锁。如果成功获取,返回 1;否则返回 0,不会睡眠。
int mutex_is_locked(struct mutex *lock) 检查 mutex 是否被当前任务持有(调试用途)。如果被持有,返回 1;否则返回 0。
int mutex_lock_interruptible(struct mutex *lock) 获取 mutex 锁。如果锁被占用,进入可中断睡眠状态,可被信号打断。成功获取锁返回 0,被信号打断返回 -EINTR。
c 复制代码
struct mutex lock;/* 定义一个互斥体 */
mutex_init(&lock);/* 初始化互斥体*/
mutex_lock(&lock);/* 上锁*/
/* 临界区 */
mutex_unlock(&lock);/* 解锁*/
相关推荐
在这habit之下1 小时前
HAProxy学习总结
学习
来两个炸鸡腿1 小时前
【Datawhale组队学习202602】Hello-Agents task06 框架应用开发实战
人工智能·学习·大模型·智能体
盐焗西兰花1 小时前
鸿蒙学习实战之路-STG系列(4/11)-应用选择页功能详解
服务器·学习·harmonyos
Starry_hello world1 小时前
Linux 信号量
linux·运维
再战300年2 小时前
Samba在ubuntu上安装部署
linux·运维·ubuntu
qq_416276422 小时前
通用音频表征的对比学习
学习·音视频
勇闯逆流河2 小时前
【Linux】基础开发工具(软件包、vim)
linux·运维·服务器
岳清源2 小时前
【无标题】Keepalived
linux·服务器·网络
先做个垃圾出来………2 小时前
Python常见文件操作
linux·数据库·python