之前在Linux RCU 内核同步机制中对RCU使用场景和使用方式理解存在不足,这里记录一下目前对RCU使用方面更多的个人理解。

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

synchronize_rcu / synchronize_sched 怎么区分?

在非抢占配置下synchronize_rcu就是synchronize_sched。
一般服务器典型配置就是非抢占,因此暂不分析抢占情况。

synchronize_sched_expedited如何使用

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

synchronize_rcu如何使用?

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

但是由于其实现机制,还可以有不规范的使用方式以满足一些特殊需求,比如等待所有cpu离开某一段代码段,后面会有文章说明。

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(离开最外层临界区时)
  • rcu_preempt_note_context_switch <- rcu_note_context_switch(每次)
  • 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完成