之前在Linux RCU 内核同步机制中对RCU基本流程做了记录,这里记录一下抢占式RCU和RCU使用方面更多的理解。

由于高版本整合了抢占和非抢占RCU更新端的API,不再有synchronize_rcu/synchronize_sched/synchronize_rcu_bh这种区分了,只在临界区API中保留了不同RCU的区别。因此本文在抢占式RCU段落以外的内容,均假定为未开启抢占配置。

本文内容参考内核版本 3.10.0-862.el7.x86_64

抢占式RCU

抢占式RCU需要开启编译配置CONFIG_PREEMPT_RCU。

被抢占的临界区,在临界区退出之前,gp不会完成。

大体流程

抢占式RCU大体流程与非抢占RCU类似,参考

  • 当需要启动一个gp时,软中断将rsp->gp_flags标记为RCU_GP_FLAG_INIT
  • 内核线程更新rsp和rnp相关内容
  • 软中断将新gp相关内容向下传递到rdp
  • 每个cpu在定时器中断中判定当前task是否在临界区
    • 如果不在临界区,调用rcu_preempt_qs
      • 标记rdp经历过了qs
      • task->rcu_read_unlock_special清除RCU_READ_UNLOCK_NEED_QS
    • 如果在临界区内
      • task->rcu_read_unlock_special置位RCU_READ_UNLOCK_NEED_QS
  • 每个cpu在RCU软中断中检查qs状态,如果经历过qs则逐级上报
  • 但是如果一个rcu临界区被抢占了,在rcu_report_qs_rnp上报中会发现rnp->gp_tasks不为空,阻断继续向上传递
  • 后面就需要rcu_read_unlock_special进来干活了

rcu_read_lock

  • 增加task->rcu_read_lock_nesting计数,标记当前进程在临界区

rcu_preempt_note_context_switch(进程发生调度)

  • 判断当前进程状态
    • 如果当前进程在临界区内且RCU_READ_UNLOCK_BLOCKED未置位(用于标记经历过抢占)
      • task->rcu_read_unlock_special置位RCU_READ_UNLOCK_BLOCKED
      • task->rcu_blocked_node记录当前CPU所属的rnp
      • 将task->rcu_node_entry挂在rnp->blkd_tasks链表头部。如果当前cpu需要上报qs则更新rnp->gp_tasks用于标记当前gp被抢占的task链表。
    • 如果当前进程经历过抢占,正在退出最外层临界区(说明rnp->blkd_tasks和tp_task需要处理)
      • 调用rcu_read_unlock_special(函数内部逻辑运行在关中断状态且rnp上锁。处理被阻塞的rnp相关流程)
        • 如果task->rcu_read_unlock_special存在置位RCU_READ_UNLOCK_NEED_QS,调用rcu_preempt_qs。因为只有退出最外层临界区时才会进到这个函数
        • 如果当前处于中断或软中断服务中,不进行后续流程,直接返回。(应该是在退出临界区过程中进入了中断,中断里再次进入并离开临界区。因为中断处理完成后就会立即再进行rcu_read_unlock_special,没必要在中断里做这个事)
        • 如果task->rcu_read_unlock_special存在置位RCU_READ_UNLOCK_BLOCKED
          • 清掉RCU_READ_UNLOCK_BLOCKED置位
          • 从task->rcu_blocked_node获取阻塞的rnp(这里的task还可能会迁移?)
          • 将task从rnp->blkd_tasks链表中摘出来,并更新gp_tasks
          • 如果当前task是链表最后一个
            • 调用rcu_report_unblock_qs_rnp
              • 如果该rnp下所有CPU未全部经历qs或依然有阻塞task,则返回。
              • 如果没有上级rnp,调用rcu_report_qs_rsp
                • 唤醒RCU内核线程
              • 如果存在上级rnp,调用rcu_report_qs_rnp
                • 满足所有rnp均度过qs情况下调用rcu_report_qs_rsp
  • 调用rcu_preempt_qs标记cpu的qs。(这里只是标记当前CPU的qs,但是当前gp的完成还需要判断rnp->gp_tasks链表)

