原子操作
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); /* 释放信号量*/
耗费时间比自旋锁高:
当线程睡眠时会发生:
-
保存当前寄存器状态
-
进入调度器
-
选择下一个线程
-
切换栈
-
切换页表
-
切换 cache 相关状态
这个叫:上下文切换(context switch)
在 ARM 多核系统上:
一次切换大概是:几微秒级别(甚至更多)
而 spinlock 等待可能只是:几十纳秒~几百纳秒
互斥体
使用 mutex 的时候要注意如下几点:
①、mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
②、和信号量一样,mutex 保护的临界区可以调用引起阻塞的 API 函数。
③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并
且 mutex 不能递归上锁和解锁。
这里要注意:
mutex 是"线程拥有型锁"
它关心:
-
谁持有
-
是否优先级继承
-
是否睡眠
spinlock 是"CPU 排他锁"
它关心:
-
共享数据是否被占用
-
不关心线程身份
seqlock 是"版本控制锁"
它关心:
-
数据是否被修改
-
读根本没有持有概念
| 函数原型 | 功能说明 |
|---|---|
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);/* 解锁*/