RCU全称Read Copy Update,这是一种对读写锁的优化。当所有对相关数据结构的操作时读者行为时,则通过最高层次的禁止线程调度就可以了,如果需要对这个队列进行写操作,那么可以先将原来的数值复制一份出来,然后对复制出来的数据进行处理,最后在适当的时机进行更新。而更新的适当时机则是所有的处理器上都进行了一次线程切换之后,因为只有这样才能保证整个所有的处理器上的读者都释放了相应的资源。
struct foo { int a; char b; long c; }; DEFINE_SPINLOCK(foo_mutex); struct foo *gbl_foo; void foo_update_a(int new_a) { struct foo *new_fp; struct foo *old_fp; new_fp = kmalloc(sizeof(*new_fp), GFP_KERNEL); spin_lock(&foo_mutex); old_fp = gbl_foo; *new_fp = *old_fp; new_fp->a = new_a; rcu_assign_pointer(gbl_foo, new_fp); spin_unlock(&foo_mutex); synchronize_rcu(); kfree(old_fp); } int foo_get_a(void) { int retval; rcu_read_lock(); retval = rcu_dereference(gbl_foo)->a; rcu_read_unlock(); return retval; }
上面是RCU适用的一个示例,其中foo_update_a用于更新RCU内容,foo_mutex 用于更新保护;foo_get_a用于获取一个RCU中的数据指针,RCU_READ_LOCK实现RCU的互斥访问。另外一个关键函数是synchronize_rcu(),这个函数用于保证更新是同步的——直到所有的读者都已近退出才会返回。再返回之后调用kfree释放原来的内存。rcu_dereference和rcu_assign_pointer分别用于提取RCU中的内容以及给RCU赋值,不过提取和赋值对象都是指针。
#define rcu_assign_pointer(p, v) \ __rcu_assign_pointer((p), (v), __rcu) #define __rcu_assign_pointer(p, v, space) \ ({ \ smp_wmb(); \ (p) = (typeof(*v) space *)(v); \ })
对RCU的提取主体是上面宏的展开,可以看到开销基本为0。最主要是没有因为加锁而导致处理是串行处理。另外从这里也可以看出来,之所以需要以指针方式访问,是因为需要内存在读者退出来之前利用rcu_dereference获取的指针来访问数据。而对这部分内存的释放是在一段graceperiod之后。这样就可以保证读者不受锁的限制,而所有的锁延迟开销都在写者锁部分。
static inline void rcu_read_lock(void) { __rcu_read_lock(); __acquire(RCU); rcu_read_acquire(); }
上面是读者获取锁的函数实现,从函数的属性来看这个函数只能够在本文件中使用,所以是RCU队列的内部函数。整个函数实现有三行,其中只有第一行是真正的实现,而第二行类似于前面所提到的user标志。
void __rcu_read_lock(void) { current->rcu_read_lock_nesting++; barrier(); }
可以看到读操作仅仅是对当前线程中的标志进行计数防止当前进程在没有退出队列的时候被打断。从这一行代码也可以想到解锁操作就是将计数递减下来。
对RCU队列的更新操作利用两个函数实现,一个是call_rcu,另一个是synchronize_rcu。Call_rcu是更新的异步版本,synchronize_rcu是更新的同步版本。经典RCU中包含一个重要的数据结构rcu_ctrlblk,这个结构体包含一个cpumask的成员变量用于记录每个处理器的位图。处理器位图在grace period开始的时候会被设置为1,每次处理器调度之后就会将位图中相应的位清零。为了保证对这个位图的访问是互斥进行的,就需要额外的一个互斥锁来对这个位图进行保护。
void synchronize_rcu(void) { int cpu; for_each_possible_cpu(cpu) run_on(cpu); }
上面是synchronize_rcu函数的伪代码,总体就是一个循环,在这个循环里面设置处理器的亲和性使得当前代码可以在所有的处理器上运行一遍,并清除rcu_ctrlblk中的位图中的所有的位。如果这段代码运行结束,那么表明所有的当前线程已经调度了,则原来的数据可以删除了,就可以调用内存回收函数将内存回收掉。
void foo_update_a(int new_a) { struct foo *new_fp; struct foo *old_fp; new_fp = kmalloc(sizeof(*new_fp), GFP_KERNEL); spin_lock(&foo_mutex); old_fp = gbl_foo; *new_fp = *old_fp; new_fp->a = new_a; rcu_assign_pointer(gbl_foo, new_fp); spin_unlock(&foo_mutex); call_rcu(&old_fp->rcu, foo_reclaim); } void foo_reclaim(struct rcu_head *rp) { struct foo *fp = container_of(rp, struct foo, rcu); kfree(fp); }
call_rcu函数有两个参数,一个是RCU指针,另一个是一个函数指针。第二个参数会在适当的时候被调用用于释放相应的内存(由于Linux内部的实现和论文所提到的思想上做了一些变动,所以暂时不写call_rcu的具体实现)。从异步调用的经验来看,call_rcu的实现可能将两个参数挂载到一个全局的队列,然后进行一些检查后返回。每次处理器调度的时候都会设置相关的标记,当所有的处理器上都发生调度时则会从队列中取下函数以及函数参数并进行调用。
从上面的叙述可以看出,经典的RCU队列不能用在可抢断内核中,同时在进行更新的时候需要一个全局的锁不适用于多核系统上,每次退出grace period都需要调用一次,又可能因为一些频繁的grace period而打断正在休眠的处理器。所以真正的实现都对经典RCU进行了一些优化以便于用在具体的内核上,不过核心思想还是读取和更新分离。