<深入浅出> linux内核 RCU (一)经典RCU

以内核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; 
};


这里的cpumask就担当上文里每cpu变量的角色。而cur,completed,则用来同步。我们可以这样理解,cur和completed是系统本身特性,也即系统实时经历的grace编号,一般情况下,新开一个graceperiod等待周期的话,cur会加1,当graceperiod结束后,会将completed置为cur,所以通常情况下,都是completed追着cur跑。那么我们可能会猜测,是不是如果complete= curr -1 的时候,就表示系统中graceperiod还没有结束?当completed= curr的时候,就表示系统中不存在graceperiod等待周期了?我们姑且这么理解,实际上有些许差别,但设计思想都是一样的。 

上述结构是系统相关,那么下面的结构,就是与具体的写者相关了:

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链表里。


你可能感兴趣的:(数据结构,linux,优化,struct)