RCU synchronize原理分析

RCU synchronize原理分析

http://www.wowotech.net/kernel_synchronization/223.html

    RCU(Read-Copy Update)是Linux内核比较成熟的新型读写锁,具有较高的读写并发性能,常常用在需要互斥的性能关键路径。在kernel中,rcu有tiny rcu和tree rcu两种实现,tiny rcu更加简洁,通常用在小型嵌入式系统中,tree rcu则被广泛使用在了server, desktop以及android系统中。本文将以tree rcu为分析对象。


1 如何度过宽限期

    RCU的核心理念是读者访问的同时,写者可以更新访问对象的副本,但写者需要等待所有读者完成访问之后,才能删除老对象。这个过程实现的关键和难点就在于如何判断所有的读者已经完成访问。通常把写者开始更新,到所有读者完成访问这段时间叫做宽限期(Grace Period)。内核中实现宽限期等待的函数是synchronize_rcu。

1.1 读者锁的标记

         在普通的TREE RCU实现中,rcu_read_lock和rcu_read_unlock的实现非常简单,分别是关闭抢占和打开抢占:

 
 
  1. staticinlinevoid __rcu_read_lock(void)
  2. {
  3. preempt_disable();
  4. }
  5.  
  6. staticinlinevoid __rcu_read_unlock(void)
  7. {
  8. preempt_enable();
  9. }

    这时是否度过宽限期的判断就比较简单:每个CPU都经过一次抢占。因为发生抢占,就说明不在rcu_read_lock和rcu_read_unlock之间,必然已经完成访问或者还未开始访问。

1.2 每个CPU度过quiescnet state

    接下来我们看每个CPU上报完成抢占的过程。kernel把这个完成抢占的状态称为quiescent state。每个CPU在时钟中断的处理函数中,都会判断当前CPU是否度过quiescent state。

 
 
  1. void update_process_times(int user_tick)
  2. {
  3. ......
  4. rcu_check_callbacks(cpu, user_tick);
  5. ......
  6. }
  7.  
  8. void rcu_check_callbacks(int cpu,int user)
  9. {
  10. ......
  11. if(user || rcu_is_cpu_rrupt_from_idle()){
  12. /*在用户态上下文,或者idle上下文,说明已经发生过抢占*/
  13. rcu_sched_qs(cpu);
  14. rcu_bh_qs(cpu);
  15. }elseif(!in_softirq()){
  16. /*仅仅针对使用rcu_read_lock_bh类型的rcu,不在softirq,
  17. *说明已经不在read_lock关键区域*/
  18. rcu_bh_qs(cpu);
  19. }
  20. rcu_preempt_check_callbacks(cpu);
  21. if(rcu_pending(cpu))
  22. invoke_rcu_core();
  23. ......
  24. }
    这里补充一个细节说明, Tree RCU 有多个类型的 RCU State ,用于不同的 RCU 场景,包括 rcu_sched_state rcu_bh_state rcu_preempt_state 。不同的场景使用不同的 RCU API ,度过宽限期的方式就有所区别。例如上面代码中的 rcu_sched_qs rcu_bh_qs ,就是为了标记不同的 state 度过 quiescent state 。普通的 RCU 例如内核线程、系统调用等场景,使用 rcu_read_lock 或者 rcu_read_lock_sched ,他们的实现是一样的;软中断上下文则可以使用 rcu_read_lock_bh ,使得宽限期更快度过。

    细分这些场景是为了提高RCU的效率。rcu_preempt_state将在下文进行说明。

1.3 汇报宽限期度过

         每个CPU度过quiescent state之后,需要向上汇报直至所有CPU完成quiescent state,从而标识宽限期的完成,这个汇报过程在软中断RCU_SOFTIRQ中完成。软中断的唤醒则是在上述的时钟中断中进行。

update_process_times

    -> rcu_check_callbacks

        -> invoke_rcu_core

RCU_SOFTIRQ软中断处理的汇报流程如下:

