目录
2.1.1 rcu_read_lock() 和 rcu_read_unlock()
1.源码概况:
在Linux内核的源代码中,RCU的实现主要集中在include/linux/rcupdate.h
和kernel/rcupdate.c
文件中。下面是一个简化的RCU机制分析:
1.1 RCU数据结构
struct rcu_head
这是RCU机制中最核心的数据结构,用于标记需要被RCU处理的对象。它包含一个call_entry
字段,指向一个回调函数列表,当所有读取者结束时,这个列表中的函数将被执行。
struct rcu_head {
union {
struct callback_head callback;
struct rcu_callback_list list;
};
};
2. 核心函数
2.1 读端(Reader)
读端主要涉及以下几个关键函数:
2.1.1 rcu_read_lock() 和 rcu_read_unlock()
这两个函数用于标记RCU读临界区的开始和结束。在大多数架构上,它们是空操作,几乎没有开销。rcu_read_lock()
读取线程在读取RCU保护的数据前,调用此函数。它并不真正锁定任何资源,而是标记线程为RCU读取者。rcu_read_unlock()
读取线程在完成读取操作后,调用此函数。这使得更新线程能够检测到读取操作的完成。
#define rcu_read_lock() __rcu_read_lock()
#define rcu_read_unlock() __rcu_read_unlock()
static inline void __rcu_read_lock(void)
{
preempt_disable();
}
static inline void __rcu_read_unlock(void)
{
preempt_enable();
}
这里的 preempt_disable()
和 preempt_enable()
用于禁止和启用内核抢占。
2.1.2 rcu_dereference()
用于安全地读取RCU保护的指针。
#define rcu_dereference(p) \
__rcu_dereference_check((p), 0, __rcu)
#define __rcu_dereference_check(p, c, space) \
({ \
typeof(*p) *_________p1 = (typeof(*p) *__force)READ_ONCE(p); \
RCU_LOCKDEP_WARN(!(c), "suspicious rcu_dereference_check() usage"); \
rcu_dereference_raw(p); \
})
READ_ONCE()
确保只读取一次内存,防止编译器优化导致的重复读取。
2.2 写端(Updater)
写端主要涉及以下几个关键函数:
2.2.1 rcu_assign_pointer()
用于安全地更新RCU保护的指针。当更新线程需要替换某个由RCU保护的指针时,使用此函数。它并不立即更新指针,而是设置新的值并安排后续的处理。
#define rcu_assign_pointer(p, v) \
__rcu_assign_pointer((p), (v), __rcu)
#define __rcu_assign_pointer(p, v, space) \
do { \
smp_wmb(); \
(p) = (typeof(*v) __force space *)v; \
} while (0)
smp_wmb()
是一个内存屏障,确保在更新指针之前,所有之前的写操作都已完成。
2.2.2 synchronize_rcu()
等待所有现有的RCU读临界区完成。更新线程在修改RCU保护的数据前,必须调用此函数。这个函数会检查是否处于RCU读临界区,然后根据情况选择不同的等待策略,确保新值可见于所有后续的读取者。
void synchronize_rcu(void)
{
RCU_LOCKDEP_WARN(lock_is_held(&rcu_bh_lock_map) ||
lock_is_held(&rcu_lock_map) ||
lock_is_held(&rcu_sched_lock_map),
"Illegal synchronize_rcu() in RCU read-side critical section");
if (rcu_blocking_is_gp())
return;
if (rcu_gp_is_expedited())
synchronize_rcu_expedited();
else
wait_rcu_gp(call_rcu);
}
2.3 RCU的核心实现
2.3.1 Grace Period 的检测
RCU通过检测所有CPU是否都经过了一个quiescent state来判断一个grace period是否结束。
static void rcu_process_callbacks(struct rcu_state *rsp)
{
unsigned long flags;
bool needwake;
rcu_do_batch(rsp);
needwake = rcu_prepare_for_idle(rsp);
if (needwake)
invoke_rcu_callbacks(rsp);
}
rcu_do_batch()
函数处理回调队列,rcu_prepare_for_idle()
检查是否所有CPU都进入了quiescent state。
3.2.2 回调处理
RCU使用回调函数来处理旧数据的释放。
-
call_rcu
- 作用:注册一个回调函数,该函数将在未来的某个时间点(下一个 grace period 结束后)被调用。
- 调用时机:通常在更新 RCU 保护的数据结构后立即调用。
- 执行位置:在当前进程的上下文中执行。
-
invoke_rcu_callbacks
- 作用:执行之前通过
call_rcu
注册的回调函数。 - 调用时机:在 grace period 结束后,由 RCU 核心机制触发。
- 执行位置:通常在软中断上下文或专门的 RCU 线程中执行。
- 作用:执行之前通过
3.2.2.1 工作流程
- 注册回调:
- 当一个 RCU 保护的数据结构被更新时,更新者调用
call_rcu
。 call_rcu
将回调函数和相关数据添加到当前 CPU 的 RCU 回调队列中。
- 当一个 RCU 保护的数据结构被更新时,更新者调用
- 等待 grace period:
- RCU 核心机制负责检测 grace period 的结束。
- 在这期间,读者可以安全地访问旧版本的数据。
- 执行回调:
- 当 grace period 结束时,RCU 核心机制调用
invoke_rcu_callbacks
。 invoke_rcu_callbacks
遍历回调队列,执行每个注册的回调函数。
- 当 grace period 结束时,RCU 核心机制调用
Grace period在 RCU (Read-Copy-Update) 机制中是一个关键概念。它指的是一段时间间隔,在这段时间结束后,可以保证所有正在进行的 RCU 读操作都已经完成。
RCU回调函数通常在一个特殊的上下文(如NMI或软中断)中执行,以避免干扰正常的内核执行流。call_rcu()
函数负责调度这些回调。call_rcu 的简化实现:
void call_rcu(struct rcu_head *head, rcu_callback_t func)
{
unsigned long flags;
struct rcu_data *rdp;
head->func = func;
local_irq_save(flags);
rdp = this_cpu_ptr(&rcu_data);
__call_rcu(head, func, rdp, false, 0);
local_irq_restore(flags);
}
这个函数主要做了以下几件事:
- 将回调函数
func
保存到rcu_head
结构中。 - 禁用本地中断以确保操作的原子性。
- 获取当前 CPU 的 RCU 数据结构。
- 调用
__call_rcu
函数来实际注册回调。 - 恢复本地中断。
__call_rcu
函数是实际执行回调注册的地方:
static void
__call_rcu(struct rcu_head *head, rcu_callback_t func,
struct rcu_data *rdp, bool lazy, unsigned long flags)
{
unsigned long js;
struct rcu_node *rnp;
// 检查是否设置了debug标志和是否处于禁止回调的状态
RCU_LOCKDEP_WARN(rcu_scheduler_active == RCU_SCHEDULER_INACTIVE,
"call_rcu() before rcu_init_sched()");
if (debug_rcu_head_queue(head)) {
// 处理调试相关的操作
return;
}
// 设置回调函数
head->func = func;
head->next = NULL;
// 获取当前的 grace period 计数
js = rdp->jiffies_snap;
// 选择适当的回调队列并将回调添加到队列中
if (lazy)
// 对于懒惰RCU,使用不同的队列
rcu_segcblist_add_cblist_tail(&rdp->lazy_segcblist, head, js);
else
rcu_segcblist_add_cblist_tail(&rdp->cblist, head, js);
// 如果需要加速处理,则触发RCU核心机制
if (__is_kfree_rcu_offset((unsigned long)func))
rcu_accelerate_cbs(rdp->mynode, rdp);
// 如果当前CPU被离线,则将回调转移到在线CPU
if (unlikely(!rcu_segcblist_completely_offloaded(&rdp->cblist))) {
rcu_lockdep_assert_cblist_protected(rdp);
rcu_seg_inv_notify(rdp);
}
// 如果需要唤醒RCU核心机制,则执行唤醒操作
if (rcu_segcblist_need_wake(&rdp->cblist))
rcu_ring_wake(rdp);
}
这个函数完成了以下主要任务:
- 进行一些调试和安全检查。
- 将回调函数设置到
rcu_head
结构中。 - 将回调添加到适当的回调队列中。
- 如果需要,触发 RCU 核心机制来加速处理。
- 处理 CPU 离线情况下的回调转移。
- 如果需要,唤醒 RCU 核心机制。
invoke_rcu_callbacks 的简化实现:
static void invoke_rcu_callbacks(struct rcu_state *rsp)
{
struct rcu_data *rdp;
struct rcu_head *rhp;
long n;
for_each_online_cpu(n) {
rdp = per_cpu_ptr(rsp->rda, n);
while ((rhp = rcu_callback_dequeue(rdp)) != NULL)
rhp->func(rhp);
}
}
这个函数遍历所有在线CPU,处理每个CPU的回调队列。
3.2.2.2 关键点
- 解耦:
call_rcu
和invoke_rcu_callbacks
的分离允许 RCU 更新操作快速完成,而不需要等待所有读者完成。 - 延迟执行:
call_rcu
注册的回调不会立即执行,而是等到安全的时机(grace period 结束后)才由invoke_rcu_callbacks
执行。 - 性能优化:这种机制允许批量处理回调,减少了同步开销,提高了系统整体性能。
- 灵活性:回调机制为 RCU 用户提供了极大的灵活性,允许他们定义在 grace period 结束后需要执行的清理操作。
call_rcu
和 invoke_rcu_callbacks
共同构成了 RCU 的回调处理系统。call_rcu
负责注册回调,而 invoke_rcu_callbacks
负责在适当的时机执行这些回调。这种设计使得 RCU 能够在保证数据一致性的同时,实现高效的无锁读取和低延迟的更新操作。
2.4. RCU的优化
2.4.1 Tree RCU
为了提高可扩展性,Linux内核实现了Tree RCU。它使用树状结构来组织CPU,减少了全局同步的需求。
struct rcu_node {
raw_spinlock_t lock;
unsigned long gp_seq;
unsigned long gp_seq_needed;
unsigned long qsmask;
u8 wait_blkd_tasks;
u8 child_overflow;
u8 forced_restart;
struct rcu_node *parent;
unsigned long grpmask;
int level;
struct rcu_node *rcu_node_tree;
...
};
rcu_node
结构体代表树中的一个节点,管理一组CPU的RCU状态。
2.4.2 RCU加速
Linux内核还实现了一些RCU的加速机制,比如 Expedited RCU grace periods。
void synchronize_rcu_expedited(void)
{
bool need_full_sync = false;
struct rcu_node *rnp;
unsigned long flags;
int ret;
ret = synchronize_rcu_expedited_wait();
if (ret > 0)
return; // 快速路径成功
// 如果快速路径失败,回退到完整同步
synchronize_rcu();
}
这个函数首先尝试一个快速路径,如果失败则回退到标准的 synchronize_rcu()
。
2.4 RCU栅栏
RCU栅栏的主要目的是确保所有先前的RCU读取临界区都已经完成。它的核心实现是synchronize_rcu()
函数。
void synchronize_rcu(void)
{
RCU_LOCKDEP_WARN(!rcu_is_watching(),
"synchronize_rcu() called with preemption or interrupts disabled");
if (rcu_expedited)
synchronize_rcu_expedited();
else
wait_rcu_gp(call_rcu);
}
这个函数的主要步骤:
- 首先进行锁依赖检查,确保调用环境正确。
- 如果启用了快速模式(
rcu_expedited
为true),调用synchronize_rcu_expedited()
。 - 否则,调用
wait_rcu_gp(call_rcu)
等待一个完整的宽限期(grace period)。
void wait_rcu_gp(call_rcu_func_t crf)
{
struct rcu_synchronize rcu;
init_rcu_head_on_stack(&rcu.head);
init_completion(&rcu.completion);
crf(&rcu.head, wakeme_after_rcu);
wait_for_completion(&rcu.completion);
destroy_rcu_head_on_stack(&rcu.head);
}
wait_rcu_gp()
函数的工作流程:
- 初始化一个
rcu_synchronize
结构体。 - 设置一个RCU回调(
wakeme_after_rcu
)。 - 等待回调完成,这保证了所有先前的RCU读取临界区都已结束。
2.5 周期性检查
RCU的周期性检查主要通过rcu_check_callbacks()
函数实现,它被定期调用来处理RCU的各种任务。这通常通过软中断处理程序完成,例如rcu_sched_qs()
和rcu_bh_qs()
。
void rcu_check_callbacks(int cpu, int user)
{
struct rcu_state *rsp;
if (user || rcu_is_cpu_rrupt_from_idle()) {
*this_cpu_ptr(&rcu_dynticks.dynticks) |= RCU_DYNTICK_CTRL_MASK;
rcu_dynticks_task_enter();
}
for_each_rcu_flavor(rsp)
__rcu_check_callbacks(rsp, cpu);
}
这个函数的主要任务:
- 更新当前CPU的动态时钟状态。
- 对每种RCU风格(如RCU-bh、RCU-sched等)调用
__rcu_check_callbacks()
。
static void __rcu_check_callbacks(struct rcu_state *rsp, int cpu)
{
struct rcu_data *rdp = per_cpu_ptr(rsp->rda, cpu);
if (check_for_new_grace_period(rsp, rdp))
invoke_rcu_core();
if (rcu_pending(cpu))
invoke_rcu_core();
}
- 检查是否需要开始新的宽限期。
- 检查是否有待处理的RCU操作。
- 如果需要,调用
invoke_rcu_core()
来处理RCU核心逻辑。
5. 总结
RCU是一个复杂而强大的同步机制,它通过允许读操作几乎无开销地进行,同时保证写操作的正确性,极大地提高了系统的并发性能。其核心在于巧妙地利用了硬件缓存一致性协议和操作系统调度特性,实现了一种"等待"所有读操作完成的机制,而无需显式地跟踪每个读操作。
这个分析涵盖了RCU的基本概念、主要组成部分、核心实现以及一些优化技术。然而,由于RCU的复杂性,实际的内核实现还包含了大量的细节和特殊情况处理,这里只是概述了其中的核心内容。
2.例示代码
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/init.h>
struct RCUStruct {
int a;
struct rcu_head rcu;
};
static struct RCUStruct* Global_pointer;
static struct task_struct* RCURDThread1, *RCURDThread2, *RCUWTThread;
static int RCURDThreadFunc1(void* argc) {
struct RCUStruct* pointer = NULL;
while(1) {
msleep(5);
rcu_read_lock();
mdelay(10);
pointer = rcu_dereference(Global_pointer);
if(pointer)
printk("%s : read a = %d\n", __func__, pointer->a);
rcu_read_unlock();
}
return 0;
}
static int RCURDThreadFunc2(void* argc) {
struct RCUStruct* pointer = NULL;
while(1) {
msleep(5);
rcu_read_lock();
mdelay(10);
pointer = rcu_dereference(Global_pointer);
if(pointer)
printk("%s : read a = %d\n", __func__, pointer->a);
rcu_read_unlock();
}
return 0;
}
static void MyRCUDel(struct rcu_head* rcuh) {
struct RCUStruct* p = container_of(rcuh, struct RCUStruct, rcu);
printk("%s : a = %d\n", __func__, p->a);
kfree(p);
}
static int RCUWTThreadFunc(void* argc) {
struct RCUStruct* old_pointer;
struct RCUStruct* new_pointer;
int value = (unsigned long)argc;
while(1) {
msleep(10);
new_pointer = kmalloc(sizeof(struct RCUStruct), GFP_KERNEL);
old_pointer = Global_pointer;
*new_pointer = *old_pointer;
new_pointer->a = value;
rcu_assign_pointer(Global_pointer, new_pointer);
call_rcu(&old_pointer->rcu, MyRCUDel);
printk("%s : write to new %d\n", __func__, value);
value++;
}
return 0;
}
static int __init RCUFuncInit(void) {
int value = 2;
printk("Prompt:Successfully initialized the kernel module.\n");
Global_pointer = kzalloc(sizeof(struct RCUStruct), GFP_KERNEL);
RCURDThread1 = kthread_run(RCURDThreadFunc1, NULL, "RCURD1");
RCURDThread2 = kthread_run(RCURDThreadFunc2, NULL, "RCURD2");
RCUWTThread = kthread_run(RCUWTThreadFunc, (void*)(unsigned long)value, "RCUWT");
return 0;
}
static void __exit RCUFuncExit(void) {
printk("Prompt:Successfully uninstalled kernel module!\n");
kthread_stop(RCURDThread1);
kthread_stop(RCURDThread2);
kthread_stop(RCUWTThread);
if(Global_pointer)
kfree(Global_pointer);
}
module_init(RCUFuncInit);
module_exit(RCUFuncExit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("lenn louis");
3.Makefile
obj-m:=rcu.o
CURRENT_PAHT:=$(shell pwd)
LINUX_KERNEL:=$(shell uname -r)
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PAHT) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PAHT) cleals
相关链接