rcu_read_unlock

  • 判断task->rcu_read_lock_nesting
    • 如果不为1,说明处于多层嵌套临界区,只需要减1既可
    • 如果为1,说明在退出最外层临界区。
      • rcu_read_lock_nesting先置为负数(如果这里后面发生了抢占,可以在__schedule中尽早调用rcu_read_unlock_special尝试完成gp)
      • 根据task->rcu_read_unlock_special判断调用rcu_read_unlock_special函数
      • task->rcu_read_lock_nesting置0,标记task彻底退出了临界区。

非抢占式RCU环境下

synchronize_rcu / synchronize_sched 怎么区分?

在非抢占配置下synchronize_rcu就是synchronize_sched。
低版本服务器典型配置就是非抢占。高版本整合了RCU更新端API,不再区分。

synchronize_sched_expedited如何使用

可以看作与synchronize_rcu等同。区别在于会尝试使用绑定cpu的高优先级migration内核线程在每个CPU上调度一次,加速gp过程。但是这对于其他需要运行的线程不友好,因此尽量不要使用该api。以synchronize_net为例,只有在持有rtnl_mutex时才会使用该api加速gp,以减少mutex持有时间。

synchronize_rcu如何使用?

只用于等待确保所有cpu上之前的rcu_read_lock临界区都退出。

synchronize_rcu是否可以代替synchronize_rcu_bh使用?

这个问题可以替换为call_rcu回调完成后,是否可能还有cpu会处于rcu_read_lock_bh临界区。
从当前的实现上看,个人认为是不可能的,因为rcu_read_lock_bh临界区内,是不应该发生rcu_sched_qs被调用的情况的。
但是,从api的语义角度考虑,不应该这么替代使用。

rcu_barrier如何使用

只用于等待确保所有cpu上之前的call_rcu回调全部执行完成。

rcu_barrier是否可以代替rcu_barrier_bh使用

不可以。语义上这两个api无关,分别对应的是call_rcu和call_rcu_bh。当前版本的实现也不支持这种代替操作,理由如下。

  • 如果rcu_sched_state上没有callback的话,rcu_barrier会直接返回。
  • qs判定条件不同,虽然时钟中断时synchronize_rcu的判定更严格,但是rcu_sched_qs还有其他的调用位置,因此无法保证sched的qs不晚于bh的qs。一次gp的结束是在独立的内核线程中做出的操作,分别是rcu_sched与rcu_bh,内核线程需要调度,因此就算qs同时判定,也可能发生rcu_sched更早被调度,其gp结束更早,其callback被更早调用的情况。
  • 一个cpu上的callbac的回调会分批执行,默认blimit为10,参数/sys/module/rcutree/parameters/blimit。当执行数超过blimit,且need_resched()为真或非idle且非独立线程时会中断执行,剩余未执行的callback重新挂回rdp->nxtlist。因此gp结束且开始执行回调时,并不一定所有回调都会执行完,sched的callback都执行完时,bh的callback未执行完也是可能发生的。

PS:rcu_struct_flavors链表中rcu_bh_state在rcu_sched_state前,遍历所有的rsp时,会先处理rcu_bh_state。但这个顺序依赖于实现,而且在这个问题上也没有作用。

synchronize_rcu是否可以代替rcu_barrier使用

不可以。理由如下。

  • synchronize_rcu只用了一个cpu的call_rcu回调完成用于标识gp的完成,但是callback的回调是在gp完成以后,分布于多个cpu的,且一个cpu上的callback不一定会一次全部执行完。
  • 单核情况更严重,synchronize_rcu用了一个快速判定gp完成的方式,直接返回了,完全没有等待callback的效果。

rcu_barrier是否可以代替synchronize_rcu使用

不可以。理由如下。

  • rcu_barrier会等待所有cpu上的callback都执行完成,如果所有cpu都没有callback挂载,那么会直接返回,不会等待任何qs与gp。

