概述
RCU - Read-Copy-Update (读时加锁,写时拷贝,读后更新)
为读写锁的升级版
特点:
运行读者和写者并发执行;
最大程度减少读者侧的开销;
没有死锁问题
没有优先级反之问题
没有内存泄漏问题
很好的实时延迟
写者的同步开销比较大,写者之间需要互斥处理
基本思想
RCU锁的要素包括:
读标志
如果一个Reader企图占据一把RCU锁,它是不需要付出任何代价的,只需要设置一个标志,让外界知道有Reader在占据这把RCU锁,多个Reader可以共同持有一把RCU锁。
使用rcu_read_lock和rcu_read_unlock来界定读者的临界区,访问受RCU保护的数据时,需要始终在该临界区域内访问;
在访问受保护的数据之前,需要使用rcu_dereference来获取RCU-protected指针;
当使用不可抢占的RCU时,rcu_read_lock/rcu_read_unlock之间不能使用可以睡眠的代码;
写时拷贝
如果有一个Write企图更新RCU锁所保护的数据,那么它会首先查看该RCU锁的读标志,如果有该标志,说明有最少一个Reader持有了该RCU锁,它需要对原始数据make a copy,写这个副本并将更新过的副本保存在某处,等待时机用该副本更新原始数据。
多个Updater更新数据时,需要使用互斥机制进行保护;
Updater使用rcu_assign_pointer来移除旧的指针指向,指向更新后的临界资源;
Updater使用synchronize_rcu或call_rcu来启动Reclaimer,对旧的临界资源进行回收,其中synchronize_rcu表示同步等待回收,call_rcu表示异步回收;
更新时机
这个时机就是用副本更新原始数据的时间点,这个时间点如何确定是RCU锁实现的算法核心,它直接可以确定所有的数据结构。确切来讲,Writer必须waitting for all readers leaving,方可Update原始数据。
Reclaimer回收的是旧的临界资源;
为了确保没有读者正在访问要回收的临界资源,Reclaimer需要等待所有的读者退出临界区,这个等待的时间叫做宽限期(Grace Period);
2.1 Grace Period, 以及Reclaimer如何对旧的临界区进行资源回收?
中间的黄色部分代表的就是Grace Period,中文叫做宽限期,从Removal到Reclamation,中间就隔了一个宽限期;
只有当宽限期结束后,才会触发回收的工作,宽限期的结束代表着Reader都已经退出了临界区,因此回收工作也就是安全的操作了;
宽限期是否结束,与处理器的执行状态检测有关,也就是检测静止状态Quiescent Status;
RCU的性能与可扩展性依赖于它是否能有效的检测出静止状态(Quiescent Status),并且判断宽限期是否结束。
图中Readers和Updater并发执行;
当Updater执行Removal操作后,调用synchronize_rcu,标志着更新结束并开始进入回收阶段;
在synchronize_rcu调用后,此时可能还有新的读者来读取临界资源(更新后的内容),但是,Grace Period只等待Pre-Existing的读者,也就是在图中的Reader-4, Reader-5。只要这些之前就存在的RCU读者退出临界区后,意味着宽限期的结束,因此就进行回收处理工作了;
synchronize_rcu并不是在最后一个Pre-ExistingRCU读者离开临界区后立马就返回,它可能存在一个调度延迟;
2.2 Quiescent Status
Quiescent Status,用于描述处理器的执行状态。当某个CPU正在访问RCU保护的临界区时,认为是活动的状态,而当它离开了临界区后,则认为它是静止的状态。当所有的CPU都至少经历过一次QS后,宽限期将结束并触发回收工作。
在时钟tick中检测CPU处于用户模式或者idle模式,则表明CPU离开了临界区;
在不支持抢占的RCU实现中,检测到CPU有context切换,就能表明CPU离开了临界区;
从图中看:
从清除QS开始检测,当检查到每个cpu都经历了一个QS,即认为一个宽限期结束,可开启下一个宽限期。
linux实现
The RCU API, 2019 edition [LWN.net]
synchronize_rcu() may be used in place of synchronize_rcu_bh() and synchronize_sched(), as well as all current uses of synchronize_rcu_mult().
synchronize_rcu_expedited() may be used in place of synchronize_rcu_bh_expedited() and synchronize_sched_expedited().
call_rcu() may be used in place of call_rcu_bh() and call_rcu_sched().
rcu_barrier() may be used in place of rcu_barrier_bh() and rcu_barrier_sched().
get_state_synchronize_rcu() and cond_synchronize_rcu() may be used in place of get_state_synchronize_sched() and cond_synchronize_sched(), respectively.
3.1 RCU update Removal API
RCU写者,可以通过两种方式来等待宽限期的结束:
调用同步接口 synchronize_xxx 等待宽限期结束;
异步接口 call_rcu_xxx 注册资源释放回调函数。(等到宽限期结束后, rcu core会进行回调处理),分别如上图的左右两侧所示;
从图中的接口调用来看,同步接口中实际会去调用异步接口,只是同步接口中增加了一个wait_for_completion睡眠等待操作,并且会将wakeme_after_rcu回调函数传递给异步接口,当宽限期结束后,在异步接口中回调了wakeme_after_rcu进行唤醒处理;
目前内核中提供了三种RCU:
可抢占RCU:rcu_read_lock/rcu_read_unlock来界定区域,在读端临界区可以被其他进程抢占;
不可抢占RCU(RCU-sched):rcu_read_lock_sched/rcu_read_unlock_sched来界定区域,在读端临界区不允许其他进程抢占;
关下半部RCU(RCU-bh):rcu_read_lock_bh/rcu_read_unlock_bh来界定区域,在读端临界区禁止软中断;
3.1.1 __call_rcu 函数的调用流程如下,两个功能:
注册回调函数,而回调的函数的维护是在rcu_data结构中的struct rcu_segcblist cblist字段中;
判断是否需要开启新的宽限期GP;
3.1.2 那么通过__call_rcu注册的这些回调函数在哪里调用呢?答案是在RCU_SOFTIRQ软中断中:
当invoke_rcu_core时,在该函数中调用raise_softirq接口,从而触发软中断回调函数rcu_process_callbacks 的执行;
与宽限期GP相关的操作处理:
在rcu_process_callbacks中会调用rcu_gp_kthread_wake唤醒内核线程,最终会在rcu_gp_kthread线程中执行;
User注册的RCU资源释放回调函数执行的操作,都在rcu_do_batch函数中执行,其中有两种执行方式:
如果不支持优先级继承的话,直接调用即可;
支持优先级继承,在把回调的工作放置在rcu_cpu_kthread内核线程中,其中内核为每个CPU都创建了一个rcu_cpu_kthread内核线程;
3.2 宽限期开始与结束
内核分别为rcu_preempt_state, rcu_bh_state, rcu_sched_state创建了内核线程rcu_gp_kthread;
rcu_gp_kthread内核线程主要完成三个工作:
创建新的宽限期GP;
等待强制静止状态,设置超时,提前唤醒说明所有处理器经过了静止状态;
宽限期结束处理。其中,前边两个操作都是通过睡眠等待在某个条件上。
3.3 静止状态检测及报告
对这种状态的检测通常都是周期性的进行,放置在时钟中断处理中就是情理之中了。
rcu_sched/rcu_bh类型的RCU中,当检测CPU处于用户模式或处于idle线程中,说明当前CPU已经离开了临界区,经历了一个QS静止状态,对于rcu_bh的RCU,如果没有出去softirq上下文中,也表明CPU经历了QS静止状态;
在rcu_pending满足条件的情况下,触发软中断的执行,rcu_process_callbacks将会被调用;
在rcu_process_callbacks回调函数中,对宽限期进行判断,并对静止状态逐级上报,如果整个树状结构都经历了静止状态,那就表明了宽限期的结束,从而唤醒内核线程去处理;
顺便提一句,在rcu_pending函数中,rcu_pending->__rcu_pending->check_cpu_stall->print_cpu_stall的流程中,会去判断是否有CPU stall的问题,这个在内核中有文档专门来描述,不再分析了;