以内核2.6.21为例,说明经典RCU的实现。(出于畏难情绪,作者暂不分析可抢占RCU、可睡眠RCU)
RCU 是READCOPY UPDATE的简写,设计思想的来源是,对读写锁进行优化,减少写者之间的同步,即如果同时有几个写者进行竞争,那么写者就对资源进行拷贝,从而允许多个写者对资源进行修改,最后由系统决定什么时候更新。
上面描述,引出了RCU的关键问题,即系统如何决定更新资源.。
经典RCU,人为的规定了系统更新资源的时间点,那就是所有CPU经历了一次进程切换(grace period,具体为什么这么叫,记住就行。。。)。为什么这么设计?因为RCU读者的实现就是关抢占执行读 取,读完了当然就可以进程切换了,也就等于是写者可以操作临界区了。那么就自然可以想到,内核会设计两个元素,来分别表示写者被挂起的起始点,以及每cpu变量,来表示该cpu是否经过了一次进程切换(quies state)。就是说,当写者被挂起后,
1)重置每cpu变量,值为1。
2)当某个cpu经历一次进程切换后,就将自己的变量设为0。
3)当所有的cpu变量都为0后,就可以唤醒写者了。
下面我们来分别看linux里是如何完成这三步的。为了更好的理解linuxrcu的设计思路,我们先直接给出rcu里的几个数据结构:
struct rcu_ctrlblk { long cur; long completed; cpumask_t cpumask; };
上述结构是系统相关,那么下面的结构,就是与具体的写者相关了:
struct rcu_data { long quiescbatch; int passed_quiesc; long batch; struct rcu_head *nxtlist; struct rcu_head **nxttail; struct rcu_head *curlist; struct rcu_head **curtail; struct rcu_head *donelist; struct rcu_head **donetail; };
上面的结构,要达到的作用是,跟踪单个CPU上的rcu事务。
1) 一个flag标志passed_quiesc,表示当前cpu是否已经切换过进程了,
看到这里,我们会有个疑问,进程切换会把此标志置为1,那什么时候置为0?
我们可以推测,应该是写者被加入到等待队列的时候;
2)一个计数batch表示本次被阻塞的写者,需要在哪个graceperiod之后被激活;
3)一个计数quiescbatch,用来比较当前cpu是否处于等待进程切换完成。
剩下的三组指针我们下面介绍。
rcu写者的整体流程,假设系统中出现rcu写者阻塞,那么流程如下:
1) &writer被添加到CPU[n]每cpu变量rcu_data的nxtlist,
这个链表代表需要提交给rcu处理的回调;
2)CPU[n]时钟中断检测到自己的nxtlist不为空,因此唤醒rcutasklet
3)rcu的软中断 rcu_process_callbacks会挨个检查本CPU的三个链表,
3.1)先看nxtlist里有没有待处理的回调,如果有的话,说明有写者待处理,那么还要分两种情况:
3.1.1)如果系统第一次出现写者阻塞,也即之前的写者都已经处理完毕,那么此时curlist链表一定为空(curlist专门存放已被rcu检测到的写者请求),于是就把nxtlist里的所有成员都移动到curlist指向,并把当前CPU需要等待的graceperiod周期rdp->batch设置为当前系统处理的graceperiod的下一个grace周期,即rcp->cur+ 1。由于这算是一个新的graceperiod,即start_rcu_batch,于是还接着需要增加系统的graceperiod计数,即rcp->cur++,同时,将全局的cpusmask设置为全f,代表新的graceperiod,需要检测所有的cpu是否都经过了一次进程切换。代码如下:
void __rcu_process_callbacks(struct rcu_ctrlblk *rcp, struct rcu_data *rdp) { if (rdp->nxtlist && !rdp->curlist) { move_local_cpu_nxtlist_to_curlist(); rdp->batch = rcp->cur + 1; if (!rcp->next_pending) { rcp->next_pending = 1; rcp->cur++; cpus_andnot(rcp->cpumask, cpu_online_map, nohz_cpu_mask); } } }
接着跳转至3.2。
3.1.2)如果系统之前已经有写者在被rcu监控着,但还没来得及经过一个graceperiod,这个时候curlist不为空,nxtlist也不为空,写者会被加入nxtlist中。由于curlist不为空,说明上个rcu周期的写者还没有处理完,于是不会将本次阻塞的写者加入curlist,一直到上次的curlist里回调被处理完(都移动到了donelist),才会将后来的写者纳入RCU考虑(移动到curlist)。进入下一步。
3.2)rcu_process_callbacks调用每CPU函数rcu_check_quiescent_state开始监控,所有的CPU是否会经历一个进程切换。 这个函数是如何得知需要开始监控的? 答案在于quiescbatch与全局的rcp->cur比较。 初始化时rdp->quiescbatch =rcp->completed = rcp->cur。 由于3.1有新graceperiod开启,所以rcp->cur已经加1了,于是rdp->quiescbatch和rcp->curr不等,进而将此cpu的rdp->passed_quiesc设为0,表示这个周期开始,我要等待这个cpu经历一个进程切换,等待该CPU将passed_quiesc置为1。即与前面讲到的passed_quiesc标志置0的时机吻合。最后将rdp->quiescbatch置为 rcp->cur,以免下次再进入软中断里将passed_quiesc重复置0。
void rcu_check_quiescent_state(struct rcu_ctrlblk *rcp, struct rcu_data *rdp) { if (rdp->quiescbatch != rcp->cur) { /* start new grace period: */ rdp->qs_pending = 1; rdp->passed_quiesc = 0; rdp->quiescbatch = rcp->cur; return; } }
3.3)本次软中断结束,下次软中断到来,再次进入rcu_check_quiescent_state进行检测,如果本CPU的rdp->passed_quiesc已经置1,则需要cpu_quiet将本CPU标志位从全局的rcp->cpumask中清除,如果cpumask为0了,则说明自上次RCU写者被挂起以来,所有CPU都已经历了一次进程切换,于是本次rcu等待周期结束,将rcp->completed置为rcp->cur,重置cpumask为全f,并尝试重新开启一个新的grace period。我们可以看到RCU用了如此多的同步标志,却少用spinlock锁,是多么巧妙的设计,不过这也提高了理解的难度。
void rcu_check_quiescent_state(struct rcu_ctrlblk *rcp, struct rcu_data *rdp) { if (rdp->quiescbatch != rcp->cur) { /* start new grace period: */ rdp->qs_pending = 1; rdp->passed_quiesc = 0; rdp->quiescbatch = rcp->cur; return; } if (!rdp->passed_quiesc) return; /*this cpu has passed a quies state*/ if (likely(rdp->quiescbatch == rcp->cur)) { cpu_clear(cpu, rcp->cpumask); if (cpus_empty(rcp->cpumask)) rcp->completed = rcp->cur; } }
3.4)下次再进入rcu软中断__rcu_process_callbacks,发现rdp->batch已经比rcp->completed小了(因为上一步骤中,后者增大了),则将rdp->curlist上的回调移动到rdp->donelist里,接着还会再次进入rcu_check_quiescent_state,但是由于当前CPU的rdp->qs_pending已经为1了,所以不再往下清除cpu掩码。__rcu_process_callbacks
代码变成了:
void __rcu_process_callbacks(struct rcu_ctrlblk *rcp, struct rcu_data *rdp) { if (rdp->curlist && !rcu_batch_before(rcp->completed, rdp->batch)) { *rdp->donetail = rdp->curlist; rdp->donetail = rdp->curtail; rdp->curlist = NULL; rdp->curtail = &rdp->curlist; } if (rdp->nxtlist && !rdp->curlist) { move_local_cpu_nxtlist_to_curlist(); rdp->batch = rcp->cur + 1; if (!rcp->next_pending) { rcp->next_pending = 1; rcp->cur++; cpus_andnot(rcp->cpumask, cpu_online_map, nohz_cpu_mask); } } if (rdp->donelist) rcu_do_batch(rdp); }
3.5)经过千山万水终于来到rcu_do_batch(如果rdp->donelist有的话)在此函数里,执行RCU写者挂载的回调。
point:
1) 仅当系统检测到一个grace period的所有CPU都经历了进程切换后,才会给系统一个信息要求启动新batch,在此期间的所有写者请求,都暂存在本地CPU的nxtlist链表里。