rcu_process_callbacks

    -> __rcu_process_callbacks

        -> rcu_check_quiescent_state

            -> rcu_report_qs_rdp

                -> rcu_report_qs_rnp

其中rcu_report_qs_rnp是从叶子节点向根节点的遍历过程,同一个节点的子节点都通过quiescent state后,该节点也设置为通过。


这个树状的汇报过程,也就是“Tree RCU”这个名字得来的缘由。

树结构每层的节点数量和叶子节点数量由一系列的宏定义来决定:

 
 
  1. #define MAX_RCU_LVLS 4
  2. #define RCU_FANOUT_1 (CONFIG_RCU_FANOUT_LEAF)
  3. #define RCU_FANOUT_2 (RCU_FANOUT_1 * CONFIG_RCU_FANOUT)
  4. #define RCU_FANOUT_3 (RCU_FANOUT_2 * CONFIG_RCU_FANOUT)
  5. #define RCU_FANOUT_4 (RCU_FANOUT_3 * CONFIG_RCU_FANOUT)
  6.  
  7. #if NR_CPUS <= RCU_FANOUT_1
  8. # define RCU_NUM_LVLS 1
  9. # define NUM_RCU_LVL_0 1
  10. # define NUM_RCU_LVL_1 (NR_CPUS)
  11. # define NUM_RCU_LVL_2 0
  12. # define NUM_RCU_LVL_3 0
  13. # define NUM_RCU_LVL_4 0
  14. #elif NR_CPUS <= RCU_FANOUT_2
  15. # define RCU_NUM_LVLS 2
  16. # define NUM_RCU_LVL_0 1
  17. # define NUM_RCU_LVL_1 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_1)
  18. # define NUM_RCU_LVL_2 (NR_CPUS)
  19. # define NUM_RCU_LVL_3 0
  20. # define NUM_RCU_LVL_4 0
  21. #elif NR_CPUS <= RCU_FANOUT_3
  22. # define RCU_NUM_LVLS 3
  23. # define NUM_RCU_LVL_0 1
  24. # define NUM_RCU_LVL_1 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_2)
  25. # define NUM_RCU_LVL_2 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_1)
  26. # define NUM_RCU_LVL_3 (NR_CPUS)
  27. # define NUM_RCU_LVL_4 0
  28. #elif NR_CPUS <= RCU_FANOUT_4
  29. # define RCU_NUM_LVLS 4
  30. # define NUM_RCU_LVL_0 1
  31. # define NUM_RCU_LVL_1 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_3)
  32. # define NUM_RCU_LVL_2 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_2)
  33. # define NUM_RCU_LVL_3 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_1)
  34. # define NUM_RCU_LVL_4 (NR_CPUS)

1.3 宽限期的发起与完成

    所有宽限期的发起和完成都是由同一个内核线程rcu_gp_kthread来完成。通过判断rsp->gp_flags & RCU_GP_FLAG_INIT来决定是否发起一个gp;通过判断! (rnp->qsmask) && !rcu_preempt_blocked_readers_cgp(rnp))来决定是否结束一个gp。

发起一个GP时,rsp->gpnum++;结束一个GP时,rsp->completed = rsp->gpnum。

1.4 rcu callbacks处理

    rcu的callback通常是在sychronize_rcu中添加的wakeme_after_rcu,也就是唤醒synchronize_rcu的进程,它正在等待GP的结束。

         callbacks的处理同样在软中断RCU_SOFTIRQ中完成

rcu_process_callbacks

    -> __rcu_process_callbacks

        -> invoke_rcu_callbacks

            -> rcu_do_batch

                -> __rcu_reclaim

    这里RCU的callbacks链表采用了一种分段链表的方式,整个callback链表,根据具体GP结束的时间,分成若干段:nxtlist -- *nxttail[RCU_DONE_TAIL] -- *nxttail[RCU_WAIT_TAIL] -- *nxttail[RCU_NEXT_READY_TAIL] -- *nxttail[RCU_NEXT_TAIL]。

    rcu_do_batch只处理nxtlist -- *nxttail[RCU_DONE_TAIL]之间的callbacks。每个GP结束都会重新调整callback所处的段位,每个新的callback将会添加在末尾,也就是*nxttail[RCU_NEXT_TAIL]。

