您的位置:首页 > 运维架构 > Linux

Linux RCU队列(3)树形RCU队列实现代码分析

2014-12-02 00:13 295 查看
经过之前的分析基本清楚了整个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);
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  linux