[toc]
kernel/rcu/srcu.c Sleepable Read-Copy Update (SRCU) 允许睡眠的RCU变体
历史与背景
这项技术是为了解决什么特定问题而诞生的?
Sleepable Read-Copy Update (SRCU) 的诞生是为了解决标准RCU(包括Classic RCU和Preemptible RCU)的一个核心限制:其读侧临界区(read-side critical section)内不允许睡眠。
标准的RCU通过监控CPU的静止状态(如上下文切换、进入idle)来确定宽限期(Grace Period),而这些监控手段都假设了读者在持有“锁”(即在rcu_read_lock()
和rcu_read_unlock()
之间)时不会主动放弃CPU(即睡眠)。
然而,在内核中存在很多场景,开发者需要在访问一个被RCU保护的、读多写少的数据结构的同时,执行一些可能会导致睡眠的操作。例如:
- 与用户空间交互:调用
copy_from_user()
或copy_to_user()
,这些函数在访问的用户内存被换出到磁盘时,会发生页错误并睡眠。 - 获取互斥锁:调用
mutex_lock()
,如果锁已被其他任务持有,当前任务会睡眠等待。 - 内存分配:使用
kmalloc()
并传入GFP_KERNEL
标志,在内存不足时可能会睡眠等待。
在这些场景下,标准的RCU无法使用。SRCU就是为了将RCU的“读者几乎零开销”这一核心优势,扩展到这些读侧临界区必须允许睡眠的场景中而设计的。
它的发展经历了哪些重要的里程碑或版本迭代?
SRCU是RCU家族的一个重要分支,其发展本身就是一个里程碑。
- 概念的提出与实现:由Paul E. McKenney等人设计并实现,作为对RCU适用范围的一次重大扩展。其核心创新在于放弃了依赖调度器状态来检测宽限期,转而采用一种更显式的、基于计数器的追踪机制。
- API的独立与成熟:SRCU拥有一套完全独立的API(
srcu_read_lock()
,synchronize_srcu()
等)和一个独立的struct srcu_struct
来定义作用域。这使得它可以与系统中的其他RCU变体共存而互不干扰。 - 性能与扩展性优化:与Tree RCU类似,SRCU的内部实现也在不断优化,以更好地适应大规模多核系统,尽管其固有的开销使其性能上限低于非睡眠的RCU变体。
目前该技术的社区活跃度和主流应用情况如何?
SRCU是内核中一个稳定且关键的同步原语。虽然它的使用范围不如通用的Preemptible RCU广泛,但在其适用的特定领域是不可或缺的。
- 主流应用:
- 追踪子系统 (Tracepoints, ftrace):追踪回调函数在执行时可能会进行复杂的操作,甚至需要获取锁,因此它们通常在SRCU的读侧临界区内运行。
- 文件系统:某些文件系统的内部数据结构,其访问路径上可能涉及需要睡眠的I/O操作或锁。
- 网络:一些需要与用户空间进行复杂交互的网络管理代码。
核心原理与设计
它的核心工作原理是什么?
SRCU的核心原理与RCU相同:读-拷贝-更新,并通过等待宽限期来延迟内存回收。但它实现宽限期检测的方式完全不同。
SRCU不依赖CPU的调度状态,而是为每一个被保护的数据结构实例维护一个struct srcu_struct
。这个结构体内部有一个小数组(通常是两个元素)的计数器和一个当前“活动”计数器的索引。
读者 (Readers):
srcu_read_lock(ss)
:读者调用此函数时,会原子地增加当前活动计数器的值。ss
是指向struct srcu_struct
的指针。这个操作是一个真正的原子操作,比rcu_read_lock()
的开销要大。srcu_read_unlock(ss)
:读者完成读取后,调用此函数原子地减少同一个计数器的值。- 由于只是增减计数器,读者在临界区内完全可以睡眠,不会影响机制的正确性。
更新者 (Updaters):
- 更新者执行标准的“读-拷贝-更新”流程。
- 当需要等待宽限期时,它调用
synchronize_srcu(ss)
。
synchronize_srcu
的工作流程 (SRCU宽限期的精髓):- 切换索引 (Flip):
synchronize_srcu
首先切换srcu_struct
中的活动计数器索引(例如,从0切换到1)。从此以后,所有新的srcu_read_lock()
调用都会去操作索引为1的计数器。 - 等待旧读者:然后,
synchronize_srcu
会等待索引为0的那个旧的计数器值变为零。它会以一种可睡眠的方式反复检查这个计数器。 - 宽限期结束:一旦旧计数器的值变为零,就意味着所有在切换索引之前进入读侧临界区的读者,现在都已经调用了
srcu_read_unlock()
并退出了。至此,一个宽限期就结束了。此时释放旧数据是安全的。
- 切换索引 (Flip):
它的主要优势体现在哪些方面?
- 允许读者睡眠:这是其最核心、最不可替代的优势。
- 读写并发:继承了RCU的优点,读者不会阻塞写者,反之亦然。
- 作用域独立:每个
srcu_struct
管理自己的宽限期,不同SRCU实例之间互不影响。
它存在哪些已知的劣势、局-限性或在特定场景下的不适用性?
- 读者开销更高:与几乎零开销的
rcu_read_lock()
相比,srcu_read_lock()
和srcu_read_unlock()
需要执行原子增减操作,并访问共享的srcu_struct
内存,这带来了更高的性能开销和缓存争用。 - 宽限期可能更长:SRCU的宽限期依赖于所有旧读者完成其(可能很长的)睡眠操作,因此通常比非睡眠RCU的宽限期要长得多。
- 需要显式定义结构:开发者必须为每个需要SRCU保护的场景定义并初始化一个
struct srcu_struct
实例。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
当一个读多写少的数据结构,其读者访问路径上不可避免地存在可能导致睡眠的操作时,SRCU是首选乃至唯一的解决方案。
- 追踪回调:内核的tracepoint允许其他子系统挂接回调函数。这些回调函数的行为是未知的,可能需要睡眠。为了保护被追踪代码的上下文,整个回调的执行都由SRCU来保护。
- 需要与用户空间交互的内核接口:例如一个
ioctl
的实现,它需要读取一个被RCU保护的全局配置,同时又要用copy_from_user
从用户空间拷贝参数。 - 需要获取
mutex
的读者:当读者在访问共享数据后,需要获取一个mutex
来操作其他资源时。
是否有不推荐使用该技术的场景?为什么?
- 读者不睡眠的场景:如果可以保证读侧临界区内绝不会发生睡眠,绝对应该使用标准的Preemptible RCU。因为SRCU的读者开销要大得多,在这种场景下使用SRCU是一种不必要的性能浪费。
- 写密集型负载:与所有RCU变体一样,它不适合写操作频繁的场景。
对比分析
请将其 与 其他相似技术 进行详细对比。
特性 | SRCU (Sleepable RCU) | Preemptible RCU (标准RCU) | 互斥锁 (mutex ) |
---|---|---|---|
读者是否可睡眠 | 是 | 否 | 是 |
读者开销 | 中等 (原子操作 + 缓存行争用)。 | 极低 (通常只是禁用/启用抢占)。 | 高 (可能涉及睡眠和上下文切换)。 |
读-写并发 | 允许。读者和写者互不阻塞。 | 允许。读者和写者互不阻塞。 | 不允许。完全互斥。 |
宽限期机制 | 基于每个实例的显式计数器。 | 基于全局的调度器状态 (静止状态)。 | N/A (使用锁,没有宽限期概念)。 |
性能 | 读性能优于mutex ,但远逊于标准RCU。 |
读性能极高,是所有方案中最好的。 | 在高争用下性能和扩展性差。 |
适用场景 | 读多写少,且读者必须睡眠的场景。 | 读多写少,且读者不睡眠的通用场景。 | 保护需要睡眠的、任意读写比例的临界区。 |
kernel/rcu/srcutree.c
srcu_init 初始化 SRCU 结构
1 | void __init srcu_init(void) |