关于rcu的内容本来想放在内核锁介绍里, 后来发现要解释的内容太多了, 索性就分成几章. 本章来分析rcu的实现.
5. rcu
Added on 2017.12.20
之前看rcu代码时一直没有弄明白rcu_node的分布, 直到最近看到https://lwn.net/Articles/305782/这篇文档才恍然大悟. 以前写的注释懒得改了, 直接在这里加笔说明.rcu_state结构有包含一个rcu_node的数组node以及rcu_node指针数组level, 其注释说明了rcu_node的分层思想. 首先一个问题, 为什么要为rcu_node分层? 假定一个8核的cpu, 如果只有一个rcu_node, 那么当8个核同时进入qs时必然会竞争锁(rcu_node.lock), 这与rcu本意(减少读者开销)背道而驰(最后获取锁的cpu为竞争该锁需等待7次). 为了减少lock contention, rcu使用了分层的技巧, 最上层的rcu_node为根节点, 需要所有cpu都清除对应标记位才会唤醒rcu线程, 后面的rcu_node都是叶子节点, 每个节点只对应部分cpu, 只要对应的cpu清除标记位就会上报给上一级的节点清除对应标记位. 举例而言, 一个两层的rcu_node, 第一层为根节点, 第二层为4个叶子节点, 每个节点按顺序分别对应两个cpu. 当某一时刻8个cpu同时进入qs, 此时有4个cpu可以成功获取锁(每个节点一个cpu成功获取锁), 而竞争失败的cpu仅需等待一次即可重新获取锁. 对于第一次竞争成功的cpu在修改qsmask后由于对应位未完全清除直接返回, 第二次竞争成功的cpu在修改qsmask后qsmask完全清零, 因此上报给上一级rcu_node(同样需要竞争该节点的锁). 最坏的情况下需要等待4次(第一级1次, 顶层3次).这么做有多少优化? This results in a significant reduction in lock contention: instead of six CPUs contending for a single lock each grace period, we have only three for the upper rcu_node's lock (a reduction of 50%) and only two for each of the lower rcu_nodes' locks (a reduction of 67%). 按原文的说法的确是非常高效了.解释清楚这一点再回头看rcu_node应该就非常好理解了, 更多的注释懒得写了, 还是看源码注释吧.严格来说read-copy-update(AKA rcu)并不是一种锁而是内核的一种同步机制. 假设一个场景只有一个线程在修改需要被保护的数据, 而有多个读取数据的线程. 如果使用自旋锁或型号量保护该数据势必导致读者之间的竞争(该竞争毫无意义). 如果我们能推迟写者的修改直到所有读者都完成读操作那么就无需锁保护该数据, 这就是rcu的基本思想. rcu将修改与更新两个操作分隔开来, 其中更新操作将等待所有读者都退出读临界区后执行. 借此读者与写者可以不用加锁, 大大减少了竞争锁的开销. 由于rcu是以牺牲写者性能来减少锁的开销, 因此rcu通常用于读者多写者少的场景.
由于读者不再加锁, 带来的问题是何时才是合适的更新点? 一个合理的时间点是所有cpu都调度一次后的时间点. 因此不同于其它内核锁, rcu有自己的内核线程用于处理同步的时间点(所以说rcu更像是一种同步手段而非锁, 尽管我们调用的接口里有lock这个词).相比与其它内核锁, rcu明显更复杂, 其api也更多. 但万变不离其宗, 它最基本的接口为如下5个:rcu_read_lock(): 标记读者进入读临界区.rcu_read_unlock(): 标记读者退出临界区.rcu_assign_pointer(): 声明一个rcu指针.rcu_dereference(): 解引用一个rcu指针.synchronize_rcu() / call_rcu(): 等待rcu同步(call_rcu立即返回).因为rcu读者无需复杂的操作, 因此我们先分析前4个接口, 然后我们从rcu线程初始化开始分析何时为合适的更新点, 最后回到synchronize_rcu()看看写者何时结束同步.在正式开始前我们先解释几个名词:grace period(gp): 所有cpu经历一次调度的周期(说是周期但实际长度不定), 是rcu用于同步的信号.quiescent-state(qs): 某个cpu经历一次调度后的状态, 当所有cpu都进入qs后一次gp才完成.quiescent-state forcing(fqs): 当cpu因idle而长期未进入qs时, 其它cpu会请求一次强制qs.rcu_read_lock()/rcu_read_unlock()(defined in include/linux/rcupdate.h)成对使用, 用于指示读者进入/退出一个读临界区. 其实现非常简单, 由于未定义DEBUG_LOCK_ALLOC与PROVE_RCU, 其实现仅调用__rcu_read_lock()/__rcu_read_unlock(). 注意在rcu读临界区内不能做任何阻塞操作(阻塞引起调度会导致rcu错误的认为读者已退出临界区)!
1 /** 2 * 标记进入rcu读临界区 3 * 当一个cpu调用synchronize_rcu()而其它cpu处于临界区时 4 * synchronize_rcu()保证会阻塞直到其它cpu退出临界区 5 * 类似的call_rcu()也会推迟调用对应回调直到其它cpu退出读临界区 6 * 注意, 该等待只会针对先进入临界区的情况, rcu同步不会等待新进入读临界区的读者 7 * rcu读临界区可能会非常复杂, 一个推迟的回调可能会推迟到最外面rcu读临界区结束 8 * 在非抢占内核中使用非抢占的rcu实现(TREE_RCU与TINY_RCU)时在读临界区阻塞是非法的 9 * 在可抢占内核中使用可抢占的rcu实现(TREE_PREEMPT_RCU与TINY_PREEMPT_RCU)时10 * 读临界区是可抢占的, 但显式阻塞是非法的11 * 在实时内核中使用可抢占的rcu实现时读临界区是可抢占的也可被阻塞, 但必须优先获取自旋锁12 * 如果觉得麻烦, 简单来说在非抢占内核禁止在读临界区阻塞!13 *14 **/15 static inline void rcu_read_lock(void)16 {17 __rcu_read_lock();18 __acquire(RCU);19 rcu_lock_acquire(&rcu_lock_map);20 rcu_lockdep_assert(!rcu_is_cpu_idle(), \21 "rcu_read_lock() used illegally while idle");22 }23 static inline void rcu_read_unlock(void)24 {25 rcu_lockdep_assert(!rcu_is_cpu_idle(), \26 "rcu_read_unlock() used illegally while idle");27 rcu_lock_release(&rcu_lock_map);28 __release(RCU);29 __rcu_read_unlock();30 }
__rcu_read_lock()/__rcu_read_unlock()有两种实现, 在未定义可抢占rcu(PREEMPT_RCU)时仅需开关抢占, 否则要对current->rcu_read_lock_nesting自增减并做内存屏障. 可见通常rcu的读者无需加解锁, 这极大减少了并发时读者的等待开销.
1 #ifndef CONFIG_PREEMPT_RCU 2 //defined in include/linux/rcupdate.h 3 static inline void __rcu_read_lock(void) 4 { 5 preempt_disable(); 6 } 7 static inline void __rcu_read_unlock(void) 8 { 9 preempt_enable();10 }11 #else12 //defined in kernel/rcupdate.c13 void __rcu_read_lock(void)14 {15 current->rcu_read_lock_nesting++;16 barrier();17 }18 EXPORT_SYMBOL_GPL(__rcu_read_lock);19 void __rcu_read_unlock(void)20 {21 struct task_struct *t = current;22 if (t->rcu_read_lock_nesting != 1) {23 --t->rcu_read_lock_nesting;24 } else {25 barrier();26 t->rcu_read_lock_nesting = INT_MIN;27 barrier();28 if (unlikely(ACCESS_ONCE(t->rcu_read_unlock_special)))29 rcu_read_unlock_special(t);30 barrier();31 t->rcu_read_lock_nesting = 0;32 }33 }34 EXPORT_SYMBOL_GPL(__rcu_read_unlock);35 #endif
再来看下rcu赋值与解引用接口. rcu_assign_pointer()(defined in include/linux/rcupdate.h)用于申明rcu保护的指针p并将v赋值给p. 该接口会做内存屏障(dmb_wmb()), 原因有二, 其一芯片架构要求其二防止编译器重排序代码. 这会导致效率降低, 因此在某些情况下也可以使用不带内存屏障的接口RCU_INIT_POINTER()(defined in include/linux/rcupdate.h). 关于RCU_INIT_POINTER()的使用须知见其注释, 保险起见仍然使用前者.
1 #define rcu_assign_pointer(p, v) \ 2 __rcu_assign_pointer((p), (v), __rcu) 3 #define __rcu_assign_pointer(p, v, space) \ 4 do { \ 5 smp_wmb(); \ 6 (p) = (typeof(*v) __force space *)(v); \ 7 } while (0) 8 #define RCU_INIT_POINTER(p, v) \ 9 do { \10 p = (typeof(*v) __force __rcu *)(v); \11 } while (0)
rcu解引用操作的接口是rcu_dereference()(defined in include/linux/rcupdate.h). 该接口实际调用rcu_dereference_check(), 在未定义DEBUG_LOCK_ALLOC时rcu_read_lock_held()永远返回1.
1 #define rcu_dereference(p) rcu_dereference_check(p, 0) 2 #define rcu_dereference_check(p, c) \ 3 __rcu_dereference_check((p), rcu_read_lock_held() || (c), __rcu) 4 #define __rcu_dereference_check(p, c, space) \ 5 ({ \ 6 typeof(*p) *_________p1 = (typeof(*p)*__force )ACCESS_ONCE(p); \ 7 rcu_lockdep_assert(c, "suspicious rcu_dereference_check() usage"); \ 8 rcu_dereference_sparse(p, space); \ 9 smp_read_barrier_depends(); \10 ((typeof(*p) __force __kernel *)(_________p1)); \11 })
遍历使用rcu保护的双向链表的接口list_for_each_entry_rcu()(defined in include/linux/rculist.h), 使用rcu保护的双向链表操作均在该文件下.
1 //必须使用带_rcu的链表操作, 遍历链表时必须被rcu_read_lock保护2 #define list_for_each_entry_rcu(pos, head, member) \3 for (pos = list_entry_rcu((head)->next, typeof(*pos), member); \4 &pos->member != (head); \5 pos = list_entry_rcu(pos->member.next, typeof(*pos), member))6 #define list_entry_rcu(ptr, type, member) \7 ({ typeof (*ptr) __rcu *__ptr = (typeof (*ptr) __rcu __force *)ptr; \8 container_of((typeof(ptr))rcu_dereference_raw(__ptr), type, member); \9 })
从读者接口看rcu仿佛没有什么保护操作, 那是因为绝大部分工作落在了rcu线程与写者上. 让我们先从rcu的初始化入手, 看看rcu线程究竟做了什么. rcu_init()(defined in kernel/rcutree.c)初始化rcu数据结构而rcu_spawn_gp_kthread()(defined in kernel/rcutree.c)生成rcu检测线程.
先来看看rcu_init(), rcu_bootup_announce()打印rcu相关配置宏, rcu_init_geometry()检测并设置节点参数, 全局变量rcu_sched_state与rcu_bh_state使用rcu_init_one()初始化. 这里先来看下rcu_state的成员node, NUM_RCU_NODES由NR_CPUS与RCU_FANOUT_LEAF决定, 前者为cpu个数后者为配置的叶子节点个数, 对于小系统而言仅使用一层节点, 即NUM_RCU_NODES为1, RCU_NUM_LVLS也为1, 此处仅讨论一层节点的情况. 对于一层节点每个cpu的rcu_data的mynode均指向根节点. 此外rcu_init_one()还会注册软中断回调rsp_wakeup()(该回调作用在后文讨论), 将该rsp挂入rcu_struct_flavors链表头(rcu_spawn_gp_kthread()会根据该链表节点创建rcu线程). 由于未定义TREE_PREEMPT_RCU, __rcu_init_preempt()为空. 注册软中断RCU_SOFTIRQ的回调rcu_process_callbacks.1 static void __init rcu_init_one(struct rcu_state *rsp, struct rcu_data __percpu *rda) 2 { 3 static char *buf[] = { "rcu_node_0", \ 4 "rcu_node_1", "rcu_node_2", "rcu_node_3" }; 5 static char *fqs[] = { "rcu_node_fqs_0", \ 6 "rcu_node_fqs_1", "rcu_node_fqs_2", "rcu_node_fqs_3" }; 7 int cpustride = 1; 8 int i; 9 int j;10 struct rcu_node *rnp;11 BUILD_BUG_ON(MAX_RCU_LVLS > ARRAY_SIZE(buf));12 if (rcu_num_lvls > RCU_NUM_LVLS)13 panic("rcu_init_one: rcu_num_lvls overflow");14 //num_rcu_lvl[]为每层的rcu_node个数, 对于小系统而言只有一个节点15 for (i = 0; i < rcu_num_lvls; i++)16 rsp->levelcnt[i] = num_rcu_lvl[i];17 //rsp->level[0]是静态初始化的(见RCU_STATE_INITIALIZER宏), 为rsp->node[0]18 for (i = 1; i < rcu_num_lvls; i++)19 rsp->level[i] = rsp->level[i - 1] + rsp->levelcnt[i - 1];20 rcu_init_levelspread(rsp);21 for (i = rcu_num_lvls - 1; i >= 0; i--) {22 cpustride *= rsp->levelspread[i];23 rnp = rsp->level[i];24 for (j = 0; j < rsp->levelcnt[i]; j++, rnp++) {25 raw_spin_lock_init(&rnp->lock);26 lockdep_set_class_and_name(&rnp->lock, &rcu_node_class[i], buf[i]);27 raw_spin_lock_init(&rnp->fqslock);28 lockdep_set_class_and_name(&rnp->fqslock, &rcu_fqs_class[i], fqs[i]);29 rnp->gpnum = rsp->gpnum;30 rnp->completed = rsp->completed;31 rnp->qsmask = 0;32 rnp->qsmaskinit = 0;33 rnp->grplo = j * cpustride;34 rnp->grphi = (j + 1) * cpustride - 1;35 if (rnp->grphi >= NR_CPUS)36 rnp->grphi = NR_CPUS - 1;37 if (i == 0) {38 rnp->grpnum = 0;39 rnp->grpmask = 0;40 rnp->parent = NULL;41 } else {42 rnp->grpnum = j % rsp->levelspread[i - 1];43 rnp->grpmask = 1UL << rnp->grpnum;44 rnp->parent = rsp->level[i - 1] + j / rsp->levelspread[i - 1];45 }46 rnp->level = i;47 INIT_LIST_HEAD(&rnp->blkd_tasks);48 rcu_init_one_nocb(rnp);49 }50 }51 rsp->rda = rda;52 //初始化rsp->gp_wq等待队列53 init_waitqueue_head(&rsp->gp_wq);54 //初始化软中断任务回调55 init_irq_work(&rsp->wakeup_work, rsp_wakeup);56 rnp = rsp->level[rcu_num_lvls - 1];57 //每个cpu的rda初始化, 对于一层rnp所有rda的mynode都指向根节点58 for_each_possible_cpu(i) {59 while (i > rnp->grphi)60 rnp++;61 per_cpu_ptr(rsp->rda, i)->mynode = rnp;62 rcu_boot_init_percpu_data(i, rsp);63 }64 //将rcu_sched_state与rcu_bh_state挂入链表, 全局变量rcu_struct_flavors为链表头65 list_add(&rsp->flavors, &rcu_struct_flavors);66 }67 void __init rcu_init(void)68 {69 int cpu;70 rcu_bootup_announce();71 rcu_init_geometry();72 rcu_init_one(&rcu_sched_state, &rcu_sched_data);73 rcu_init_one(&rcu_bh_state, &rcu_bh_data);74 __rcu_init_preempt();75 open_softirq(RCU_SOFTIRQ, rcu_process_callbacks);76 cpu_notifier(rcu_cpu_notify, 0);77 for_each_online_cpu(cpu)78 rcu_cpu_notify(NULL, CPU_UP_PREPARE, (void *)(long)cpu);79 }
rcu_spawn_gp_kthread()是内核初始化时创建rcu检测线程的接口. for_each_rcu_flavor()会遍历双向链表头rcu_struct_flavors的节点(它包含两个节点rcu_sched_state与rcu_bh_state), 并创建内核线程(rcu_sched与rcu_bh), 线程函数均为rcu_gp_kthread()(defined in kernel/rcutree.c).
1 static int __init rcu_spawn_gp_kthread(void) 2 { 3 unsigned long flags; 4 struct rcu_node *rnp; 5 struct rcu_state *rsp; 6 struct task_struct *t; 7 for_each_rcu_flavor(rsp) { 8 t = kthread_run(rcu_gp_kthread, rsp, rsp->name); 9 BUG_ON(IS_ERR(t));10 rnp = rcu_get_root(rsp);11 raw_spin_lock_irqsave(&rnp->lock, flags);12 rsp->gp_kthread = t;13 raw_spin_unlock_irqrestore(&rnp->lock, flags);14 rcu_spawn_nocb_kthreads(rsp);15 }16 return 0;17 }18 early_initcall(rcu_spawn_gp_kthread);
让我们从该线程入手来分析rcu同步的机制. 一次完整的rcu同步需要cpu发起一次gp, 等待所有cpu经历一次gp后再完成本次gp的清理工作. 具体到rcu线程, 首先rcu_gp_kthread()会一直睡眠直到被rsp->gp_flags置位RCU_GP_FLAG_INIT才被唤醒, 该标志位被认为是发起一次gp的标记. 被唤醒后调用rcu_gp_init()执行gp的初始化, 若初始化失败则请求调度. 如成功发起一次gp则开始睡眠等待本次gp完成. 该睡眠仅在三种情况下唤醒, 所有cpu都经历一次完整的gp(rnp->qsmask为0), cpu请求fqs(rsp->gp_flags & RCU_GP_FLAG_FQS)或超时. 第一种情况执行完成gp后的清理工作, 第二种情况执行fqs, 最后一种情况修改到期时间后继续睡眠等待. 让我们按顺序一步步分析.
1 static int __noreturn rcu_gp_kthread(void *arg) 2 { 3 int fqs_state; 4 unsigned long j; 5 int ret; 6 struct rcu_state *rsp = arg; 7 //获取rcu根节点 8 struct rcu_node *rnp = rcu_get_root(rsp); 9 for (;;) {10 //处理gp起始11 for (;;) {12 wait_event_interruptible(rsp->gp_wq, rsp->gp_flags & RCU_GP_FLAG_INIT);13 if ((rsp->gp_flags & RCU_GP_FLAG_INIT) && rcu_gp_init(rsp))14 break;15 cond_resched();16 flush_signals(current);17 }18 //处理强制qs19 fqs_state = RCU_SAVE_DYNTICK;20 j = jiffies_till_first_fqs;21 if (j > HZ) {22 j = HZ;23 jiffies_till_first_fqs = HZ;24 }25 for (;;) {26 rsp->jiffies_force_qs = jiffies + j;27 ret = wait_event_interruptible_timeout(rsp->gp_wq, \28 (rsp->gp_flags & RCU_GP_FLAG_FQS) || (!ACCESS_ONCE(rnp->qsmask) && \29 !rcu_preempt_blocked_readers_cgp(rnp)), j);30 //rnp->qsmask为0, 完成本次gp, 退出循环31 if (!ACCESS_ONCE(rnp->qsmask) && !rcu_preempt_blocked_readers_cgp(rnp))32 break;33 //rsp->gp_flags置位, 发起强制qs后睡眠34 if (ret == 0 || (rsp->gp_flags & RCU_GP_FLAG_FQS)) {35 fqs_state = rcu_gp_fqs(rsp, fqs_state);36 cond_resched();37 //因超时唤醒, 继续睡眠38 } else {39 cond_resched();40 flush_signals(current);41 }42 j = jiffies_till_next_fqs;43 if (j > HZ) {44 j = HZ;45 jiffies_till_next_fqs = HZ;46 } else if (j < 1) {47 j = 1;48 jiffies_till_next_fqs = 1;49 }50 }51 //处理gp结束52 rcu_gp_cleanup(rsp);53 }54 }
首先rsp->gp_flags的RCU_GP_FLAG_INIT位何时置位? 通过调用rcu_start_gp_advanced()(defined in kernel/rcutree.c)或rcu_start_gp()(defined in kernel/rcutree.c)来请求发起一次gp(后者再前者基础上又调整了回调接口的优先级). 在置位RCU_GP_FLAG_INIT后将rsp->wakeup_work加入队列, 在下个定时中断到来时会调用它的回调.
1 //判断当前cpu是否需要一个未启动的gp, 调用者必须关中断以防止竞争 2 static int cpu_needs_another_gp(struct rcu_state *rsp, struct rcu_data *rdp) 3 { 4 int i; 5 //当前进行的gp序号(rsp->completed)与最近已完成的gp序号(rsp->gpnum)不相等 6 //说明当前gp未结束, 不能发起新的gp 7 if (rcu_gp_in_progress(rsp)) 8 return 0; 9 //一个nocb的cpu需要新的gp10 if (rcu_nocb_needs_gp(rsp))11 return 1;12 //这是一个nocb的cpu或不在线的cpu无需gp13 if (!rdp->nxttail[RCU_NEXT_TAIL])14 return 0;15 //该cpu有新注册的回调, 需要新的gp16 if (*rdp->nxttail[RCU_NEXT_READY_TAIL])17 return 1;18 //最近到期的gp序号小于rdp子链表中回调的序号, 需要新的gp19 for (i = RCU_WAIT_TAIL; i < RCU_NEXT_TAIL; i++)20 if (rdp->nxttail[i - 1] != rdp->nxttail[i] && \21 ULONG_CMP_LT(ACCESS_ONCE(rsp->completed), rdp->nxtcompleted[i]))22 return 1;23 return 0;24 }25 //唤醒rcu线程发起一次新的gp, 本接口调用者必须获取rnp->lock并关闭中断26 static void rcu_start_gp_advanced(struct rcu_state *rsp, \27 struct rcu_node *rnp, struct rcu_data *rdp)28 {29 /**30 * 如果rcu线程不存在就无法处理gp请求31 * 如果cpu无需新的gp则不发起gp, 判断是否需要gp见cpu_needs_another_gp()32 *33 **/34 if (!rsp->gp_kthread || !cpu_needs_another_gp(rsp, rdp)) {35 return;36 }37 //设置标志位后再唤醒rcu线程38 rsp->gp_flags = RCU_GP_FLAG_INIT;39 /**40 * 当获取rnp->lock时不能直接唤醒(会导致死锁), 因此选择在中断上下文唤醒41 * irq_work_queue()将rsp->wakeup_work加入irq_work_list并在下个tick时唤醒并处理42 *43 **/44 irq_work_queue(&rsp->wakeup_work);45 }46 //类似rcu_start_gp_advanced()但会提升调用cpu的回调所属的子链表(advance callbacks)47 static void rcu_start_gp(struct rcu_state *rsp)48 {49 struct rcu_data *rdp = this_cpu_ptr(rsp->rda);50 struct rcu_node *rnp = rcu_get_root(rsp);51 /**52 * 如果当前没有正在进行的gp, 在该时间点上任何已注册的回调都满足下个gp要求53 * 此外提升回调优先级还能减少cpu_needs_another_gp()错误的判断无用的gp的概率54 * 因此先提升回调优先级后发起gp55 *56 **/57 rcu_advance_cbs(rsp, rnp, rdp);58 rcu_start_gp_advanced(rsp, rnp, rdp);59 }
rsp->wakeup_work的回调在rcu_init_one()中初始化为rsp_wakeup()(defined in kernel/rcutree.c). 该函数就一个功能, 唤醒rcu_gp_kthread()线程.
1 static void rsp_wakeup(struct irq_work *work)2 {3 struct rcu_state *rsp = container_of(work, struct rcu_state, wakeup_work);4 //唤醒rcu_gp_kthread()以开启一次gp5 wake_up(&rsp->gp_wq);6 }
当rcu线程被唤醒后会调用rcu_gp_init()(defined in kernel/rcutree.c)初始化一次新的gp. 注意该接口返回1才是成功初始化(执行后面的代码), 返回0则重新调度.
1 static int rcu_gp_init(struct rcu_state *rsp) 2 { 3 struct rcu_data *rdp; 4 struct rcu_node *rnp = rcu_get_root(rsp); 5 raw_spin_lock_irq(&rnp->lock); 6 //清除标记位 7 rsp->gp_flags = 0; 8 //已有进行中的gp, 直接返回 9 if (rcu_gp_in_progress(rsp)) {10 raw_spin_unlock_irq(&rnp->lock);11 return 0;12 }13 //发起新的gp(自增当前gp序号)并初始化rcu状态14 rsp->gpnum++;15 trace_rcu_grace_period(rsp->name, rsp->gpnum, "start");16 record_gp_stall_check_time(rsp);17 raw_spin_unlock_irq(&rnp->lock);18 mutex_lock(&rsp->onoff_mutex);19 /**20 * 从根节点开始按广度优先(BF)策略为所有rcu_node设置qs标记位(该节点上报qs所需的cpu)21 * 注意其它cpu只能访问叶子节点, 因此在当前节点初始化完前并不能看到gp在进行22 * 该gp在初始化完成前并不会完成处理, 因为初始化与清理都在rcu线程中执行23 *24 **/25 rcu_for_each_node_breadth_first(rsp, rnp) {26 raw_spin_lock_irq(&rnp->lock);27 rdp = this_cpu_ptr(rsp->rda);28 rcu_preempt_check_blocked_tasks(rnp);29 rnp->qsmask = rnp->qsmaskinit;30 ACCESS_ONCE(rnp->gpnum) = rsp->gpnum;31 WARN_ON_ONCE(rnp->completed != rsp->completed);32 ACCESS_ONCE(rnp->completed) = rsp->completed;33 if (rnp == rdp->mynode)34 rcu_start_gp_per_cpu(rsp, rnp, rdp);35 rcu_preempt_boost_start_gp(rnp);36 trace_rcu_grace_period_init(rsp->name, rnp->gpnum, \37 rnp->level, rnp->grplo, rnp->grphi, rnp->qsmask);38 raw_spin_unlock_irq(&rnp->lock);39 cond_resched();40 }41 mutex_unlock(&rsp->onoff_mutex);42 return 1;43 }
再来看下rsp->gp_flags的RCU_GP_FLAG_FQS位何时置位? 通过调用force_quiescent_state()(defined in kernel/rcutree.c)来请求fqs.
1 static void force_quiescent_state(struct rcu_state *rsp) 2 { 3 unsigned long flags; 4 bool ret; 5 struct rcu_node *rnp; 6 struct rcu_node *rnp_old = NULL; 7 rnp = per_cpu_ptr(rsp->rda, raw_smp_processor_id())->mynode; 8 //向上查找根节点直到rnp为NULL, 此时rnp_old为root node 9 for (; rnp != NULL; rnp = rnp->parent) {10 ret = (ACCESS_ONCE(rsp->gp_flags) & RCU_GP_FLAG_FQS) || \11 !raw_spin_trylock(&rnp->fqslock);12 if (rnp_old != NULL)13 raw_spin_unlock(&rnp_old->fqslock);14 if (ret) {15 rsp->n_force_qs_lh++;16 return;17 }18 rnp_old = rnp;19 }20 raw_spin_lock_irqsave(&rnp_old->lock, flags);21 raw_spin_unlock(&rnp_old->fqslock);22 if (ACCESS_ONCE(rsp->gp_flags) & RCU_GP_FLAG_FQS) {23 rsp->n_force_qs_lh++;24 raw_spin_unlock_irqrestore(&rnp_old->lock, flags);25 return;26 }27 rsp->gp_flags |= RCU_GP_FLAG_FQS;28 raw_spin_unlock_irqrestore(&rnp_old->lock, flags);29 //唤醒rcu线程30 wake_up(&rsp->gp_wq);31 }
rcu线程在检测到RCU_GP_FLAG_FQS位后会调用rcu_gp_fqs()(defined in kernel/rcutree.c)处理. rcu_gp_fqs()原理很简单, 遍历所有最底层的节点, 判断节点rnp->qsmask仍置位的位对应的cpu是否为idle(需要强制设置qs)并(调用rcu_report_qs_rnp())处理.
1 //(通过唤醒rsp->gp_wq)上报所有cpu都经历qs的事件, 调用者必须获取rnp->lock 2 static void rcu_report_qs_rsp(struct rcu_state *rsp, \ 3 unsigned long flags) __releases(rcu_get_root(rsp)->lock) 4 { 5 WARN_ON_ONCE(!rcu_gp_in_progress(rsp)); 6 raw_spin_unlock_irqrestore(&rcu_get_root(rsp)->lock, flags); 7 wake_up(&rsp->gp_wq); 8 } 9 /** 10 * 类似rcu_report_qs_rdp(), 允许一个rcu_node所属的一组cpu上报qs状态 11 * 该节点不一定是叶子节点(但通常情况下都是), 调用该函数时必须获取rnp->lock, 退出前释放 12 * 13 **/ 14 static void rcu_report_qs_rnp(unsigned long mask, struct rcu_state *rsp, \ 15 struct rcu_node *rnp, unsigned long flags) __releases(rnp->lock) 16 { 17 struct rcu_node *rnp_c; 18 //遍历rcu_node节点 19 for (;;) { 20 //该cpu对应掩码已清除, 返回 21 if (!(rnp->qsmask & mask)) { 22 raw_spin_unlock_irqrestore(&rnp->lock, flags); 23 return; 24 } 25 rnp->qsmask &= ~mask; 26 trace_rcu_quiescent_state_report(rsp->name, rnp->gpnum, \ 27 mask, rnp->qsmask, rnp->level, rnp->grplo, rnp->grphi, !!rnp->gp_tasks); 28 //仍有其它cpu对应的比特位置位, 返回 29 if (rnp->qsmask != 0 || rcu_preempt_blocked_readers_cgp(rnp)) { 30 raw_spin_unlock_irqrestore(&rnp->lock, flags); 31 return; 32 } 33 //本节点掩码均已清空, 尝试向上清除父节点的掩码 34 mask = rnp->grpmask; 35 //到达根节点退出循环, 注意此时仍掌握该节点的锁 36 if (rnp->parent == NULL) { 37 break; 38 } 39 raw_spin_unlock_irqrestore(&rnp->lock, flags); 40 rnp_c = rnp; 41 rnp = rnp->parent; 42 raw_spin_lock_irqsave(&rnp->lock, flags); 43 WARN_ON_ONCE(rnp_c->qsmask); 44 } 45 //到达此处则说明当前cpu是(本次gp中)最后一个达到qs的cpu, 唤醒rcu线程执行清理 46 rcu_report_qs_rsp(rsp, flags); 47 } 48 //扫描rcu_node叶子节点, 使用传入的回调处理任何还未增加qs计数的dyntick state 49 static void force_qs_rnp(struct rcu_state *rsp, int (*f)(struct rcu_data *)) 50 { 51 unsigned long bit; 52 int cpu; 53 unsigned long flags; 54 unsigned long mask; 55 struct rcu_node *rnp; 56 //遍历最底层的叶子节点(上层节点会在rcu_report_qs_rnp()中同步修改) 57 rcu_for_each_leaf_node(rsp, rnp) { 58 //为什么要做调度判断 59 cond_resched(); 60 mask = 0; 61 raw_spin_lock_irqsave(&rnp->lock, flags); 62 //没有进行中的gp则无需qs 63 if (!rcu_gp_in_progress(rsp)) { 64 raw_spin_unlock_irqrestore(&rnp->lock, flags); 65 return; 66 } 67 if (rnp->qsmask == 0) { 68 rcu_initiate_boost(rnp, flags); 69 continue; 70 } 71 cpu = rnp->grplo; 72 bit = 1; 73 for (; cpu <= rnp->grphi; cpu++, bit <<= 1) { 74 /** 75 * 遍历rnp所属的所有cpu 76 * 仅当cpu的标记位仍置位且该cpu已经历一个qs时将该位清除 77 * f为dyntick_save_progress_counter或rcu_implicit_dynticks_qs 78 * 79 **/ 80 if ((rnp->qsmask & bit) != 0 && f(per_cpu_ptr(rsp->rda, cpu))) 81 mask |= bit; 82 } 83 //有cpu经历了qs变化则上报状态变化 84 if (mask != 0) { 85 rcu_report_qs_rnp(mask, rsp, rnp, flags); 86 continue; 87 } 88 raw_spin_unlock_irqrestore(&rnp->lock, flags); 89 } 90 rnp = rcu_get_root(rsp); 91 if (rnp->qsmask == 0) { 92 raw_spin_lock_irqsave(&rnp->lock, flags); 93 rcu_initiate_boost(rnp, flags); 94 } 95 } 96 int rcu_gp_fqs(struct rcu_state *rsp, int fqs_state_in) 97 { 98 int fqs_state = fqs_state_in; 99 struct rcu_node *rnp = rcu_get_root(rsp);100 rsp->n_force_qs++;101 if (fqs_state == RCU_SAVE_DYNTICK) {102 force_qs_rnp(rsp, dyntick_save_progress_counter);103 fqs_state = RCU_FORCE_QS;104 } else {105 force_qs_rnp(rsp, rcu_implicit_dynticks_qs);106 }107 //清除标记位防止重入108 if (ACCESS_ONCE(rsp->gp_flags) & RCU_GP_FLAG_FQS) {109 raw_spin_lock_irq(&rnp->lock);110 rsp->gp_flags &= ~RCU_GP_FLAG_FQS;111 raw_spin_unlock_irq(&rnp->lock);112 }113 return fqs_state;114 }
如果rcu线程是因rnp->qsmask为0(所有cpu均经历一次qs)唤醒, 调用rcu_gp_cleanup()(defined in kernel/rcutree.c)执行清理工作.
1 /** 2 * 设置下一个到期值, 该值用来标记回调因此即使cpu处于dyntick-idle也可以处理回调 3 * 本接口调用者必须获取rnp->lock并关闭中断 4 * 5 **/ 6 static unsigned long rcu_cbs_completed(struct rcu_state *rsp, struct rcu_node *rnp) 7 { 8 /** 9 * 如果rcu为idle我们直接等待下个gp, 但我们只有在索引的rnp为根节点时才认为该rcu为idle 10 * 换言之此时一个新的gp已经开始但还未初始化当前非根节点的结构 11 * 12 **/ 13 if (rcu_get_root(rsp) == rnp && rnp->gpnum == rnp->completed) 14 return rnp->completed + 1; 15 //否则等待一个可能的部分gp加上一个完整的gp 16 return rnp->completed + 2; 17 } 18 /** 19 * 为该cpu上任何未设置到期值的回调设置到期值(rdp->nxtcompleted[]) 20 * 同时修改之前已设置到期值(但被证明过于消极)的回调的到期值 21 * 如果rcu处于idle时引用一个非根节点rcu_node并设置回调到期值就会导致这种情况 22 * 具体可见rcu_cbs_completed()返回的到期值 23 * 本接口调用者必须获取rnp->lock并关闭中断 24 * 25 **/ 26 static void rcu_accelerate_cbs(struct rcu_state *rsp, \ 27 struct rcu_node *rnp, struct rcu_data *rdp) 28 { 29 unsigned long c; 30 int i; 31 //如果最后一个节点为空说明整个链表为空 32 //如果RCU_DONE_TAIL节点的next为空说明没有未到期的节点 33 if (!rdp->nxttail[RCU_NEXT_TAIL] || !*rdp->nxttail[RCU_DONE_TAIL]) 34 return; 35 /** 36 * 从最近设置到期值的回调子链表开始查找第一个到期值不为未来的到期值的子链表 37 * 关键点是任何之后的子链表都可以与刚加入的回调设置同一到期值, 即可以合并到一个链表管理 38 * 39 **/ 40 c = rcu_cbs_completed(rsp, rnp); 41 for (i = RCU_NEXT_TAIL - 1; i > RCU_DONE_TAIL; i--) 42 if (rdp->nxttail[i] != rdp->nxttail[i - 1] && \ 43 !ULONG_CMP_GE(rdp->nxtcompleted[i], c)) 44 break; 45 /** 46 * 没有需要设置到期值的回调子链表则直接返回 47 * 注意此处i先自增, 因此后面指向的都是可以使用同一到期值的子链表 48 * 49 **/ 50 if (++i >= RCU_NEXT_TAIL) 51 return; 52 //将下标在i之后的所有的子链表都提升至下标为i的链表类型并修改到期的gp 53 for (; i <= RCU_NEXT_TAIL; i++) { 54 rdp->nxttail[i] = rdp->nxttail[RCU_NEXT_TAIL]; 55 rdp->nxtcompleted[i] = c; 56 } 57 rcu_start_future_gp(rnp, rdp); 58 if (!*rdp->nxttail[RCU_WAIT_TAIL]) 59 trace_rcu_grace_period(rsp->name, rdp->gpnum, "AccWaitCB"); 60 else 61 trace_rcu_grace_period(rsp->name, rdp->gpnum, "AccReadyCB"); 62 } 63 //本接口调用者必须获取rnp->lock并关闭中断 64 static void __rcu_process_gp_end(struct rcu_state *rsp, \ 65 struct rcu_node *rnp, struct rcu_data *rdp) 66 { 67 //判断当前节点是否经过一个gp 68 if (rdp->completed == rnp->completed) { 69 //没有经过一个gp, 修改回调的到期gp 70 rcu_accelerate_cbs(rsp, rnp, rdp); 71 } else { 72 //已经过一个gp, 修改到期的回调所属的链表 73 rcu_advance_cbs(rsp, rnp, rdp); 74 //在该cpu上记录这次到期的gp 75 rdp->completed = rnp->completed; 76 trace_rcu_grace_period(rsp->name, rdp->gpnum, "cpuend"); 77 //如果当前gp(rdp->gpnum)小于已到期的gp(rdp->completed), 更新当前gp 78 if (ULONG_CMP_LT(rdp->gpnum, rdp->completed)) { 79 rdp->gpnum = rdp->completed; 80 rdp->passed_quiesce = 0; 81 } 82 if ((rnp->qsmask & rdp->grpmask) == 0) 83 rdp->qs_pending = 0; 84 } 85 } 86 /** 87 * 移动任何已到期的回调到RCU_DONE_TAIL子链表并设置RCU_NEXT_TAIL子链表中回调的到期值 88 * 本接口调用者必须获取rnp->lock并关闭中断 89 * 90 **/ 91 static void rcu_advance_cbs(struct rcu_state *rsp, struct rcu_node *rnp, 92 struct rcu_data *rdp) 93 { 94 int i, j; 95 if (!rdp->nxttail[RCU_NEXT_TAIL] || !*rdp->nxttail[RCU_DONE_TAIL]) 96 return; 97 //从RCU_WAIT_TAIL开始判断子链表的gp是否到期, 到期则合入RCU_DONE_TAIL子链表 98 for (i = RCU_WAIT_TAIL; i < RCU_NEXT_TAIL; i++) { 99 if (ULONG_CMP_LT(rnp->completed, rdp->nxtcompleted[i]))100 break;101 rdp->nxttail[RCU_DONE_TAIL] = rdp->nxttail[i];102 }103 //恢复(被合入RCU_DONE_TAIL子链表而)乱序的子链表104 for (j = RCU_WAIT_TAIL; j < i; j++)105 rdp->nxttail[j] = rdp->nxttail[RCU_DONE_TAIL];106 for (j = RCU_WAIT_TAIL; i < RCU_NEXT_TAIL; i++, j++) {107 if (rdp->nxttail[j] == rdp->nxttail[RCU_NEXT_TAIL])108 break;109 rdp->nxttail[j] = rdp->nxttail[i];110 rdp->nxtcompleted[j] = rdp->nxtcompleted[i];111 }112 rcu_accelerate_cbs(rsp, rnp, rdp);113 }114 static void rcu_gp_cleanup(struct rcu_state *rsp)115 {116 unsigned long gp_duration;117 int nocb = 0;118 struct rcu_data *rdp;119 struct rcu_node *rnp = rcu_get_root(rsp);120 raw_spin_lock_irq(&rnp->lock);121 gp_duration = jiffies - rsp->gp_start;122 if (gp_duration > rsp->gp_max)123 rsp->gp_max = gp_duration;124 /**125 * 我们知道当前gp已结束, 但其它线程仍以为正在继续126 * 所幸它们也无事可做, 所以我们可以释放锁并在所有rcu_node中标记当前gp的结束127 *128 **/129 raw_spin_unlock_irq(&rnp->lock);130 /**131 * 更新rcu_node->completed可以让cpu无需等待下个gp开始后再处理它们的回调132 * 此外在任意节点记录下个gp起始前保证所有节点记录当前gp结束也可以避免rcu gp初始化竞争133 * 即更新completed的顺序为rnp->rdp->rsp, 因为rsp最后更新所以其它线程不会发起新的gp134 *135 **/136 rcu_for_each_node_breadth_first(rsp, rnp) {137 raw_spin_lock_irq(&rnp->lock);138 ACCESS_ONCE(rnp->completed) = rsp->gpnum;139 rdp = this_cpu_ptr(rsp->rda);140 //只处理当前cpu, 其它cpu在自身rcu_process_callbacks()中处理141 if (rnp == rdp->mynode)142 __rcu_process_gp_end(rsp, rnp, rdp);143 nocb += rcu_future_gp_cleanup(rsp, rnp);144 raw_spin_unlock_irq(&rnp->lock);145 cond_resched();146 }147 rnp = rcu_get_root(rsp);148 raw_spin_lock_irq(&rnp->lock);149 rcu_nocb_gp_set(rnp, nocb);150 //宣布当前gp结束151 rsp->completed = rsp->gpnum;152 trace_rcu_grace_period(rsp->name, rsp->completed, "end");153 rsp->fqs_state = RCU_GP_IDLE;154 rdp = this_cpu_ptr(rsp->rda);155 rcu_advance_cbs(rsp, rnp, rdp);156 if (cpu_needs_another_gp(rsp, rdp))157 rsp->gp_flags = 1;158 raw_spin_unlock_irq(&rnp->lock);159 }
到此终于将rcu线程大致梳理一遍, 让我们回头重新整理下. rcu线程创建后一直睡眠直到其它线程调用rcu_start_gp_advanced()或rcu_start_gp()唤醒rcu线程. rcu线程调用rcu_gp_init()初始化相关变量, 正式发起一次gp. 在发起一次gp后需要等待所有cpu进入qs, 因此rcu线程再次睡眠. 这次睡眠唤醒条件有三个: 所有cpu都经历qs后唤醒rcu线程 cpu请求fqs唤醒rcu线程, 超时调度. 当所有cpu都经历qs后rcu线程才会结束睡眠调用rcu_gp_cleanup()完成本次gp. 但有两个问题还解释不了: 除去fqs什么时候清除qsmask标记位, 为什么要rcu_sched与rcu_bh两个线程以及两个对应的rcu_state. 我们先来看下写者的同步操作, 然后再来看这两个问题.
首先是更新操作synchronize_rcu(), 该接口有三个实现, 分别对应无抢占RCU(PREEMPT_RCU, 包括TREE_RCU, depends on !PREEMPT && SMP与TINY_RCU, depends on !PREEMPT && !SMP), SMP抢占RCU(TREE_PREEMPT_RCU)与UP无抢占RCU(TINY_PREEMPT_RCU)三种情况.
1 #ifndef CONFIG_PREEMPT_RCU 2 //defined in include/linux/rcupdate.h 3 static inline void synchronize_rcu(void) 4 { 5 synchronize_sched(); 6 } 7 #else 8 #ifdef CONFIG_TREE_PREEMPT_RCU 9 //defined in kernel/rcutree.c10 void synchronize_rcu(void)11 {12 rcu_lockdep_assert(!lock_is_held(&rcu_bh_lock_map) && \13 !lock_is_held(&rcu_lock_map) && !lock_is_held(&rcu_sched_lock_map), \14 "Illegal synchronize_rcu() in RCU read-side critical section");15 if (!rcu_scheduler_active)16 return;17 if (rcu_expedited)18 synchronize_rcu_expedited();19 else20 wait_rcu_gp(call_rcu);21 }22 EXPORT_SYMBOL_GPL(synchronize_rcu);23 #endif24 #ifdef CONFIG_TINY_PREEMPT_RCU25 //defined in kernel/rcutiny.c26 void synchronize_rcu(void)27 {28 rcu_lockdep_assert(!lock_is_held(&rcu_bh_lock_map) && \29 !lock_is_held(&rcu_lock_map) && !lock_is_held(&rcu_sched_lock_map), \30 "Illegal synchronize_rcu() in RCU read-side critical section");31 #ifdef CONFIG_DEBUG_LOCK_ALLOC32 if (!rcu_scheduler_active)33 return;34 #endif35 WARN_ON_ONCE(rcu_preempt_running_reader());36 if (!rcu_preempt_blocked_readers_any())37 return;38 if (rcu_expedited)39 synchronize_rcu_expedited();40 else41 rcu_barrier();42 }43 EXPORT_SYMBOL_GPL(synchronize_rcu);44 #endif45 #endif
对于SMP无抢占RCU其实现见kernel/rcutree.c, 该源文件下均为SMP RCU的api. rcu_blocking_is_gp()用于判断是否有大于一个cpu在线, 对于UP架构或SMP且支持cpu热插拔的架构如果仅存在一个cpu在线就无需后面的步骤. rcu_expedited是内核参数, 默认为0.
1 /** 2 * 等待直到rcu-sched gp完成 3 * 只有当一个完整的rcu-sched qp完成(换言之所有当前执行的读临界区结束(rcu_read_lock()))控制会返回给调用者 4 * 意味着任何preempt_disable的代码, 包括NMI与其它非线程的硬件中断都会在同步原语返回前完成 5 * 但是这不包括软中断, 在部分内核中软中断可以运行在进程上下文且可被阻塞 6 * 这进一步保证了内存顺序, 在多cpu的系统上当同步原语返回时每个cpu都保证在最近一次读临界区后执行过一次完整的内存屏障 7 * 更进一步如果cpu A调用synchronize_rcu()并在cpu B上(调度)返回, 那么A与B同时执行一次内存屏障 8 * 9 **/10 void synchronize_sched(void)11 {12 rcu_lockdep_assert(!lock_is_held(&rcu_bh_lock_map) && \13 !lock_is_held(&rcu_lock_map) && !lock_is_held(&rcu_sched_lock_map), \14 "Illegal synchronize_sched() in RCU-sched read-side critical section");15 if (rcu_blocking_is_gp())16 return;17 if (rcu_expedited)18 synchronize_sched_expedited();19 else20 wait_rcu_gp(call_rcu_sched);21 }22 EXPORT_SYMBOL_GPL(synchronize_sched);23 //类似synchronize_sched(), 不同的是它等待rcu_read_lock_bh()临界区结束24 void synchronize_rcu_bh(void)25 {26 rcu_lockdep_assert(!lock_is_held(&rcu_bh_lock_map) &&27 !lock_is_held(&rcu_lock_map) &&28 !lock_is_held(&rcu_sched_lock_map),29 "Illegal synchronize_rcu_bh() in RCU-bh read-side critical section");30 if (rcu_blocking_is_gp())31 return;32 if (rcu_expedited)33 synchronize_rcu_bh_expedited();34 else35 wait_rcu_gp(call_rcu_bh);36 }37 EXPORT_SYMBOL_GPL(synchronize_rcu_bh);38 //对于单cpu在线时一次上下文切换就是一个rcu-sched与rcu-bh的gp, 任何阻塞等待即完成一个gp39 static inline int rcu_blocking_is_gp(void)40 {41 int ret;42 //rcu读临界区检查43 might_sleep();44 preempt_disable();45 ret = num_online_cpus() <= 1;46 preempt_enable();47 return ret;48 }
wait_rcu_gp()(defined in kernel/rcupdate.c)会等待所有rcu读者结束后再更新, 我们来看下该接口. 由于未定义DEBUG_OBJECTS_RCU_HEAD, init_rcu_head_on_stack()为空. init_completion()初始化completion的等待队列. 传入的crf参数为call_rcu_sched(), 我们仅分析该接口的SMP无抢占的实现(defined in kernel/rcutree.c). 顺带一提的是call_rcu(), 在未定义PREEMPT_RCU时也宏定义为call_rcu_sched(). 在call_rcu_sched()返回后会等待rcu.completion再返回.
1 struct rcu_synchronize { 2 struct rcu_head head; 3 struct completion completion; 4 }; 5 void wait_rcu_gp(call_rcu_func_t crf) 6 { 7 struct rcu_synchronize rcu; 8 init_rcu_head_on_stack(&rcu.head); 9 init_completion(&rcu.completion);10 crf(&rcu.head, wakeme_after_rcu);11 wait_for_completion(&rcu.completion);12 destroy_rcu_head_on_stack(&rcu.head);13 }14 EXPORT_SYMBOL_GPL(wait_rcu_gp);
call_rcu_sched()是__call_rcu()(defined in kernel/rcutree.c)的封装. 后者会初始化传入的rcu.head(head->func为wakeme_after_rcu(), 后面会讲到该回调作用)并将该节点加入rdp->nxtlist链表. 该链表的属性需稍稍解释下. 首先rcu_data.nxtlist是一个由该cpu上的rcu_head组成的单向链表, 其节点成员按回调的到期值顺序排布. 这些节点按到期值分为四类, 分别归属RCU_DONE_TAIL, RCU_WAIT_TAIL, RCU_NEXT_READY_TAIL, RCU_NEXT_TAIL. 其中RCU_DONE_TAIL类节点属于已到期的节点(节点的gp已完成), RCU_WAIT_TAIL类节点正在等待当前gp完成, RCU_NEXT_READY_TAIL类节点是在当前gp结束前加入链表的, RCU_NEXT_TAIL为新加入的节点(可能为当前gp结束后才加入的, 也可能为之前, 但不确定). rcu_data.nxttail[]是rcu_data.nxttail链表的指针的数组, 记录了每类节点的最后一个成员, 用于快速锚定某一类节点, 而rcu_data.nxtcompleted[]记录了不同类的节点回调的到期值. 当某一类节点不存在时rcu_data.nxttail[]对应下标的成员并不是空, 而是等于前一下标的成员. *rcu_data.nxttail[RCU_NEXT_TAIL]永远为空, 因为它指向链表的结束. 最后调用rcu核心处理函数__call_rcu_core()(defined in kernel/rcutree.c).
1 void call_rcu_sched(struct rcu_head *head, void (*func)(struct rcu_head *rcu)) 2 { 3 __call_rcu(head, func, &rcu_sched_state, -1, 0); 4 } 5 EXPORT_SYMBOL_GPL(call_rcu_sched); 6 static void __call_rcu(struct rcu_head *head, \ 7 void (*func)(struct rcu_head *rcu), struct rcu_state *rsp, int cpu, bool lazy) 8 { 9 unsigned long flags;10 struct rcu_data *rdp;11 //初始化rcu_head12 WARN_ON_ONCE((unsigned long)head & 0x3);13 debug_rcu_head_queue(head);14 head->func = func;15 head->next = NULL;16 local_irq_save(flags);17 rdp = this_cpu_ptr(rsp->rda);18 if (unlikely(rdp->nxttail[RCU_NEXT_TAIL] == NULL) || cpu != -1) {19 int offline;20 if (cpu != -1)21 rdp = per_cpu_ptr(rsp->rda, cpu);22 offline = !__call_rcu_nocb(rdp, head, lazy);23 WARN_ON_ONCE(offline);24 local_irq_restore(flags);25 return;26 }27 ACCESS_ONCE(rdp->qlen)++;28 if (lazy)29 rdp->qlen_lazy++;30 else31 rcu_idle_count_callbacks_posted();32 smp_mb();33 //将head挂入链表34 *rdp->nxttail[RCU_NEXT_TAIL] = head;35 rdp->nxttail[RCU_NEXT_TAIL] = &head->next;36 if (__is_kfree_rcu_offset((unsigned long)func))37 trace_rcu_kfree_callback(rsp->name, head, \38 (unsigned long)func, rdp->qlen_lazy, rdp->qlen);39 else40 trace_rcu_callback(rsp->name, head, rdp->qlen_lazy, rdp->qlen);41 __call_rcu_core(rsp, rdp, head, flags);42 local_irq_restore(flags);43 }
__call_rcu_core()首先判断cpu是否dyntick-idle, 若处于idle则请求软中断. 如果该cpu等待过久则需要加速处理, 方式是如果正在进行一个gp则强制一次qs, 否则启动一个新的gp.
1 static void __call_rcu_core(struct rcu_state *rsp, \ 2 struct rcu_data *rdp, struct rcu_head *head, unsigned long flags) 3 { 4 //如果当前cpu在线且处于idle则请求软中断强制一次调度 5 if (rcu_is_cpu_idle() && cpu_online(smp_processor_id())) 6 invoke_rcu_core(); 7 //如果禁止中断或cpu不在线无法调度则不请求rcu core 8 if (irqs_disabled_flags(flags) || cpu_is_offline(smp_processor_id())) 9 return;10 //如果回调过多或等待过久则强制一次gp11 if (unlikely(rdp->qlen > rdp->qlen_last_fqs_check + qhimark)) {12 //判断rdp与最近到期的gp是否同步13 rcu_process_gp_end(rsp, rdp);14 //如果另一个gp已经发起rnp->gpnum会增加, 在此检测rdp->gpnum的更新15 check_for_new_grace_period(rsp, rdp);16 //如果当前无进行中的gp则开启一次gp17 if (!rcu_gp_in_progress(rsp)) {18 struct rcu_node *rnp_root = rcu_get_root(rsp);19 raw_spin_lock(&rnp_root->lock);20 rcu_start_gp(rsp);21 raw_spin_unlock(&rnp_root->lock);22 } else {23 rdp->blimit = LONG_MAX;24 if (rsp->n_force_qs == rdp->n_force_qs_snap && \25 *rdp->nxttail[RCU_DONE_TAIL] != head)26 force_quiescent_state(rsp);27 rdp->n_force_qs_snap = rsp->n_force_qs;28 rdp->qlen_last_fqs_check = rdp->qlen;29 }30 }31 }32 static void invoke_rcu_core(void)33 {34 if (cpu_online(smp_processor_id()))35 raise_softirq(RCU_SOFTIRQ);36 }
让我们先来看看软中断中处理了什么. 前文分析rcu初始化时我们看到软中断的回调是rcu_process_callbacks()(defined in kernel/rcutree.c). 该回调中遍历rcu_state调用__rcu_process_callbacks()(defined in kernel/rcutree.c), 后者完成三件事: 处理其它cpu上结束的gp, 根据qs更新rcu状态并判断是否需要请求发起新的qp, 处理rcu回调.
1 static void rcu_process_callbacks(struct softirq_action *unused) 2 { 3 struct rcu_state *rsp; 4 if (cpu_is_offline(smp_processor_id())) 5 return; 6 trace_rcu_utilization("Start RCU core"); 7 for_each_rcu_flavor(rsp) 8 __rcu_process_callbacks(rsp); 9 trace_rcu_utilization("End RCU core");10 }11 //给定rcu_state与rcu_data负责rcu核心处理, 必须被拥有该rdp的cpu调用12 static void __rcu_process_callbacks(struct rcu_state *rsp)13 {14 unsigned long flags;15 struct rcu_data *rdp = __this_cpu_ptr(rsp->rda);16 WARN_ON_ONCE(rdp->beenonline == 0);17 //处理其它cpu上结束的gp18 rcu_process_gp_end(rsp, rdp);19 //根据最近的qs状态更新rcu20 rcu_check_quiescent_state(rsp, rdp);21 //该cpu是否需要请求一个新的gp22 local_irq_save(flags);23 if (cpu_needs_another_gp(rsp, rdp)) {24 raw_spin_lock(&rcu_get_root(rsp)->lock);25 rcu_start_gp(rsp);26 raw_spin_unlock_irqrestore(&rcu_get_root(rsp)->lock, flags);27 } else {28 local_irq_restore(flags);29 }30 //处理回调31 if (cpu_has_callbacks_ready_to_invoke(rdp))32 invoke_rcu_callbacks(rsp, rdp);33 }
rcu_process_gp_end()(defined in kernel/rcutree.c)是__rcu_process_gp_end()的封装, 后者在rcu线程清理时有调用(rcu线程仅处理当前cpu的节点). 需要注意的是判断gp是否在进行时我们用rsp->gpnum与rsp->completed比较, 而判断gp是否结束时我们用rdp->completed与rnp->completed比较, 原因前文也有提到: 当发起gp时先修改rsp->gpnum而结束gp时最后修改rsp->completed且所有结构的rsp->gpnum/rsp->completed/rnp->completed都是在rcu线程中修改, 而rdp->completed是在对应cpu上被修改的, 所以这么判断最合理.
1 //仅在当前gp结束时修改cpu回调优先级(在链表中的顺序), 必须被拥有该rdp的cpu调用 2 static void rcu_process_gp_end(struct rcu_state *rsp, struct rcu_data *rdp) 3 { 4 unsigned long flags; 5 struct rcu_node *rnp; 6 local_irq_save(flags); 7 rnp = rdp->mynode; 8 //当前gp未结束或加锁失败直接返回 9 if (rdp->completed == ACCESS_ONCE(rnp->completed) || \10 !raw_spin_trylock(&rnp->lock)) {11 local_irq_restore(flags);12 return;13 }14 __rcu_process_gp_end(rsp, rnp, rdp);15 raw_spin_unlock_irqrestore(&rnp->lock, flags);16 }
rcu_check_quiescent_state()(defined in kernel/rcutree.c)有两个作用, 检测是否有新的gp已经发起, 检测是否有qs需要上报, 注意如果有新的qp发起原gp的qs就失效了, 所以先判断gp后判断qs. 如果该cpu发现需要请求新的gp则调用rcu_start_gp()唤醒rcu线程.
1 /** 2 * 更新cpu对应的rcu_data记录下新通知的gp 3 * 不论是自己发起的还是侦测到别人发起的gp 4 * 本接口调用者必须获取rnp->lock并关闭中断 5 * 6 **/ 7 static void __note_new_gpnum(struct rcu_state *rsp, \ 8 struct rcu_node *rnp, struct rcu_data *rdp) 9 {10 if (rdp->gpnum != rnp->gpnum) {11 rdp->gpnum = rnp->gpnum;12 trace_rcu_grace_period(rsp->name, rdp->gpnum, "cpustart");13 rdp->passed_quiesce = 0;14 rdp->qs_pending = !!(rnp->qsmask & rdp->grpmask);15 zero_cpu_stall_ticks(rdp);16 }17 }18 static void note_new_gpnum(struct rcu_state *rsp, struct rcu_data *rdp)19 {20 unsigned long flags;21 struct rcu_node *rnp;22 local_irq_save(flags);23 rnp = rdp->mynode;24 if (rdp->gpnum == ACCESS_ONCE(rnp->gpnum) || \25 !raw_spin_trylock(&rnp->lock)) {26 local_irq_restore(flags);27 return;28 }29 __note_new_gpnum(rsp, rnp, rdp);30 raw_spin_unlock_irqrestore(&rnp->lock, flags);31 }32 //判断最近检查后到现在是否有发起新的gp, 有则记录下来, 必须被拥有该rdp的cpu调用33 static int check_for_new_grace_period(struct rcu_state *rsp, struct rcu_data *rdp)34 {35 unsigned long flags;36 int ret = 0;37 local_irq_save(flags);38 if (rdp->gpnum != rsp->gpnum) {39 note_new_gpnum(rsp, rdp);40 ret = 1;41 }42 local_irq_restore(flags);43 return ret;44 }45 //记录并上报指定cpu的qs事件46 static void rcu_report_qs_rdp(int cpu, struct rcu_state *rsp, struct rcu_data *rdp)47 {48 unsigned long flags;49 unsigned long mask;50 struct rcu_node *rnp;51 rnp = rdp->mynode;52 raw_spin_lock_irqsave(&rnp->lock, flags);53 if (rdp->passed_quiesce == 0 || rdp->gpnum != rnp->gpnum || \54 rnp->completed == rnp->gpnum) {55 //该qs所属的gp已结束因此不上报事件, 我们需要一个基于当前gp的新qs56 rdp->passed_quiesce = 0;57 raw_spin_unlock_irqrestore(&rnp->lock, flags);58 return;59 }60 mask = rdp->grpmask;61 if ((rnp->qsmask & mask) == 0) {62 raw_spin_unlock_irqrestore(&rnp->lock, flags);63 } else {64 rdp->qs_pending = 0;65 rcu_accelerate_cbs(rsp, rnp, rdp);66 rcu_report_qs_rnp(mask, rsp, rnp, flags);67 }68 }69 /**70 * 该接口两个作用:71 * 检测是否有新的gp且当前cpu未注意到, 如果有则记录下来72 * 检测是否该cpu已经经历一次当前gp的qs, 如果是则记录下来73 *74 **/75 static void rcu_check_quiescent_state(struct rcu_state *rsp, struct rcu_data *rdp)76 {77 //检测rcu线程是否已发起新的gp78 if (check_for_new_grace_period(rsp, rdp))79 return;80 //判断当前gp是否还需本cpu处理自己的qs, 无需处理则返回81 if (!rdp->qs_pending)82 return;83 //在本次gp开始后是否已基尼一个qs, 没有则返回等待下次调用84 if (!rdp->passed_quiesce)85 return;86 //向rcu上报qs事件87 rcu_report_qs_rdp(rdp->cpu, rsp, rdp);88 }
cpu_has_callbacks_ready_to_invoke()(defined in kernel/rcutree.c)检测rdp->nxttail中是否有属于RCU_DONE_TAIL的子链表, 如果有则调用invoke_rcu_callbacks()(defined in kernel/rcutree.c)处理. 未定义RCU_BOOST时rcu_scheduler_fully_active在rcu_scheduler_really_started()(defined in kernel/rcutree_plugin.h)中初始化为1, rcu->boost为0, 故调用rcu_do_batch()(defined in kernel/rcutree.c)直接处理(否则走invoke_rcu_callbacks_kthread()在线程中处理).
1 static void rcu_do_batch(struct rcu_state *rsp, struct rcu_data *rdp) 2 { 3 unsigned long flags; 4 struct rcu_head *next, *list, **tail; 5 long bl, count, count_lazy; 6 int i; 7 //无回调则直接退出 8 if (!cpu_has_callbacks_ready_to_invoke(rdp)) { 9 trace_rcu_batch_start(rsp->name, rdp->qlen_lazy, rdp->qlen, 0);10 trace_rcu_batch_end(rsp->name, 0, !!ACCESS_ONCE(rdp->nxtlist), \11 need_resched(), is_idle_task(current), rcu_is_callbacks_kthread());12 return;13 }14 //关中断防止中断调用call_rcu()引起竞争15 local_irq_save(flags);16 WARN_ON_ONCE(cpu_is_offline(smp_processor_id()));17 bl = rdp->blimit;18 trace_rcu_batch_start(rsp->name, rdp->qlen_lazy, rdp->qlen, bl);19 list = rdp->nxtlist;20 //将RCU_DONE_TAIL链表取出21 rdp->nxtlist = *rdp->nxttail[RCU_DONE_TAIL];22 *rdp->nxttail[RCU_DONE_TAIL] = NULL;23 tail = rdp->nxttail[RCU_DONE_TAIL];24 for (i = RCU_NEXT_SIZE - 1; i >= 0; i--)25 if (rdp->nxttail[i] == rdp->nxttail[RCU_DONE_TAIL])26 rdp->nxttail[i] = &rdp->nxtlist;27 local_irq_restore(flags);28 count = count_lazy = 0;29 while (list) {30 next = list->next;31 //用途?32 prefetch(next);33 debug_rcu_head_unqueue(list);34 //处理节点的回调35 if (__rcu_reclaim(rsp->name, list))36 count_lazy++;37 list = next;38 //超过batch处理上限39 if (++count >= bl && (need_resched() || \40 (!is_idle_task(current) && !rcu_is_callbacks_kthread())))41 break;42 }43 local_irq_save(flags);44 trace_rcu_batch_end(rsp->name, count, !!list, need_resched(), \45 is_idle_task(current), rcu_is_callbacks_kthread());46 //回调未处理完47 if (list != NULL) {48 *tail = rdp->nxtlist;49 rdp->nxtlist = list;50 for (i = 0; i < RCU_NEXT_SIZE; i++)51 if (&rdp->nxtlist == rdp->nxttail[i])52 rdp->nxttail[i] = tail;53 else54 break;55 }56 smp_mb();57 rdp->qlen_lazy -= count_lazy;58 ACCESS_ONCE(rdp->qlen) -= count;59 rdp->n_cbs_invoked += count;60 if (rdp->blimit == LONG_MAX && rdp->qlen <= qlowmark)61 rdp->blimit = blimit;62 if (rdp->qlen == 0 && rdp->qlen_last_fqs_check != 0) {63 rdp->qlen_last_fqs_check = 0;64 rdp->n_force_qs_snap = rsp->n_force_qs;65 } else if (rdp->qlen < rdp->qlen_last_fqs_check - qhimark)66 rdp->qlen_last_fqs_check = rdp->qlen;67 WARN_ON_ONCE((rdp->nxtlist == NULL) != (rdp->qlen == 0));68 local_irq_restore(flags);69 //再次请求rcu core处理剩余回调70 if (cpu_has_callbacks_ready_to_invoke(rdp))71 invoke_rcu_core();72 }73 //处理rcu回调, 如果不支持rcu boosting则直接调用回调, 苟泽唤醒per cpu线程处理74 static void invoke_rcu_callbacks(struct rcu_state *rsp, struct rcu_data *rdp)75 {76 if (unlikely(!ACCESS_ONCE(rcu_scheduler_fully_active)))77 return;78 if (likely(!rsp->boost)) {79 rcu_do_batch(rsp, rdp);80 return;81 }82 invoke_rcu_callbacks_kthread();83 }
__rcu_reclaim()(defined in kernel/rcu.h)会调用rcu_head->func. 前文提到过synchronize_rcu()中会将该回调初始化为wakeme_after_rcu()(defined in kernel/rcupdate.c). 因为synchronize_rcu()会阻塞需要在rcu同步完成时被唤醒, 如果是call_rcu()注册的rcu_head该回调是从call_rcu()参数中传递的自己的回调.
1 static inline bool __rcu_reclaim(char *rn, struct rcu_head *head) 2 { 3 unsigned long offset = (unsigned long)head->func; 4 if (__is_kfree_rcu_offset(offset)) { 5 RCU_TRACE(trace_rcu_invoke_kfree_callback(rn, head, offset)); 6 kfree((void *)head - offset); 7 return 1; 8 } else { 9 RCU_TRACE(trace_rcu_invoke_callback(rn, head));10 head->func(head);11 return 0;12 }13 }
让我们看下wakeme_after_rcu(), 实现很简单, 直接调用complete(). complete()(defined in kernel/sched/core.c)是调度器代码, 用于唤醒一个等待(该completion.done非零的)线程. 在__wake_up_common()中会遍历complete.wait中任务链表并执行每个任务的回调.
1 static void wakeme_after_rcu(struct rcu_head *head) 2 { 3 struct rcu_synchronize *rcu; 4 rcu = container_of(head, struct rcu_synchronize, head); 5 complete(&rcu->completion); 6 } 7 void complete(struct completion *x) 8 { 9 unsigned long flags;10 spin_lock_irqsave(&x->wait.lock, flags);11 x->done++;12 __wake_up_common(&x->wait, TASK_NORMAL, 1, 0, NULL);13 spin_unlock_irqrestore(&x->wait.lock, flags);14 }15 EXPORT_SYMBOL(complete);16 /**17 * 核心唤醒函数, 非排除唤醒(nr_exclusive为0)会唤醒所有任务18 * 我们可能会唤醒一个已启动但未设置TASK_RUNNING的任务19 * 这种情况下try_to_wake_up()会返回0, 我们会跳过它继续搜索队列20 *21 **/22 static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,23 int nr_exclusive, int wake_flags, void *key)24 {25 wait_queue_t *curr, *next;26 list_for_each_entry_safe(curr, next, &q->task_list, task_list) {27 unsigned flags = curr->flags;28 if (curr->func(curr, mode, wake_flags, key) && \29 (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)30 break;31 }32 }
具体这些阻塞的线程是如何唤醒的, 让我们回到wait_rcu_gp(), 它调用的wait_for_completion()(defined in kernel/sched/core.c)是调度器接口, 后者的作用是等待直到x->done非零为止. 让我们看下它的实现, wait_for_completion()调用wait_for_common(), 最终调用do_wait_for_common(). do_wait_for_common()在栈上定义并初始化一个wait_queue_t, 其func回调为default_wake_function(), 然后循环调用action(传入的函数指针为schedule_timeout)直到x->done非零.
1 int default_wake_function(wait_queue_t *curr, \ 2 unsigned mode, int wake_flags, void *key) 3 { 4 return try_to_wake_up(curr->private, mode, wake_flags); 5 } 6 EXPORT_SYMBOL(default_wake_function); 7 static inline long __sched do_wait_for_common(struct completion *x, \ 8 long (*action)(long), long timeout, int state) 9 {10 if (!x->done) {11 DECLARE_WAITQUEUE(wait, current);12 __add_wait_queue_tail_exclusive(&x->wait, &wait);13 do {14 if (signal_pending_state(state, current)) {15 timeout = -ERESTARTSYS;16 break;17 }18 __set_current_state(state);19 spin_unlock_irq(&x->wait.lock);20 timeout = action(timeout);21 spin_lock_irq(&x->wait.lock);22 } while (!x->done && timeout);23 __remove_wait_queue(&x->wait, &wait);24 if (!x->done)25 return timeout;26 }27 x->done--;28 return timeout : 1;29 }30 static inline long __sched __wait_for_common(struct completion *x, \31 long (*action)(long), long timeout, int state)32 {33 might_sleep();34 spin_lock_irq(&x->wait.lock);35 timeout = do_wait_for_common(x, action, timeout, state);36 spin_unlock_irq(&x->wait.lock);37 return timeout;38 }39 static long __sched wait_for_common(struct completion *x, long timeout, int state)40 {41 return __wait_for_common(x, schedule_timeout, timeout, state);42 }43 void __sched wait_for_completion(struct completion *x)44 {45 wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE);46 }47 EXPORT_SYMBOL(wait_for_completion);
让我们再来思考一个问题, 什么线程会处理rcu_head的回调? 从前文看到rcu线程并不会做这件事, 调用synchronize_rcu()/call_rcu()的线程可能会请求软中断(cpu idle时), 还有呢? 让我们回溯下代码, invoke_rcu_core()有两处调用, 分别是rcu_do_batch()与rcu_check_callbacks(), 后者是定时中断处理函数update_process_times()(defined in kernel/timer.c). 即每个tick系统都会检查rcu的状态并推动状态机变化, 时间所限就不仔细分析了.
至此rcu的基本实现大致梳理完了. 小结一下:
1. rcu的原理是推迟写者更新直到所有cpu都调度一次, 由于调度过程必定会做内存屏障保证了读者看到写者的修改.2. 由第一点得出在非抢占内核上读者在临界区不能有阻塞操作, 否则无法正常完成grace period.3. 不同的模块在使用不同的rcu, 如何统一grace period? 需要建立rcu线程. rcu线程负责发起一次grace period, 强制idle的cpu经历qs及判断一次grace period的结束.4. 在定时中断中会检查rcu状态, 即时将合适的阻塞线程唤醒.5. synchronize_rcu()与call_rcu()的区别是前者是阻塞的同步操作, 后者是非阻塞的同步操作, 但后者返回时回调不一定已处理.