内核中的并发和竞态-《Android深度探索(卷1):HAL与驱动开发》笔记
原子操作
32位整型原子操作
#include <asm/atomic.h>
atomic_t n;
atomic_set(&n, 2);
atomic_add(&n, 5);
atomic_sub(&n, 1);
printk("n = %d\n", atomic_read(&n));
if (atomic_dec_and_test(&n)) {
// n 的值为0
} else {
// n 的值不为0
}
原子操作
ATOMIC_INIT(int i)//用i初始化atomict类型的变量
int atomic_read(atomic_t*v)//return 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_sub_and_test(int i,atomic_t*v)//v-=i;return v==0;
int atomic_inc_and_test(atomic_t *v)//v+=1;return v==0;
int atomic_dec_and_test(atomic_t*v)//v-=1;return v==0;
int atomic_add_negative(int i, atomic_t *v)//v-=i;return v<0;
int atomic_add_return(int i,atomic_t*v)//v+=i;return v;
int atomic_sub_return(int i,atomic_t*v)//v-=i;return v;
int atomic_inc_return(atomic_t*v)//v+=1;return v;
int atomic_dec_return(atomic_t*v)//v-=1;return v;
int atomic_add_unless(atomic_t*v, int a, int u)//if(v!=u){v+=a;return 非0;}else{return 0;}
int atomic_inc_not_zero(atomic_t*v)//if(v!=0){v+=1;return 非0;}else{return 0;}
64位整型原子操作
//注意,为了兼容性,尽量使用32位整形原子操作
atomic64_t n;
atomic64_set(&n, 2);
atomic64_add(&n, 5);
atomic64_sub(&n, 1);
printk("n = %lld\n", atomic64_read(&n));
原子操作
ATOMIC64_INIT(int i)//用i初始化atomict类型的变量
int atomic64_read(atomic_t*v)//return v
void atomic64_set(atomic_t *v, int i)//v=i
void atomic64_add(int i,atomic_t*v)//v+=i
void atomic64_sub(int i, atomic_t*v)//v-=i
void atomic64_inc(atomic_t*v)//v+=1
void atomic64_dec(atomic_t*v)//v-=1
int atomic64_sub_and_test(int i. atomic_t *v)//v-=i;return v==0
int atomic64_inc_and_test(atomic_t*v)//v++;return v==0
int atomic64_dec_and_test(atomic_t*v)//v--;return v==0
int atomic64_add_negative(int i, atomic_t*v)//v-=i;return v<0;
int atomic64_add_return(int i, atomic_t*v)//v+=i;return v;
int atomic64_sub_return(int i, atomic_t*v)//v-=i;return v;
int atomic64_inc_return(atomic_t*v)//v++;return v;
int atomic64_dec_return(atomic_t*v)//v--;reutrn v;
int atomic64_add_unless(atomic_t*v, int a, int u)//if(v!=u){v+=a;return 非0;}else{return 0;}
int atomic64_inc_not_zero(atomic_t*v)//if(v!=0){v++;return 非0;}else{return 0}
位原子操作
#include <linux/bitops.h>
unsigned long value = 0;
set_bit(0, &value); // 将第0位设置为1
// 清除第3位
clear_bit(3, &flags);
// 测试并设置第3位
test_and_set_bit(3, &flags);
// 测试并清除第3位
test_and_clear_bit(3, &flags);
// 测试第3位
int bit_value = test_bit(3, &flags);
void change_bit(int m, void *addr)//第m位取反
int test_and_set_bit(int m, void *addr)//将addr的第m位设为1,如果设置之前第m位为1,则返回非0值,否则返回0
int test_and_clear_bit(int m, void *addr)//将addr的第m位设为0,如果设置之前第m位为1,则返国非0值,否则返国0
int test_and_change_bit(int m, void *addr)//如果addr的第m位为0,则设为!,否测设为0。如果设置之前第m位为1,则返回非0值,否则返回0
int test_bit(int m, void *addr)//如果addr变量的第m位为1,则返回非0值,否则返回0
自旋锁
自旋锁(Spin Lock),当一个线程或进程尝试获取已经被占用的自旋锁时,会进入一个循环(自旋),不断检查自旋锁是否被释放,直到成功获取自旋锁为止。
自旋锁的基本用法
定义自旋锁变量:
在使用自旋锁之前,需要定义一个自旋锁变量(spinlock_t)。可以通过以下两种方式定义:
//直接定义:
spinlock_t my_lock;
//使用 DEFINE_SPINLOCK 宏定义:
DEFINE_SPINLOCK(my_lock);
//如果直接定义了自旋锁变量,需要使用 spin_lock_init 宏对其进行初始化:
spin_lock_init(&my_lock);
//获取自旋锁:
spin_lock(&my_lock);
//如果自旋锁已经被占用,spin_lock 函数会阻塞,直到自旋锁被释放。
//如果希望在获取自旋锁时立即返回,无论是否成功,可以使用 spin_trylock 函数:
if (spin_trylock(&my_lock)) {
// 成功获取自旋锁
} else {
// 未获取自旋锁
}
//释放自旋锁:
spin_unlock(&my_lock);
//一般而已的自旋锁的标准使用代码
spin_lock(&my_lock);
// 临界区代码
spin_unlock(&my_lock);
衍生机制
自旋锁主要用于对称多处理器(SMP)系统或单CPU但内核可抢占的系统。在这些系统中,自旋锁可以防止临界区被其他CPU或本CPU内的抢占进程打断。然而,在执行临界区代码时,仍然可能受到中断和底半部(BH)的影响。为了解决这个问题,可以使用自旋锁的衍生机制,如 spin_lock_irq、spin_lock_bh 等。
使用注意事项
忙等锁:自旋锁是一种忙等锁,当锁被占用时,尝试获取自旋锁的CPU会不断循环检查锁的状态,直到锁被释放。因此,只有在占用锁时间极短的情况下,使用自旋锁才是合理的。
死锁风险:不恰当地使用自旋锁可能导致系统死锁。例如,递归使用自旋锁(即在未释放自旋锁之前再次获取自旋锁)会导致死锁。此外,在自旋锁占用期间调用可能引起阻塞的函数(如 copy_from_user、copy_to_user、kmalloc 等)也可能导致死锁。
与中断和底半部相关的接口
DEFINE_SPINLOCK(lock)//定义和初始化自旋锁变量
void spin_lock_init(spinloek_t *lock)//初始化自旋锁变量
void spin_lack(spinlock_t *lock)//获取自旋锁。如果无法获取自旋锁,则不断自旋(循环)来检测自旋镜是杏可用
int spin_trylock(spinlock_t *lock)//获取自旅锁。如果成功获取自旋锁,则立刻近回非0值,如果无法获取自旋镇,则立刻返回0(并不进行自旋,也就是说该函数不会被阻寨)
void spin_unlock(spinlock_t *lock)//释放自旋锁
void spin_lock_irq(spinlock_t *lock)//获取自旋锁,并禁止中断。相当于spinlock+local irq disable
int spin_tryloek_irq(spinlock_t *lock)//获取自旋锁,并禁止中断。如果成功获取获取自旋锁,立刻返回非0值,否购立刻返回0。相当于spintryiock+local irq disabie
void spin_unlock_irq(spinlock t *lock)//释放自旋锁,并允许中断。相当于spin_unlock+local irq enable
void spin_lock_bh(spinlock t *lock)//获取自旋锁,并关闭底半部。相当于spin_lock+locai bh disable
void spin_trylock_bh(spinlock t *lock)//获取自旋锁,并关闭底半部。如果成功获取自旋锁,立到返回丰0值,否则立刻返回0。相当于spin_trylock+local bh disable
void spin_unlock_bh(spinlock t *lock)//释放自旋锁,并打开底半部。相当于spinunlock+locai bh enabie
int spin_is_locked(spinloek t *lock)//如果自旋锁已被占用,返回非0值,否则返回0
读写自旋锁
读写自旋锁(Read-Write Spinlock)是一种用于并发编程的同步机制,旨在提高对共享资源的并发访问效率。与普通自旋锁不同,读写自旋锁区分了读操作和写操作,允许多个读操作同时进行,但只允许一个写操作进行。这种机制在读操作频繁而写操作较少的情况下,能够显著提高并发性能。
读锁:允许多个执行单元同时获取读锁,只要没有写锁被持有。
写锁:只允许一个执行单元获取写锁,且在写锁被持有期间,不允许任何读锁或写锁被获取。
这种机制确保了读操作的并发性,同时保证了写操作的独占性,从而提高了并发访问共享资源的效率。
使用
// 直接定义读写自旋锁变量
rwlock_t my_rwlock;
// 使用宏定义读写自旋锁变量
DEFINE_RWLOCK(my_rwlock);
//如果直接定义了读写自旋锁变量,需要使用rwlock_init宏对其进行初始化。
rwlock_init(&my_rwlock);
// 获取读自旋锁
read_lock(&my_rwlock);
// 获取写自旋锁
write_lock(&my_rwlock);
//如果成功获取读写自旋锁,read_lock和write_lock宏会立即返回。如果未获取读写自旋锁,read_lock和write_lock宏会被阻塞(自旋),直到可以获取读写自旋锁才返回。
//如果希望不管是否成功获取读写自旋锁都立刻返回,可以使用read_trylock和write_trylock宏。
// 尝试获取读自旋锁
if (read_trylock(&my_rwlock)) {
// 成功获取读自旋锁
} else {
// 未获取读自旋锁
}
// 尝试获取写自旋锁
if (write_trylock(&my_rwlock)) {
// 成功获取写自旋锁
} else {
// 未获取写自旋锁
}
//如果read_trylock和write_trylock宏成功获取读写自旋锁,会立即返回非0值;如果未获取读写自旋锁,会立即返回0。
//不能同时申请读自旋锁和写自旋锁,否则会造成死锁。
void f(){
read_lock(&my_rwlock);
write_lock(&my_rwlock); // 死锁
}
// 释放读自旋锁
read_unlock(&my_rwlock);
// 释放写自旋锁
write_unlock(&my_rwlock);
使用读自旋锁的标准代码:
{
read_lock(&my_rwlock);
// 读临界区代码
read_unlock(&my_rwlock);
}
{
write_lock(&my_rwlock);
// 写临界区代码
write_unlock(&my_rwlock);
}
读写自旋锁与中断、底半部相关
读写自旋锁与普通自旋锁类似,也有很多与中断、底半部相关的宏,用于在特定场景下使用读写自旋锁。(底半部资料:https://zhuanlan.zhihu.com/p/686890291)
读写自旋锁操作
DEFINE_RWLOCK(lock)//定义和初始化读写自旋锁变量
void rwlock_init(rwlock t*lock)//初始化读写自旋锁变量
void read_lock(rwlock t *lock)//获取读自旋锁。如果无法获取读自旋锁,则不断自旋(循环)来检测读自旋锁是否可用
int read_trylock(rwlock t *lock)//获取谈自旋锁。如果成功获取读自旋锁,则立刻返回非0值,如果无法获取读自旋镇,则立刻返回0(并不进行自旋,也就是说该宏不会被阻塞)
void read_unlock(rwlock t *lock)//释放读自旋锁
void write_lock(rwlock t lock)//获取写自旋锁。如果无法获写读自旋锁。则不断自旋来检测写自旋锁是否可用
int write_trylock(rwlock t *lock)//获取写自旋锁。如果成功获取写自旋锁,则立刻返回非0值,如果无法获取写自旋锁,则立刻返回0(并不进行自旋,也就是说该宏不会被阻寒)
void write_unlock(rwlock t *lock)//释放写自旋锁
void read_lock_irq(rwlock t *lock)//获取读自旋锁,并禁止中断。相当于read_lock+local_irq_disable
void read_unlock_irq(rwlock t *lock)//释放读自旋锁,并允许中断。相当于read_unlock+local_irq_enable
void write_lock_irq(rwlock t *lock)//获取写自旋锁,并禁止中断。相当于write_lock+local_irq_disable
void write_unlock_irq(rwlock t *lock)//释放写自旋锁,并允许中断。相当于write_unlock+local_irq_enable
void read_lock_bh(rwlock t *lock)//释放设自旋锁,并禁止底半部。相当于rcad_lock+local_bh_disabic
void read_unlock_bh(rwlock t *lock)//彩放读自旋锁,并允许底半部。相当于rcad_lock+local_bh__enabie
void write_lock_bh(rwlock t *lock)//释放写自旋锁,并禁止底半部,相当于write_lock+local_bh_disable
void write_unlock_bh(rwlock t *lock)//释放写自旋锁,并允许底半部,相当于write_lock+local_bh_enablc
顺序锁(seqlock)
概述
顺序锁(seqlock)是一种用于多线程环境中的同步机制,特别适用于读多写少的场景。与读写自旋锁(rwlock)类似,但顺序锁为写锁赋予了更高的优先级。这意味着在获取读锁时,写锁仍然可以获取并继续执行写临界区的代码,而不会被读锁阻塞。
工作原理
seqlock_t 结构体包含一个自旋锁和一个序列号(sequence number)。序列号用于跟踪锁的状态,特别是在写操作期间。
写锁操作:当获取写锁(write_seqlock)时,序列号会加1。当释放写锁(write_sequnlock)时,序列号再次加1。
因此,在写锁未释放时,序列号的值是奇数;释放后,序列号的值是偶数。
读锁操作读取共享资源时,使用 read_seqbegin 和 read_seqretry 函数。
read_seqbegin 获取当前的序列号,并开始执行读临界区代码。
read_seqretry 用于检查在读临界区执行期间序列号是否发生变化。如果发生变化(即有写操作发生),则返回非0值,循环继续执行读操作,直到序列号不再变化。
优缺点
优点:读共享数据期间可以写共享数据,提高了并发性能。写锁不会被读锁阻塞,写操作优先级高。
缺点:如果在读共享数据的过程中发生了写操作,系统会不断循环等待和重新执行读临界区的代码,可能导致性能下降。
顺序锁的使用示例
以下是一个使用顺序锁的示例代码,展示了如何在读共享资源的情况下写共享资源:
do {
seq = read_seqbegin(&my_seqlock);
//读临界区代码
} while (read_seqretry(&my_seqlock, seq));
//----------------------------------------
write_seqlock(&my_seqlock);
//写临界区代码
write_sequnlock(&my_seqlock);
顺序锁相关函数和宏
DEFINE_SEQLOCK(lock)//定义和初始化顺序锁变量
void seqlock_init(seqlock_t *lock)//初始化顺序锈变量
void write_seqlack(seqlock_t *lock)//获取顺序锁
int write_tryseqlock(seqlock_t *lock)//获取顺序锁。如果成功获取顺序锁,函数立刻返回非0值,否则立刻返回0
write_seqlock_irqsave(lock, flags)//local_irq_save + write_seqlock
write_seqlock_irq(lock)//local_irq_disable + write_seqloek
write_seqlock_bh(lock)//local_bh_disable + write_seqlock
void write_sequnlock(seqlock t *loek)//释放顺序锁
write_sequnlock_irqrestore(lock, flags)//write_sequnlock + local_irq_restore
write_sequnlock_irq(lock)//write_sequnlock + local_irq_enable
write_sequnlock_bh(lock)//write_sequnlock + local_bh_enable
unsigned read_seqbegin(const seqlock_t * lock)//获取顺序锁的顾序号。如果lock指定的序锁未被释放,该函数会等待顺序锁释放后,才获取顺序锁的顺序号
read_seqbegin_irqsave(lock, flags)//local_irq_save + read_seqbegin
int read_seqretry(const seqlock_t *lock, unsigned iv)//检测read seqbcgin函数获取的顺序号是否与执行完读临界区的顺序号一致。如果不一致,返回非0值,否则返回0
read_seqretry_irqrestore(lock, iv, flags)//read seqretry + local irq restore
读—复制—更新(RCU)机制
采用锁机制实现数据访问的一致性存在如下两个问题。
- 效率问题。锁机制的实现需要对内存的原子化访问,这种访问操作会破坏流水线操作,降低流水线效率,这是影响性能的一个因素。另外,在采用读写锁机制的情况下,写锁是排他锁,无法实现写锁与读锁的并发操作,在某些应用下会降低性能。
- 扩展性问题。例如,当系统中CPU数量增多的时候,采用锁机制实现数据的同步访问效率偏低,并且随着CPU数量的增多,效率会越来越低,由此可见锁机制的扩展性比较差。
为了解决上述问题,Linux内核引进了RCU机制。该机制在多CPU的平台上比较适用,对于读多写少的应用尤其适用。
RCU(Read-Copy-Update,读—复制—更新)机制是一种用于多线程环境下访问共享资源的同步机制。与传统的读写锁(rwlock)不同,RCU允许读操作和写操作并发执行,从而提高了系统的并发性和性能。
原理
RCU机制的核心思想是通过延迟写操作的可见性来实现读写并发。具体来说,RCU机制包括以下几个步骤:
读操作
读操作可以直接访问共享资源,但读操作的上下文是不可抢占的。读操作使用rcu_read_lock函数来禁止抢占,以确保读操作的连续性。
写操作
写操作在修改共享资源之前,首先复制一份旧数据,然后在副本上进行修改。修改完成后,使用rcu_assign_pointer宏将指针从旧数据更新到新数据。更新指针后,后台的垃圾回收线程会在所有读操作完成后回收旧数据。
垃圾回收
RCU机制中有一个垃圾回收的后台线程,负责在所有读操作完成后回收旧数据。写操作在更新指针后需要等待一个“grace period”,即所有读操作都完成的时间段,然后才能安全地回收旧数据。
性能
从这一点上也可以将RCU看做是rwlock的升级版。因为RCU可以读写同时进行,而rwlock机制的读写是互斥的。
RCU机制的优点在于它允许读操作和写操作并发执行,从而提高了系统的并发性和性能。然而,RCU机制也有一些限制,例如写操作之间不能并发执行,且读操作的上下文必须是不可抢占的。
图片来自《Android深度探索(卷1):HAL与驱动开发》
RCU API
//读锁定
static inline void rcu_read_lock (void)
static inline void rcu_read_lock_bh(void)
//读解锁
static inline void rcu_read_unlock(void)
static inline void rcu_read_unlock_bh(void)
使用RCU机制读模式的标准代码如下:
rcu_read_lock();
//读临界区的代码
rcu_read_unlock();
//其中rcu read lock和rcu read unlock函数实际上只是禁止和打开内核的进程抢占(为防止在执行临界区代码时被其他进程打扰),从这两个函数的源代码就可以看出这一点。
statie inline void rcu_read_lock(void)
{
_rcu_read_lock();//关闭进程抢占
_acquire(RCU);
rcu_read_acquire();
}
statie inline void rcu_read_unlock(void)
{
rcu_read_release();
_release (RCU)
_rcu_read_unlock();// 打开进程抢占
}
//其中_rcu_read_lock和rcu_read_unlock函数分别用于关闭和打开进程抢占,代码如下:
static inline void _rcu_read_lock(void)
{
preempt_disable();// 关闭进程抢占
}
static inline void _rcu_read_unlock(void){
preempt_enable();// 打开进程抢占
}
//其变种rcu_read_lock_bh和rcu_read_unlock_bh函数也使用了类似的实现方法。这两个函数的实现代码如下:
statie inline void rcu_read_iock_bh(void){
rcu_read_lock_bh();//禁止软中断
_acquire(RCU_BH);
rcu_read_acquire_bh();
}
static inline void rcu_read_unlock_bh(void){
rcu_read_release_bh();
_release(RCU_BH);
_rcu_read_unlock_bh();// 打开软中断
}
// rcu read lock bh和 rcu read unlock bh函数的代码如下:
static inline void rcu_read_lock_bh (void){
local_bh_disable ();// 禁止软中断
}
static iniine void rcu_read_unlock_bh(void){
local_bh_enabie ();// 打开软中断
}
同步RCU
synchronize_rcu宏,由RCU写线程调用
当然可以,以下是使用 Markdown 格式的回答:
3. 同步RCU
synchronize_rcu
宏由 RCU 写线程调用,它会阻塞写线程,直到经过一个 grace period 后,即所有的读进程已经执行完读临界区的代码并退出,写进程才可以继续下一步操作。如果有多个 RCU 写进程调用该宏,它们将在一个 grace period 之后全部被唤醒(至于哪个写线程会执行,由它们采用的锁机制决定)。
在 Linux 2.6.11 及以前的 2.6 内核版本中,synchronize_rcu
宏被称为 synchronize_kernel
。从 Linux 2.6.12 开始,它被更名为 synchronize_rcu
。在 Linux 2.6.12 中,仍然提供了 synchronize_kernel
和一个新的函数 synchronize_sched
。这是因为以前有很多 Linux 内核开发者使用 synchronize_kernel
用于等待所有 CPU 都退出不可抢占区,而在 RCU 设计时,synchronize_kernel
只是用于等待所有 CPU 都退出读临界区。因此,synchronize_kernel
可能会随着 RCU 实现代码的修改而发生语义变化。为了防止这种情况发生,在新的修改中增加了专门的用于其他内核用户的 synchronize_sched
函数(实际上,synchronize_rcu
宏定义的就是 synchronize_sched
函数)。而 synchronize_kernel
之所以存在是为了保证代码兼容性。
4. 挂接回调函数
call_rcu
函数也由 RCU 写线程调用,该函数不会使写线程阻塞,因此可以在中断上下文或软中断(softirq)中使用。而 synchronize_rcu
、synchronize_kernel
和 synchronize_sched
只能在进程上下文使用。call_rcu
函数将把函数 func
挂接到 RCU 回调函数链上,然后立即返回。一旦所有的 CPU 都已经完成读临界区操作,func
函数将被调用来释放旧数据。head
参数用于记录回调函数 func
,一般该结构会作为被 RCU 保护的数据结构的一个字段,以便省去单独为该结构分配内存的操作。
call_rcu_bh
函数的功能几乎与 call_rcu
函数完全相同,唯一的差别是它把软中断(softirq)的完成也当做经历一个 quiescent state。因此,如果写进程使用了该函数,在进程上下文的读进程必须使用 rcu_read_lock_bh
。
链表操作的 RCU 版本
除了上面的函数和宏,RCU 还增加了链表操作的 RCU 版本,因为对于 RCU,对共享数据的操作必须保证能够被没有使用同步机制的读者看到,所以内存栅是非常必要的。
-
list_add_rcu
: 该函数把链表项new
插入到 RCU 保护的链表head
的开头。使用内存栅保证了在引用这个新插入的链表项之前,新链表项的链接指针的修改对所有读线程是可见的。 -
list_add_tail_rcu
: 该函数类似于list_add_rcu
,它将把新的链表项new
添加到被 RCU 保护的链表的末尾。 -
list_del_rcu
: 该函数从 RCU 保护的链表中移除指定的链表项entry
,并且把entry
的prev
指针设置为LIST_POISON2
(内存地址为0x00200200
的指针),但是并没有把entry
的next
指针设置为LIST_POISON1
,因为该指针可能仍然在被读进程用于遍历该链表。 -
list_replace_rcu
: 该函数使用新的链表项new
取代旧的链表项old
,内存栅保证在引用新的链表项之前,它的链接指针的修正对所有读进程可见。 -
list_for_each_entry_rcu
: 该宏用于遍历指定类型的数据结构链表,当前链表项pos
为一包含struct list_head
结构的特定的数据结构。 -
list_for_each_entry_continue_rcu
: 该宏用于在退出点之后继续遍历由 RCU 保护的链表head
。 -
hlist_del_rcu
: 该函数从由 RCU 保护的哈希链表中移除链表项n
,并设置n
的ppre
指针为LIST_POISON2
,但并未设置next
为LIST_POISON1
。 -
hlist_add_head_rcu
: 该函数用于把链表项n
插入到被 RCU 保护的哈希链表的开头,但同时允许读进程对该哈希链表的遍历。内存栅确保在引用新链表项之前,它的指针修正对所有读者可见。 -
hlist_for_each_entry_rcu
: 该宏用于遍历指定类型的数据结构哈希链表,当前链表项pos
为一包含struct list_head
结构的特定的数据结构。
RCU 的应用
本节将给出 Linux 内核中使用 RCU 的相关代码示例,并提供了非 RCU 和 RCU 版本,读者可以比较它们的不同,从而更好地理解 RCU 机制。
1. 使用 rwlock
机制的读端代码
read_lock(&my_lock);
list_for_each_entry(pos, head, member) {
// 读操作
}
read_unlock(&my_lock);
将上面的代码改成 RCU 机制后会变成如下形式:
rcu_read_lock();
list_for_each_entry_rcu(pos, head, member) {
// 读操作
}
rcu_read_unlock();
这种转换非常直接,使用 rcu_read_lock
、rcu_read_unlock
和 list_for_each_entry_rcu
分别替换 read_lock
、read_unlock
和 list_for_each_entry
即可。
2. 使用 rwlock
机制的写端代码
write_lock(&my_lock);
list_del(&entry->list);
write_unlock(&my_lock);
kfree(entry);
将上面的代码改成 RCU 机制可以得到如下代码:
list_del_rcu(&entry->list);
call_rcu(&entry->rcu, my_rcu_callback);
对于链表删除操作,list_del
替换为 list_del_rcu
和 call_rcu
,这是因为被删除的链表项可能还在被别的读进程引用,所以不能立即删除,必须等到所有读进程经历一个 quiescent state 才可以删除。my_rcu_callback
是一个回调函数,在所有读进程都完成对 entry
的引用后,该回调函数会被调用,用于释放 entry
。
RCU相比读写锁
在传统的读写锁(rwlock)机制中,读端代码通常如下所示:
static enum audit_state audit_filter_task(struct task_struct *tsk)
{
struct audit_entry *e;
enum audit_state state;
read_lock(&auditsc_lock);
list_for_each_entry(node, &audit_tsklist, list) {
if(audit_filter_rules(tsk,&e->rule,NULL,&state)){
read_unlock(&auditsc_lock);
return state;
}
}
read_unlock(&auditsc_lock);
return AUDIT_BUILD_CONTEXT;
}
- 使用 RCU 机制的读端代码
将上述代码转换为使用 RCU 机制后,代码如下:
static enum audit_state audit_filter_task(struct task_struct *tsk)
{
struct audit_entry *e;
enum audit_state state;
rcu_read_lock();
list_for_each_entry(node, &audit_tsklist, list) {
if(audit_filter_rules(tsk,&e->rule,NULL,&state)){
rcu_read_lock();
return state;
}
}
rcu_read_lock();
return AUDIT_BUILD_CONTEXT;
}
- 使用 rwlock 机制的写端代码
在传统的读写锁(rwlock)机制中,写端代码通常如下所示:
//删除rule
static inline int audit_del_rule(struct audit_rule *rule,struct list_head *list)
{
struct audit_entry *e;
write_lock(&auditsc_lock);
list_for_each_entry(e,list,list)
{
if(!audit_compare_rule(rule,&e->rule))
{
list_del(&e->list);
write_unlock(&auditsc_lock);
return 0;
}
}
write_unlock(&auditsc_lock);
return -EFAULT;
}
//添加rule
static inline int audit_add_rule(struct audit_entry *entry,struct list_head *list)
{
write_lock(&auditsc_lock);
if(entry->rule.flags & AUDIT_PREPEND){
entry->rule.flags&=~AUDIT_PREPEND;
list_add(&entry->list,list);
}else{
list_add_tail(&entry->list,list);
}
write_unlock(&auditsc_lock);
return 0;
}
使用rcu的代码如下
static inline int audit_del_rule(struct audit_rule *rule,struct list head* list)
{
struct audit_entry *e;
//do not use the _rcu iterator here,since this is the only deletion routine
list_for_entry_rcu(e,list,list){
if(!audit_compare_rule(rule,&e->rule)){
list_del_rcu(&e->list);
call_rcu(&e->rcu,audit_free_rule,e);
return 0;
}
}
return -EFAULT;
}
static inline int audit_add_rule(struct audit_entry *entry,struct list_head *list)
{
if(entry->rule.flags & AUDIT_PREPEND){
entry->rule.flags&=~AUDIT_PREPEND;
list_add_rcu(&entry->list,list);
}else{
list_add_tail_rcu(&entry->list,list);
}
return 0;
}
对于链表删除操作,list_del替换为list_del_rcu和call_rcu,这是因为被删除的链表项可能还在被别的读进程引用,所以不能立即删除,必须等到所有读进程经历一个quiescent state才可以删除。
信号量(Semaphore)和读写信号量
信号量是用于保护临界区的一种常用方法,它的使用方式与自旋锁类似。与自旋锁相同,只有得到信号量的进程才能执行临界区代码。但与自旋锁不同的是,在未获取信号量时,进程不会像自旋锁一样原地打转,而是进入休眠等待状态。因此当信号量阻塞时消耗的系统资源(主要是CPU资源)并不多,也不会出现死机的现象。
API总结如下
函数/宏 | 功能 | 适用场景 |
---|---|---|
sema_init |
初始化信号量,设置初始值。 | 初始化信号量 |
down |
阻塞方式获取信号量,若失败则休眠。 | 不能用于中断上下文 |
down_interruptible |
类似down ,但可被中断信号打断。返回0表示成功,非0表示被中断。 |
可以处理中断的场景 |
down_trylock |
非阻塞方式尝试获取信号量,立即返回,成功返回0,失败返回非0。 | 中断上下文或无需阻塞的场景 |
up |
释放信号量,唤醒等待队列中的进程。 | 用于释放信号量 |
读写信号量 API
函数/宏 | 功能 | 适用场景 |
---|---|---|
init_rwsem |
初始化读写信号量。 | 初始化读写信号量 |
down_read |
阻塞方式获取读信号量,若失败则休眠。 | 多读场景 |
down_read_trylock |
非阻塞方式尝试获取读信号量,立即返回,成功返回1,失败返回0。 | 不需阻塞的多读场景 |
up_read |
释放读信号量。 | 释放读信号量 |
down_write |
阻塞方式获取写信号量,若失败则休眠。 | 独占写入场景 |
down_write_trylock |
非阻塞方式尝试获取写信号量,立即返回,成功返回1,失败返回0。 | 不需阻塞的写入场景 |
up_write |
释放写信号量。 | 释放写信号量 |
互斥体(Mutex)
顾名思义,不过多赘述
功能 | API | 描述 | 示例代码 |
---|---|---|---|
定义互斥体 | struct mutex my_mutex; |
定义一个互斥体变量。 | struct mutex my_mutex; |
DEFINE_MUTEX(my_mutex); |
定义并静态初始化一个互斥体。 | DEFINE_MUTEX(my_mutex); |
|
初始化互斥体 | mutex_init(&mutex); |
对已定义的互斥体变量进行动态初始化。 | mutex_init(&my_mutex); |
获取互斥体 | mutex_lock(&mutex); |
阻塞式获取互斥体,不可被中断打断。 | mutex_lock(&my_mutex); |
mutex_lock_interruptible(&mutex); |
阻塞式获取互斥体,可被中断打断,若中断则返回非 0。 | if (mutex_lock_interruptible(&my_mutex)) { /* 处理错误 */ } |
|
mutex_trylock(&mutex); |
尝试获取互斥体,不会阻塞,若成功返回 1,失败返回 0。 | if (mutex_trylock(&my_mutex)) { /* 成功获取 */ } |
|
释放互斥体 | mutex_unlock(&mutex); |
释放互斥体,让其他线程可以获取该互斥体。 | mutex_unlock(&my_mutex); |
完成量
看起来是个简易的生产者消费者模型
函数名 | 描述 |
---|---|
DECLARE_COMPLETION(name) |
定义并初始化一个完成量结构体变量。 |
init_completion(struct completion *x) |
初始化一个已经定义的完成量结构体变量。 |
wait_for_completion(struct completion *x) |
等待一个完成量,如果completion.done 为0,则阻塞等待。 |
wait_for_completion_interruptible(struct completion *x) |
可中断地等待一个完成量,如果completion.done 为0,则阻塞等待,可以被信号中断。 |
complete(struct completion *x) |
唤醒一个等待的执行单元,将completion.done 加1。 |
complete_all(struct completion *x) |
唤醒所有等待同一完成量的执行单元,将completion.done 设为int的最大值。 |
总结
在现代Linux内核中,并发和竞态条件仍然是普遍存在的问题。为了解决这些问题,内核提供了多种同步机制,包括中断屏蔽、原子操作、自旋锁、信号量、互斥体等。这些机制各有优缺点,适用于不同的场景。
1. 中断屏蔽
中断屏蔽通常不单独使用,而是与其他同步机制结合使用,以确保在关键代码段执行期间不会被中断打断。中断屏蔽在处理硬件中断或需要严格控制执行顺序的场景中非常有用。
2. 原子操作
原子操作主要用于对整数和位进行操作,确保这些操作在多核系统中是不可分割的。原子操作的优点是开销小,但它们只能处理简单的操作,无法保护复杂的临界区。
3. 自旋锁
自旋锁是一种轻量级的同步机制,适用于临界区非常短且不会阻塞的情况。自旋锁的特点是线程在获取锁失败时会不断尝试(自旋),直到成功获取锁。由于自旋锁会导致CPU资源浪费,因此它不适合用于需要长时间持有锁或可能阻塞的场景。
4. 信号量
信号量是一种更通用的同步机制,允许临界区阻塞。信号量适用于较大的临界区,或者在临界区内可能发生阻塞操作的情况。信号量的缺点是开销较大,尤其是在高并发场景下。
5. 互斥体
互斥体(mutex)是一种特殊的信号量,用于保护共享资源。互斥体通常用于需要独占访问的场景,确保同一时间只有一个线程可以进入临界区。互斥体在临界区内允许阻塞,因此适用于需要长时间持有锁的场景。
6. 读写锁
读写锁(读写自旋锁和读写信号量)是自旋锁和信号量的变种,允许多个读操作同时进行,但写操作需要独占访问。读写锁适用于读操作频繁而写操作较少的场景,可以提高并发性能。
7. 完成量
完成量(completion)是一种用于同步的机制,通常用于确保某个任务完成后才能继续执行后续操作。完成量在需要严格控制代码执行顺序的场景中非常有用。
8. RCU(Read-Copy-Update)
RCU是一种高效的同步机制,适用于读操作频繁而写操作较少的场景。RCU允许多个读操作同时进行,写操作通过延迟更新的方式进行,从而减少锁的开销。RCU在现代Linux内核中得到了广泛应用。
总结
在选择同步机制时,需要根据具体的应用场景和需求来决定。自旋锁适用于短临界区且不会阻塞的情况,信号量和互斥体适用于较大的临界区或可能阻塞的场景。读写锁和RCU适用于读操作频繁的场景,而完成量则用于控制代码执行顺序。随着技术的发展,Linux内核不断引入新的同步机制,以更好地应对复杂的并发场景。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了