抢占式RCU与RCU使用
- 1. 抢占式RCU
- 2. 非抢占式RCU环境下
- 2.1. synchronize_rcu / synchronize_sched 怎么区分?
- 2.2. synchronize_sched_expedited如何使用
- 2.3. synchronize_rcu如何使用?
- 2.4. synchronize_rcu是否可以代替synchronize_rcu_bh使用?
- 2.5. rcu_barrier如何使用
- 2.6. rcu_barrier是否可以代替rcu_barrier_bh使用
- 2.7. synchronize_rcu是否可以代替rcu_barrier使用
- 2.8. rcu_barrier是否可以代替synchronize_rcu使用
- 2.9. rcu_sched_qs被调用位置
- 2.10. rcu_bh_qs被调用位置
- 2.11. rcu_preempt_qs被调用位置
- 2.12. rcu_barrier_sched实现(在非抢占配置下即为rcu_barrier)
- 2.13. synchronize_sched实现(在非抢占配置下即为synchronize_rcu)
- 3. 参考
之前在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
- 如果不在临界区,调用rcu_preempt_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_report_unblock_qs_rnp
- 调用rcu_read_unlock_special(函数内部逻辑运行在关中断状态且rnp上锁。处理被阻塞的rnp相关流程)
- 如果当前进程在临界区内且RCU_READ_UNLOCK_BLOCKED未置位(用于标记经历过抢占)
- 调用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调用rcu_barrier_func(rsp)
- 如果cpu对应的rdp->qlen等于0,不需要任何操作。
- 如果cpu对应的rdp->qlen大于0,也就是存在callback,则对该cpu调用smp_call_function_single,传入函数指针rcu_barrier_func,rsp
- 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完成