上一章,我们了解了自旋锁的相关接口与实现,下面我们来看一下基于自旋锁的衍生锁! 衍生锁种类比较多,我们本篇主要起引导作用,不详细介绍其内部实现!
自旋锁主要用来解决SMP
和调度引发的竞态问题,但是普通的自旋锁并不关心临界区在执行什么操作,对读和写都一视同仁,这样就会存在一些弊端!
我们知道,临界区如果没有被修改,同时读临界区资源是没有问题的,这也就是普通自旋锁的不足之处。
基于上述的弊端,伟大的工程师们,基于自旋锁逐渐就衍生出了一些效率更高的锁,比如:读写自旋锁,顺序自旋锁,RCU
等,下面我们一一介绍。
读写自旋锁主要解决自旋锁的同时读的问题,即:
说到底,读写自旋锁就是允许了 读的并发!
image-20230806174653416
读写自旋锁API
如下:
/* 定义和初始化自旋锁 */
rwlock_t my_rwlock;
rwlock_init(&my_rwlock); /* 动态初始化 */
/* 读锁定 */
void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);
/* 读解锁 */
void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);
/* 写锁定 */
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock);
/* 写解锁 */
void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);
其读写锁的实现不难分析,大家可自行查看! 下面主要分析一下其核心实现的思想:
读写锁思想:
spin_lock
、spin_unlock
,并操作同一把锁spinlock_t
,这样无论是读还是写,都是判断其next
下一个互斥锁的线程索引和owner
当前锁的线程索引是否一致,来实现的,这样读和写操作就冲突了。read_lock
、read_unlock
、write_lock
、write_unlock
两套接口,这样也就使得读操作可以并发read_lock
,rwlock_t
定义的value
值加1,每执行一次read_unlock
,value
值减1,因此读操作不存在等待的情况;value
值是否为0,不为0,说明有其他线程占用锁;如果为0,则说明未被占用;然后每执行一次write_lock
,value
最高位置1,每执行一次write_unlock
,value
清0读写锁在自旋锁的基础之上,允许了读的并发,但是读操作和写操作之间还是互斥的,顺序锁就优化了这种问题:
说到底,顺序自旋锁就是允许了 读和写之间 的并发!
image-20230806174727899
顺序锁的API
如下:
/* 定义和初始化顺序锁 */
seqlock_t my_lock;
seqlock_init(&my_lock); /* 动态初始化 */
// 读操作
unsigned read_seqbegin(const seqlock_t *sl);
read_seqbegin_irqsave(lock, flags)
// 重新读
int read_seqretry(const seqlock_t *sl, unsigned iv);
read_seqretry_irqrestore(lock, iv, flags)
// 写锁定
void write_seqlock(seqlock_t *sl);
int write_tryseqlock(seqlock_t *sl);
write_seqlock_irqsave(lock, flags)
write_seqlock_irq(lock)
write_seqlock_bh(lock)
// 写解锁
void write_sequnlock(seqlock_t *sl);
write_sequnlock_irqrestore(lock, flags)
write_sequnlock_irq(lock)
write_sequnlock_bh(lock)
顺序锁思想:对于顺序锁而言,尽管读写之间不互相排斥,但是如果读执行单元在读操作期间,写执行单元已经发生了写操作,那么,读执行单元必须重新读取数据,以便确保得到的数据是完整的。
sequence
的常量write_seqlock
时,将sequence
加1,调用write_sequnlock
时,将sequence
减1sequence
常量的值,然后调用read_seqretry
与上一次的sequence
值比较,看是否相同,来检测是否需要重新读一次。一个使用顺序锁的示例如下:
读执行单元在访问完被顺序锁
s1
保护的共享资源后需要调用该函数来检查,在读访问期间是否有写操作。如果有写操作,读执行单元就需要重新进行读操作。
// thread 1
do {
seqnum = read_seqbegin(&seqlock_a);
/* 读操作代码块*/
...
} while (read_seqretry(&seqlock_a, seqnum));
// thread 2
write_seqlock(&seqlock_a);
...
/* 写操作代码块*/
write_sequnlock(&seqlock_a);
Linux
衍生锁最难的部分也就是RCU
了,RCU(Read-Copy-Update)
也叫读-复制-更新。
RCU
并不是新的锁机制,在Linux中是在开发内核2.5.43时引入该技术的,并正式包含在2.6内核中。
Linux社区关于RCU的经典文档位于https://www.kernel.org/doc/ols/2001/read-copy.pdf ,Linux内核源代码Documentation/RCU/也包含了RCU的一些讲解。
RCU
与自旋锁的不同之处在于:
RCU
的读操作:使用RCU
的读端没有锁、内存屏障、原子指令类的开销,几乎可以认为是直接读(Read)RCU
的写操作:执行共享资源前,先拷贝一个副本(Copy),然后对副本进行修改,最后在合适的时机替换掉就得数据(Update),这个等待时机的过程称为宽限期。说到底,
RCU
就是彻底放开了读操作,只是在写操作时,拷贝一个副本进行修改,并在合适的时机去同步写操作的数据!
RCU
的API
:
// 读锁定
rcu_read_lock()
rcu_read_lock_bh()
// 读解锁
rcu_read_unlock()
rcu_read_unlock_bh()
// 同步RCU
synchronize_rcu()
// 回调更新
void call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *rcu));
RCU
在内核中较为复杂,本节就不展开来讲了,先对RCU
有一个较浅的认识,后续再对齐详细讲解。
本章较为浅层次的了解Kernel
自旋锁的衍生锁,分别包括:读写锁、顺序锁、RCU
,并了解其底层实现的机制:
read_lock
,rwlock_t
定义的value
值加1,每执行一次read_unlock
,value
值减1,因此读操作不存在等待的情况;value
值是否为0,不为0,说明有其他线程占用锁;如果为0,则说明未被占用;然后每执行一次write_lock
,value
最高位置1,每执行一次write_unlock
,value
清0sequence
的常量write_seqlock
时,将sequence
加1,调用write_sequnlock
时,将sequence
减1sequence
常量的值,然后调用read_seqretry
与上一次的sequence
值比较,看是否相同,来检测是否需要重新读一次。RCU
的读操作:使用RCU
的读端没有锁、内存屏障、原子指令类的开销,几乎可以认为是直接读(Read)RCU
的写操作:执行共享资源前,先拷贝一个副本(Copy),然后对副本进行修改,最后在合适的时机替换掉就得数据(Update),这个等待时机的过程称为宽限期。image-20230806173916498