rcu_sched_qs被调用位置

  • rcu_check_callbacks(时钟中断时判断之前处于用户态或cpu idle)
  • rcu_note_context_switch(以下几个调用位置)
    • __kvm_guest_enter,kvm进入guest模式不会持有任何rcu读临界区,在guest模式可能运行较长时间,因此把这看作一个qs,类似切换用户态。
    • __schedule,每次调度,看作一个qs
    • run_ksoftirqd,每次软中断进程被调用,__do_softirq完成后,被调用,看作一个qs
    • process_one_work,workqueue相关,完成每个work后看过一个qs,并尝试检查是否需要调度,用以避免长时间占用cpu(这里判断了TIF_NEED_RESCHED以及当前是否正处于被抢占中,因此不确定非抢占配置下是否会调度)

rcu_bh_qs被调用位置

  • rcu_check_callbacks(时钟中断时判断之前处于用户态或cpu idle,这里的qs是与rcu_shed_qs相同的)
  • rcu_check_callbacks(不满足上面的条件,但是处于非软中断状态。因为rcu_read_lock_bh会关软中断,in_softirq会判定处于软中断状态。因此非软中断状态也就是不在读临界区)

rcu_preempt_qs被调用位置

  • rcu_read_unlock_special(离开最外层临界区需要处理阻塞的rnp时)
  • rcu_preempt_note_context_switch <- rcu_note_context_switch(参考抢占式RCU
  • rcu_preempt_check_callbacks <- rcu_check_callbacks(时钟中断时判断当前进程是否处于临界区)

rcu_barrier_sched实现(在非抢占配置下即为rcu_barrier)

rcu_barrier_sched / rcu_barrier_bh 的区别仅为对不同的rsp调用_rcu_barrier
_rcu_barrier,简略描述即为在每个cpu上调用call_rcu_XXX,并等待所有回调完成。那么之前的回调当然也已经都完成了。

  • 读取rsp->n_barrier_done
  • 上锁rsp->barrier_mutex
  • 再次读取rsp->n_barrier_done
  • 判断两次读取值的对比,如果获取锁期间有其他进程完成了对应rsp的barrier操作,说明当前进程也完成了其目标,直接返回。
  • rsp->n_barrier_done加1
  • 调用init_completion初始化rsp->barrier_completion
  • rsp->barrier_cpu_count置为1
  • 遍历每一个在线且不为nocb的cpu(默认均不为nocb)
    • 如果cpu对应的rdp->qlen大于0,也就是存在callback,则对该cpu调用smp_call_function_single,传入函数指针rcu_barrier_func,rsp
      • 这个就是睡眠等待每个CPU调用rcu_barrier_func(rsp)
        • rsp->barrier_cpu_count加1
        • 调用rsp->call,这个call在rsp定义的时候设置的,比如rcu_sched_state.call即为call_rcu_sched。回调函数为rcu_barrier_callback,这里可以猜到内部就是减rsp->barrier_cpu_count计数并在为0时调用complete标记完成并唤醒等待进程
    • 如果cpu对应的rdp->qlen等于0,不需要任何操作。
  • rsp->barrier_cpu_count减1,如果减为0则对rsp->barrier_completion调用complete将其标记为完成并唤醒等待进程
  • rsp->n_barrier_done加1
  • 等待rsp->barrier_completion完成
  • 解锁rsp->barrier_mutex

synchronize_sched实现(在非抢占配置下即为synchronize_rcu)

  • rcu_blocking_is_gp,判断当前在线cpu数,如果为1,则判定为当前gp已经完成,不存在未完成的临界区,直接返回。
  • wait_rcu_gp,参数call_rcu_sched
    • 初始化栈上的rcu_head和completion
    • 调用参数传入的函数指针(这里参数为call_rcu_sched)回调函数为wakeme_after_rcu
    • 等待completion完成

参考

linux内核文档 What is RCU?