前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >自旋锁的衍生锁有哪些?

自旋锁的衍生锁有哪些?

作者头像
董哥聊技术
发布2023-09-26 14:17:15
2110
发布2023-09-26 14:17:15
举报
文章被收录于专栏:嵌入式艺术嵌入式艺术

【深入理解Linux内核锁】五、衍生自旋锁

上一章,我们了解了自旋锁的相关接口与实现,下面我们来看一下基于自旋锁的衍生锁! 衍生锁种类比较多,我们本篇主要起引导作用,不详细介绍其内部实现!

1、前言

自旋锁主要用来解决SMP和调度引发的竞态问题,但是普通的自旋锁并不关心临界区在执行什么操作,对读和写都一视同仁,这样就会存在一些弊端!

我们知道,临界区如果没有被修改,同时读临界区资源是没有问题的,这也就是普通自旋锁的不足之处。

基于上述的弊端,伟大的工程师们,基于自旋锁逐渐就衍生出了一些效率更高的锁,比如:读写自旋锁,顺序自旋锁,RCU等,下面我们一一介绍。

2、读写自旋锁

读写自旋锁主要解决自旋锁的同时读的问题,即:

  1. 多个线程可以同时读取临界区资源
  2. 多个线程同时写是互斥的
  3. 多个线程读和写之间是互斥的

说到底,读写自旋锁就是允许了 读的并发!

image-20230806174653416

读写自旋锁API如下

代码语言:javascript
复制
/* 定义和初始化自旋锁 */
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);

其读写锁的实现不难分析,大家可自行查看! 下面主要分析一下其核心实现的思想:

读写锁思想

  1. 普通的自旋锁,其读写互斥是因为:读写操作均调用统一接口spin_lockspin_unlock,并操作同一把锁spinlock_t,这样无论是读还是写,都是判断其next下一个互斥锁的线程索引和owner当前锁的线程索引是否一致,来实现的,这样读和写操作就冲突了。
  2. 而读写锁,读操作和写操作分别是两个接口,read_lockread_unlockwrite_lockwrite_unlock两套接口,这样也就使得读操作可以并发
  3. 读写锁的读操作思想:每执行一次read_lockrwlock_t定义的value值加1,每执行一次read_unlockvalue值减1,因此读操作不存在等待的情况;
  4. 读写锁的写操作思想:执行写操作之前,先判断value值是否为0,不为0,说明有其他线程占用锁;如果为0,则说明未被占用;然后每执行一次write_lockvalue最高位置1,每执行一次write_unlockvalue清0

3、顺序锁

读写锁在自旋锁的基础之上,允许了读的并发,但是读操作和写操作之间还是互斥的,顺序锁就优化了这种问题:

  1. 多个线程可以同时读取临界区资源
  2. 多个线程同时写是互斥的
  3. 多个线程读和写之间是独立的,可以支持写临界区的时候,读不受影响

说到底,顺序自旋锁就是允许了 读和写之间 的并发!

image-20230806174727899

顺序锁的API如下

代码语言:javascript
复制
/* 定义和初始化顺序锁 */
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)

顺序锁思想:对于顺序锁而言,尽管读写之间不互相排斥,但是如果读执行单元在读操作期间,写执行单元已经发生了写操作,那么,读执行单元必须重新读取数据,以便确保得到的数据是完整的。

  1. 顺序锁相当于在自旋锁的基础上,增加了一个sequence的常量
  2. 对于顺序锁的写操作,其使用自旋锁来实现,并且调用write_seqlock时,将sequence加1,调用write_sequnlock时,将sequence减1
  3. 对于顺序锁的读操作,为了实现读与写的并发,读操作仅仅用于读取sequence常量的值,然后调用read_seqretry与上一次的sequence值比较,看是否相同,来检测是否需要重新读一次。

一个使用顺序锁的示例如下:

读执行单元在访问完被顺序锁s1保护的共享资源后需要调用该函数来检查,在读访问期间是否有写操作。如果有写操作,读执行单元就需要重新进行读操作。

代码语言:javascript
复制
// thread 1
do {
    seqnum = read_seqbegin(&seqlock_a);
    /* 读操作代码块*/
    ...
} while (read_seqretry(&seqlock_a, seqnum));

// thread 2
write_seqlock(&seqlock_a);
...
/* 写操作代码块*/
write_sequnlock(&seqlock_a);

4、RCU

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就是彻底放开了读操作,只是在写操作时,拷贝一个副本进行修改,并在合适的时机去同步写操作的数据!

RCUAPI

代码语言:javascript
复制
// 读锁定
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有一个较浅的认识,后续再对齐详细讲解。

5、总结

本章较为浅层次的了解Kernel自旋锁的衍生锁,分别包括:读写锁、顺序锁、RCU,并了解其底层实现的机制:

  • 读写锁
    • 读写锁的读操作思想:每执行一次read_lockrwlock_t定义的value值加1,每执行一次read_unlockvalue值减1,因此读操作不存在等待的情况;
    • 读写锁的写操作思想:执行写操作之前,先判断value值是否为0,不为0,说明有其他线程占用锁;如果为0,则说明未被占用;然后每执行一次write_lockvalue最高位置1,每执行一次write_unlockvalue清0
  • 顺序锁
    • 顺序锁相当于在自旋锁的基础上,增加了一个sequence的常量
    • 对于顺序锁的写操作,其使用自旋锁来实现,并且调用write_seqlock时,将sequence加1,调用write_sequnlock时,将sequence减1
    • 对于顺序锁的读操作,为了实现读与写的并发,读操作仅仅用于读取sequence常量的值,然后调用read_seqretry与上一次的sequence值比较,看是否相同,来检测是否需要重新读一次。
  • RCU
    • RCU的读操作:使用RCU的读端没有锁、内存屏障、原子指令类的开销,几乎可以认为是直接读(Read)
    • RCU的写操作:执行共享资源前,先拷贝一个副本(Copy),然后对副本进行修改,最后在合适的时机替换掉就得数据(Update),这个等待时机的过程称为宽限期。

image-20230806173916498

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-09-26 07:14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 嵌入式艺术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 【深入理解Linux内核锁】五、衍生自旋锁
    • 1、前言
      • 2、读写自旋锁
        • 3、顺序锁
          • 4、RCU
            • 5、总结
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档