为了保护共享数据,需要一些同步机制,如自旋锁(spinlock),读写锁(rwlock),它们使用起来非常简单,而且是一种很有效的同步机制,在UNIX系统和Linux系统中得到了广泛的使用。但是随着计算机硬件的快速发展,获得这种锁的开销相对于CPU的速度在成倍地增加,原因很简单,CPU的速度与访问内存的速度差距越来越大,而这种锁使用了原子操作指令,它需要原子地访问内存,也就说获得锁的开销与访存速度相关,另外在大部分非x86架构上获取锁使用了内存栅(Memory Barrier),这会导致处理器流水线停滞或刷新,因此它的开销相对于CPU速度而言就越来越大。
在这种背景下,一个高性能的锁机制RCU呼之欲出,它克服了以上锁的缺点,具有很好的扩展性,但是这种锁机制的使用范围比较窄,它只适用于读多写少的情况,如网络路由表的查询更新、设备状态表的维护、数据结构的延迟释放以及多径I/O设备的维护等。
RCU的原理
Read Copy Update
读(Read):读者不需要获得任何锁就可访问RCU保护的临界区;
拷贝(Copy):写者在访问临界区时,写者“自己”将先拷贝一个临界区副本,然后对副本进行修改;
更新(Update):RCU机制将在在适当时机使用一个回调函数把指向原来临界区的指针重新指向新的被修改的临界区,锁机制中的垃圾收集器负责回调函数的调用。(时机:所有引用该共享临界区的CPU都退出对临界区的操作。即没有CPU再去操作这段被RCU保护的临界区后,这段临界区即可回收了,此时回调函数即被调用)
quiescent state(静默状态过程),它表示为CPU发生上下文切换的过程
grace period(即“适当时机”),它表示为所有CPU都经历一次quiescent state所需要的等待的时间,也即系统中所有的读者完成对共享临界区的访问
RCU的结构体定义,只有一个用于串接链表的next指针和一个函数指针,这个函数指针即是上述提及的回调函数,这个需使用RCU机制的用户向链表注册,即挂接到链表下,从而在适当时机下得到调用
示例:写者从链表中删除元素B。
写者首先遍历该链表得到指向元素B的指针,然后修改元素B的前一个元素的next指针指向元素B的next指针指向的元素C,修改元素B的next指针指向的元素C的prep指针指向元素B的prep指针指向的元素A。在此期间可能有读者访问该链表,由于修改指针指向的操作是原子的,因此这个过程不需要同步,而元素B的指针并没有去修改,因为读者可能正在使用B元素来得到链表的下一个或前一个元素,即A或C。当写者完成上述操作后便向系统注册一个回调函数func以便在 grace period之后能够删除元素B,注册完毕后写着便可认为它已经完成删除操作(实际上并未完成)。
垃圾收集器在检测到所有的CPU不在引用该链表后,即所有的CPU已经经历了一次quiescent state(即grace period),当grace period完成后,系统便会去调用先前写者注册的回调函数func,从而真正的删除了元素B。这便是RCU机制的一种使用范例。
核心实现
图中Readers和Updater并发执行;
当Updater执行Removal操作后,调用synchronize_rcu,标志着更新结束并开始进入回收阶段;
在synchronize_rcu调用后,此时可能还有新的读者来读取临界资源(更新后的内容),但是,Grace Period只等待Pre-Existing的读者,也就是在图中的Reader-4, Reader-5。只要这些之前就存在的RCU读者退出临界区后,意味着宽限期的结束,因此就进行回收处理工作了;
synchronize_rcu并不是在最后一个Pre-ExistingRCU读者离开临界区后立马就返回,它可能存在一个调度延迟;
适用场景
1.适合用于同步基于指针实现的数据结构(例如链表,哈希表等)。因为指针赋值是一条单指令.也就是说是一个原子操作. 因它更改指针指向没必要考虑它的同步.只需要考虑cache的影响
2.适用用读操作远远大与写操作的场景。RCU实际上是一种改进的rwlock,读者几乎没有什么同步开销,它不需要锁,不使用原子指令,而且在除alpha的所有架构上也不需要内存栅(Memory Barrier),因此不会导致锁竞争,内存延迟以及流水线停滞。不需要锁也使得使用更容易,因为死锁问题就不需要考虑了。写者的同步开销比较大,它需要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其它写者的修改操作。