2 可抢占的RCU

    如果config文件定义了CONFIG_TREE_PREEMPT_RCU=y,那么sychronize_rcu将默认使用rcu_preempt_state。这类rcu的特点就在于read_lock期间是允许其它进程抢占的,因此它判断宽限期度过的方法就不太一样。

    从rcu_read_lock和rcu_read_unlock的定义就可以知道,TREE_PREEMPT_RCU并不是以简单的经过抢占为CPU渡过GP的标准,而是有个rcu_read_lock_nesting计数

 
 
  1. void __rcu_read_lock(void)
  2. {
  3. current->rcu_read_lock_nesting++;
  4. barrier();/* critical section after entry code. */
  5. }
  6.  
  7. void __rcu_read_unlock(void)
  8. {
  9. struct task_struct *t = current;
  10.  
  11. if(t->rcu_read_lock_nesting !=1){
  12. --t->rcu_read_lock_nesting;
  13. }else{
  14. barrier();/* critical section before exit code. */
  15. t->rcu_read_lock_nesting = INT_MIN;
  16. barrier();/* assign before ->rcu_read_unlock_special load */
  17. if(unlikely(ACCESS_ONCE(t->rcu_read_unlock_special)))
  18. rcu_read_unlock_special(t);
  19. barrier();/* ->rcu_read_unlock_special load before assign */
  20. t->rcu_read_lock_nesting =0;
  21. }
  22. }

    当抢占发生时,__schedule函数会调用rcu_note_context_switch来通知RCU更新状态,如果当前CPU处于rcu_read_lock状态,当前进程将会放入rnp->blkd_tasks阻塞队列,并呈现在rnp->gp_tasks链表中。

    从上文1.3节宽限期的结束处理过程我们可以知道,rcu_gp_kthread会判断! (rnp->qsmask) && !rcu_preempt_blocked_readers_cgp(rnp))两个条件来决定GP是否完成,其中!rnp->qsmask代表每个CPU都经过一次quiescent state,quiescent state的定义与传统RCU一致;!rcu_preempt_blocked_readers_cgp(rnp)这个条件就代表了rcu是否还有阻塞的进程。

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
RCU stall的机制原理主要涉及到RCU的核心概念和实现方式。下面是RCU stall的机制原理的解释: RCU(Read-Copy-Update)是一种用于实现并发读写数据结构的机制。其核心思想是通过复制数据结构,并使用多版本并发控制来实现读操作的并发性,从而避免锁竞争。 RCU机制的主要原理如下: 1. 读操作:当一个线程进行读操作时,它可以自由地访问数据结构,而不需要获取锁或同步机制。这是因为RCU使用了多版本并发控制,每个版本都是一个瞬时快照。 2. 写操作:当一个线程进行写操作时,它会创建一个新的数据版本,并将更新后的数据写入新版本中。然后,它将原来的数据版本标记为废弃,并等待所有正在进行读操作的线程完成后,才会释放废弃版本的内存资源。 RCU stall的机制原理涉及到RCU机制中的一些问题和限制: 1. 长时间的更新操作:如果写操作非常耗时,那么正在进行读操作的线程需要等待更新操作完成,从而导致RCU stall。 2. 读写冲突:当一个线程正在进行读操作时,如果另一个线程正在进行写操作,读操作线程需要等待写操作完成才能继续进行读操作,也可能导致RCU stall。 3. 延迟回收:在RCU机制中,内存资源的释放是通过延迟回收来实现的。如果回收操作被延迟,可能会导致内存占用过高,进而影响系统性能。 总结来说,RCU stall的机制原理主要是由于长时间的更新操作、读写冲突和延迟回收等因素导致的。为了解决或减轻RCU stall问题,需要合理设计数据结构、控制更新频率、使用适当的同步机制以及增加系统资源等措施。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值