经过之前的分析基本清楚了整个RCU的结构体,下面就通过分代码来解读RCU的实现。
void synchronize_rcu(void) { if (!rcu_scheduler_active) return; wait_rcu_gp(call_rcu); }
当rcu_scheduler_active变量将会在第一个任务创建之前由0转换为1.当这个变量等于0的时候,RCU可以假设当前系统中仅仅存在一个任务。这样进行RCU等待就没有必要,所以通过这个变量对初始化时RCU队列进行优化,当这个变量转换为1时,表明系统已经准备好探测真正的grace period。顺便提一下,这里之所以从synchronize_rcu函数开始,是因为这个函数最后会调用call_rcu,而这个函数的指针则是作为参数传递到wait_rcu_gp中。
void wait_rcu_gp(call_rcu_func_t crf) { struct rcu_synchronize rcu; init_rcu_head_on_stack(&rcu.head); init_completion(&rcu.completion); crf(&rcu.head, wakeme_after_rcu); wait_for_completion(&rcu.completion); destroy_rcu_head_on_stack(&rcu.head); }
Wait_rcu_gp函数的实现如上图所示,首先从栈中分配一个结构体,这个阶梯就是中包含rcu的实现——一个队列指针,一个函数指针,已经一个同步等待的结构体(如果是异步rcu则没有同步等待结构体)。在初始化之后就转入到调用异步的call_rcu函数中,然后进行等待操作以及对堆栈上的RCU对象进行清理。先看一下wait_for_completion。
void wait_for_completion(struct completion *x) { wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE); }
从wait_for_completion函数的实现可以看出,不能在x上进行唤醒操作。则当前函数将会无限等待下去(MAX_SCHEDULE_TIMEOUT)而不会被信号打断(TASK_UNINTERRUPTIBLE)。
static void wakeme_after_rcu(struct rcu_head *head) { struct rcu_synchronize *rcu; rcu = container_of(head, struct rcu_synchronize, head); complete(&rcu->completion); }
再看看wakeme_after_rcu函数的实现,这个函数的实现仅仅唤醒当前正在等待的任务而不会释放内存,因为内存在堆栈中。
void call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *rcu)) { __call_rcu(head, func, &rcu_preempt_state); } static void __call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *rcu), struct rcu_state *rsp) { unsigned long flags; struct rcu_data *rdp; head->func = func; head->next = NULL; smp_mb(); local_irq_save(flags); rdp = this_cpu_ptr(rsp->rda); *rdp->nxttail[RCU_NEXT_TAIL] = head; rdp->nxttail[RCU_NEXT_TAIL] = &head->next; rdp->qlen++; if (irqs_disabled_flags(flags)) { local_irq_restore(flags); return; } if ((rdp->qlen > rdp->qlen_last_fqs_check + qhimark)) { rcu_process_gp_end(rsp, rdp); check_for_new_grace_period(rsp, rdp); if (!rcu_gp_in_progress(rsp)) { unsigned long nestflag; struct rcu_node *rnp_root = rcu_get_root(rsp); raw_spin_lock_irqsave(&rnp_root->lock, nestflag); rcu_start_gp(rsp, nestflag); /* rlses rnp_root->lock */ } else { rdp->blimit = LONG_MAX; if (rsp->n_force_qs == rdp->n_force_qs_snap && *rdp->nxttail[RCU_DONE_TAIL] != head) force_quiescent_state(rsp, 0); rdp->n_force_qs_snap = rsp->n_force_qs; rdp->qlen_last_fqs_check = rdp->qlen; } } else if (ULONG_CMP_LT(rsp->jiffies_force_qs, jiffies)) force_quiescent_state(rsp, 1); local_irq_restore(flags); }
上面是call_rcu的具体实现,不过这个实现只包括一部分。因为还有很大一部分涉及到系统调度相关。
函数的两个参数和函数里面定义的结构体rcu_data结构在之前的讲解中已经介绍了。其中rcu_state是一个全局结构体,而rcu_data则代表一个处理器相关的数据,在rcu_state和rcu_data之间还有一层数据结构rcu_node(稍后会介绍),这三个结构体组成RCU的树形结构。函数开头会将正在等待的参数给挂载到当前处理器的数据节点中,然后相应的队列计数会自加1,当这个计数超过一定的上限就会引起系统强制性开启grace period——毕竟RCU占用了很多额外的内存。
接下来是一个if判断进行流程控制,首先看下面的if部分。根据上面的分析,这里表明挂载到队列的回调函数过多需要进行一次强制性的grace period。函数首先更新状态,表明当前正在进行grace period处理之中。RCU层次处理包含两个层次递降过程:第一次是初始化的时候,利用rcu_start_gp将rsp当中的gpnum给递增上去,然后逐步更新rnp和rdp,同时rdp上的相关处理器位以及其他的资源也需要初始化;这些操作完成之后表明grace period完成,下一步就是逐层的往上争取锁并改变与处理器相应的状态。当所有的处理器都经过依次调度之后达到最上层的rsp则会引起rsp利用gpnum更新completed,并同时更新rnp上的completed,这是导致rnp和rdp上的completed不相等,那么表明grace period结束,需要向上提交回调函数并且更新rdp的completed。下面看一下rcu_process_gp_end的内部实现函数__rcu_process_gp_end。
static void __rcu_process_gp_end(struct rcu_state *rsp, struct rcu_node *rnp, struct rcu_data *rdp) { if (rdp->completed != rnp->completed) { rdp->nxttail[RCU_DONE_TAIL] = rdp->nxttail[RCU_WAIT_TAIL]; rdp->nxttail[RCU_WAIT_TAIL] = rdp->nxttail[RCU_NEXT_READY_TAIL]; rdp->nxttail[RCU_NEXT_READY_TAIL] = rdp->nxttail[RCU_NEXT_TAIL]; rdp->completed = rnp->completed; if (ULONG_CMP_LT(rdp->gpnum, rdp->completed)) rdp->gpnum = rdp->completed; if ((rnp->qsmask & rdp->grpmask) == 0) rdp->qs_pending = 0; } }
这个函数检测当前处理器结构体rdp的completed和当前处理器的上层节点结构体rnp的completed数据成员变量是否相等来检验grace period是否结束。如果结束了,则需要将回调函数队列给提交正在等待的队列进行提交。不过在介绍这个队列之前,首先看一下两个宏,前者是grace period结束的截止时间,如果在三个时钟中断之后grace period还没有结束则表明grace period超时;后者是回调函数队列的长度。再由之前的回调函数挂载到队列的操作可以知道,回调函数队列长度为4刚好等于三个等待的队列和一个需要被执行的队列。
#define RCU_JIFFIES_TILL_FORCE_QS 3 #define RCU_NEXT_SIZE 4
那么为什么是三个时钟中断呢?其实由之前的分析也很好理解,在rnp的gpnum被更新为rsp的gpnum之后最少需要三个时钟中断达到grace period(第一个时钟中断将rdp的gpnum更新为rnp的gpnum,第二个时钟中断相应的处理器往上层移动,第三个时钟中断更新rsp和rnp的completed为rsp的gpnum)。
Nxttail队列是一个间接的队列,他所指向的是nxtlist中的数据。下面又遇到一个宏,宏的定义如下:
#define ULONG_CMP_LT(a, b) (ULONG_MAX / 2 < (a) - (b)) #define ULONG_MAX (~0UL)
这里需要注意的是,当a<b的时候宏才有可能为真。~0UL等于将32位全部置为1,但是除以2之后得到最大的正数(相当于右移1位),而当a小于b的时候才有可能使得最高位等于1。这里表明当前rdp的gpnum小于completed,只有一种情况会出现这种现象,那就是当前处理器处于扩展QS,也就是当前处理器可能已经离线了,所以直接将gpnum更新为completed就可以了。
接下来是一个位操作,不论是rnp还是rdp都包含两个部分的位,第一个是qsmask,这个表明rdp管理的每一个处理器QS状态的掩码,如果rdp中的qsmask清零则表明相应的处理器穿过了QS,如果rdp中的所有qsmask都被清零则需要利用rdp中的grpmask将上层的rnp中的qsmask相应的位清零。对于rnp也是一样的操作。所以这里的判断是对相应的位进行测试,如果测试位等于0表明当前处理器上面没有正在等待的回调函数队列。这里有两种情况等于0,第一种是当前处理器已经离线,那么肯定测试等于0;第二种是当前处理器已经经过层层考核将相应的位清零所以也等于0.
static void __note_new_gpnum(struct rcu_state *rsp, struct rcu_node *rnp, struct rcu_data *rdp) { if (rdp->gpnum != rnp->gpnum) { rdp->gpnum = rnp->gpnum; if (rnp->qsmask & rdp->grpmask) { rdp->qs_pending = 1; rdp->passed_quiesce = 0; } else rdp->qs_pending = 0; } }
上面是check_for_new_grace_period函数的内部实现,这个函数就是对rdp中相应的状态进行初始化。如果当前的处理器并没有被清零,那么需要设置QS标志为0,这个标志会在以后层层更新之后设置为1.
static int rcu_gp_in_progress(struct rcu_state *rsp) { return (rsp->completed) != (rsp->gpnum); }
第三个函数,有上面的解释很好理解。上面提到的三个函数实际上是当前grace period的三种状态。
Rcu_start_gp函数开始一个grace period,当处理器存在回调函数等待grace period并且当前状态并没有处于grace period中时会新开启一个grace period。
static int cpu_needs_another_gp(struct rcu_state *rsp, struct rcu_data *rdp) { return *rdp->nxttail[RCU_DONE_TAIL + (rsp->completed) != rdp->completed] && !rcu_gp_in_progress(rsp); }
由于start_rcu_gp太长,所以进行分布分析。首先start_rcu_gp会进行一些验证保证当前适合开启一个新的grace period。当rsp的completed不等于rdp中的completed则表明已经开启了一个从QS等到grace period结束的过程,所以这样的话nxttail中的等待队列不能为空,否则就出现错误了——只有队列中存在数据才需要申请graceperiod;而如果两者相等则表明grace period结束,这样RCU_DONE_TAIL必须不等于0。
if (rsp->fqs_active) { rsp->fqs_need_gp = 1; raw_spin_unlock_irqrestore(&rnp->lock, flags); return; }
上面对状态位的测试表明当前正在处于强制性QS阶段,先设置一下标志位fqs_need_gp在强制性QS结束之后会补齐开启grace period的过程。
rsp->gpnum++; rsp->signaled = RCU_GP_INIT; rsp->jiffies_force_qs = jiffies + RCU_JIFFIES_TILL_FORCE_QS;
graceperiod初始化实际包含两部分,第一部分是对rsp的初始化,第二部分将这种初始化给深入到下面的rnp和rdp中,这样处理的原因也是不希望加锁打击面太大。回过头看上面的初始化实际上是在禁止当前处理器中断以及对顶层rnp中的lock加锁实现的,而有一些初始化需要对整个系统加锁,毕竟搞这么复杂的RCU实现有一个目的是为了处理器的热插拔,而在上面的处理中不需要这么大的系统锁。当然也因为这个原因,所以这里的标志是RCU_GP_INIT。
raw_spin_unlock(&rnp->lock); raw_spin_lock(&rsp->onofflock); rcu_for_each_node_breadth_first(rsp, rnp) { raw_spin_lock(&rnp->lock); rcu_preempt_check_blocked_tasks(rnp); rnp->qsmask = rnp->qsmaskinit; rnp->gpnum = rsp->gpnum; rnp->completed = rsp->completed; if (rnp == rdp->mynode) rcu_start_gp_per_cpu(rsp, rnp, rdp); rcu_preempt_boost_start_gp(rnp); raw_spin_unlock(&rnp->lock); }
上面对rnp解锁之后,不久就有一个对rnp的上锁过程。实际上这里下面的rnp和前面的rnp可能不是同一个,因为这里是对整个rnp队列进行遍历操作。所以需要一个更大的专用的锁进行全局的保护——就是rsp中的onoff锁。在互斥条件下,所有的rnp都不能进行热插拔处理。同时将rnp状态进行初始化,qsmaskinit是一个动态变化的处理器掩码,这个掩码系列随着处理器的数目而可能发生变化。在这个处理中,如果当前的rdp的rnp等于循环的rnp则需要设置rdp。到这里基本上所有的RCU初始化都已经初始化了。其他处理器的rdp上的设置在前面的__note_new_gpnum函数里面实现。最后会将当前状态设置为RCU_SIGNAL_INIT。
static void rcu_start_gp(struct rcu_state *rsp, unsigned long flags) { struct rcu_data *rdp = this_cpu_ptr(rsp->rda); struct rcu_node *rnp = rcu_get_root(rsp); if (!rcu_scheduler_fully_active || !cpu_needs_another_gp(rsp, rdp)) { raw_spin_unlock_irqrestore(&rnp->lock, flags); return; } if (rsp->fqs_active) { rsp->fqs_need_gp = 1; raw_spin_unlock_irqrestore(&rnp->lock, flags); return; } rsp->gpnum++; rsp->signaled = RCU_GP_INIT; rsp->jiffies_force_qs = jiffies + RCU_JIFFIES_TILL_FORCE_QS; record_gp_stall_check_time(rsp); if (NUM_RCU_NODES == 1) { rcu_preempt_check_blocked_tasks(rnp); rnp->qsmask = rnp->qsmaskinit; rnp->gpnum = rsp->gpnum; rnp->completed = rsp->completed; rsp->signaled = RCU_SIGNAL_INIT; rcu_start_gp_per_cpu(rsp, rnp, rdp); rcu_preempt_boost_start_gp(rnp); raw_spin_unlock_irqrestore(&rnp->lock, flags); return; } raw_spin_unlock(&rnp->lock); raw_spin_lock(&rsp->onofflock); rcu_for_each_node_breadth_first(rsp, rnp) { raw_spin_lock(&rnp->lock); /* irqs already disabled. */ rcu_preempt_check_blocked_tasks(rnp); rnp->qsmask = rnp->qsmaskinit; rnp->gpnum = rsp->gpnum; rnp->completed = rsp->completed; if (rnp == rdp->mynode) rcu_start_gp_per_cpu(rsp, rnp, rdp); rcu_preempt_boost_start_gp(rnp); raw_spin_unlock(&rnp->lock); } rnp = rcu_get_root(rsp); raw_spin_lock(&rnp->lock); rsp->signaled = RCU_SIGNAL_INIT; raw_spin_unlock(&rnp->lock); raw_spin_unlock_irqrestore(&rsp->onofflock, flags); }
接下来是force_quiescent_state函数,这个函数可能存在两次调用机会。第一次和第二次的第二个参数不一样,前者为0后者为1.前者表明当前还不非常紧急可以暂缓强制QS,而后者则表明当前已经必须进行QS处理了。Force_quiescent_state函数主体是一个switch分支结构,分支结构起始位置是一个状态位的设置是不是很熟悉,下面就根据rsp当前的状态进行全局的分支调用,主要状态是RCU_SAVE_DYNTICK和RCU_FORCE_QS。
rsp->fqs_active = 1; switch (rsp->signaled) { case RCU_GP_IDLE: case RCU_GP_INIT: break; case RCU_SAVE_DYNTICK: if (RCU_SIGNAL_INIT != RCU_SAVE_DYNTICK) break; raw_spin_unlock(&rnp->lock); /* irqs remain disabled */ force_qs_rnp(rsp, dyntick_save_progress_counter); raw_spin_lock(&rnp->lock); /* irqs already disabled */ if (rcu_gp_in_progress(rsp)) rsp->signaled = RCU_FORCE_QS; break; case RCU_FORCE_QS: raw_spin_unlock(&rnp->lock); /* irqs remain disabled */ force_qs_rnp(rsp, rcu_implicit_dynticks_qs); raw_spin_lock(&rnp->lock); /* irqs already disabled */ break; } rsp->fqs_active = 0; if (rsp->fqs_need_gp) { raw_spin_unlock(&rsp->fqslock); /* irqs remain disabled */ rsp->fqs_need_gp = 0; rcu_start_gp(rsp, flags); /* releases rnp->lock */ return; }
上面同样调用force_qs_rnp两次,不过同样的第二个参数不相同。前者的主要目的是记录下当前的处理器dynticks状态,以避免正在休眠的处理器被唤醒,而后者则将进行真正的强制性QS操作。从上面的代码流程看下来仿佛所有的QS都需要经过强制性QS处理才能穿过QS,实际上不然,因为还有一部分会在处理器时钟中断中处理。处理流程和上面的流程很相似,不过调用是从时钟中断处理中发出来的。真正的强制性QS只在两种可能下处理,第一种是grace period结束,第二种grace period可能结束——也就是超时了。所以grace period只需要设置rnp的处理就可以了而不需要考虑rdp,而rdp的处理则是在时钟中断中进行调用——时钟中断引起调度,顺便处理下rdp表明当前处理器已经进行了一次调度是完全可行的。
static void force_qs_rnp(struct rcu_state *rsp, int (*f)(struct rcu_data *)) { unsigned long bit; int cpu; unsigned long flags; unsigned long mask; struct rcu_node *rnp; rcu_for_each_leaf_node(rsp, rnp) { mask = 0; raw_spin_lock_irqsave(&rnp->lock, flags); if (!rcu_gp_in_progress(rsp)) { raw_spin_unlock_irqrestore(&rnp->lock, flags); return; } if (rnp->qsmask == 0) { rcu_initiate_boost(rnp, flags); /* releases rnp->lock */ continue; } cpu = rnp->grplo; bit = 1; for (; cpu <= rnp->grphi; cpu++, bit <<= 1) { if ((rnp->qsmask & bit) != 0 && f(per_cpu_ptr(rsp->rda, cpu))) mask |= bit; } if (mask != 0) { rcu_report_qs_rnp(mask, rsp, rnp, flags); continue; } raw_spin_unlock_irqrestore(&rnp->lock, flags); } rnp = rcu_get_root(rsp); if (rnp->qsmask == 0) { raw_spin_lock_irqsave(&rnp->lock, flags); rcu_initiate_boost(rnp, flags); /* releases rnp->lock. */ } }
上面这个函数看起来一大堆,实际关键的处理就是一个对rnp自叶节点网上层进行清空处理器掩码的过程——因为下层的节点处理可能逐层上传将上层节点的grpmask掩码进行清零处理。如果掩码成功处理了,则会引起将当前的graceperiod设置为完成——也就是更新rnp的completed和rsp的completed。
static void __rcu_process_callbacks(struct rcu_state *rsp, struct rcu_data *rdp) { unsigned long flags; if (ULONG_CMP_LT(rsp->jiffies_force_qs, jiffies)) force_quiescent_state(rsp, 1); rcu_process_gp_end(rsp, rdp); rcu_check_quiescent_state(rsp, rdp); if (cpu_needs_another_gp(rsp, rdp)) { raw_spin_lock_irqsave(&rcu_get_root(rsp)->lock, flags); rcu_start_gp(rsp, flags); /* releases above lock */ } if (cpu_has_callbacks_ready_to_invoke(rdp)) invoke_rcu_callbacks(rsp, rdp); }
处理器调度的时会调用rcu_process_callbacks函数,这个函数主要处理rdp中的处理器掩码进行清空。具体过程不再进一步展开,直接找到关键的部分。
static void rcu_report_qs_rdp(int cpu, struct rcu_state *rsp, struct rcu_data *rdp, long lastgp) { unsigned long flags; unsigned long mask; struct rcu_node *rnp; rnp = rdp->mynode; raw_spin_lock_irqsave(&rnp->lock, flags); if (lastgp != rnp->gpnum || rnp->completed == rnp->gpnum) { rdp->passed_quiesce = 0; raw_spin_unlock_irqrestore(&rnp->lock, flags); return; } mask = rdp->grpmask; if ((rnp->qsmask & mask) == 0) { raw_spin_unlock_irqrestore(&rnp->lock, flags); } else { rdp->qs_pending = 0; rdp->nxttail[RCU_NEXT_READY_TAIL] = rdp->nxttail[RCU_NEXT_TAIL]; rcu_report_qs_rnp(mask, rsp, rnp, flags); /* rlses rnp->lock */ } }
函数实现,实际有两个部分,前者当rnp和rdp的completed相等则表明是新的grace period,所以需要设置passed_quiesce为0表明还没有穿过QS。否则下面需要根据当前rdp的grpmask设置rnp的位。从这里可以看出,在调度支出肯定有一个判断当rdp的qsmask等于0时才会调用这个函数,否则只是设置与当前处理器相关的函数。
下面还有一个关键的函数调用会回调函数,函数的实现分两种情况表明RCU支持两种情况,一种RCU只作为数据来进行处理,第二种是RCU作为函数来调用。前者实际上是异步处理过程,不管什么样的东西直接当做数据进行释放,而后者则是作为同步处理,所做的操作主要是唤醒等待队列。
static inline void __rcu_reclaim(char *rn, struct rcu_head *head) { unsigned long offset = (unsigned long)head->func; if (__is_kfree_rcu_offset(offset)) { kfree((void *)head - offset); } else { head->func(head); } }