RCU机制分析

转自http://liu1227787871.blog.163.com/blog/static/205363197201283022655801/

一、原理概述

RCU(Read-Copy Update),顾名思义就是读-拷贝修改,它是基于其原理命名的。对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。这个时机就是所有引用该数据的CPU都退出对共享数据的操作。
因此RCU实际上是一种改进的rwlock,读者几乎没有什么同步开销,它不需要锁,不使用原子指令,而且在除alpha的所有架构上也不需要内存栅(Memory Barrier),因此不会导致锁竞争,内存延迟以及流水线停滞。不需要锁也使得使用更容易,因为死锁问题就不需要考虑了。写者的同步开销比较大,它需要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其它写者的修改操作。读者必须提供一个信号给写者以便写者能够确定数据可以被安全地释放或修改的时机。有一个专门的垃圾收集器来探测读者的信号,一旦所有的读者都已经发送信号告知它们都不在使用被RCU保护的数据结构,垃圾收集器就调用回调函数完成最后的数据释放或修改操作。 RCU与rwlock的不同之处是:它既允许多个读者同时访问被保护的数据,又允许多个读者和多个写者同时访问被保护的数据(注意:是否可以有多个写者并行访问取决于写者之间使用的同步机制),读者没有任何同步开销,而写者的同步开销则取决于使用的写者间同步机制。但RCU不能替代rwlock,因为如果写比较多时,对读者的性能提高不能弥补写者导致的损失。
读者在访问被RCU保护的共享数据期间不能被阻塞,这是RCU机制得以实现的一个基本前提,也就说当读者在引用被RCU保护的共享数据期间,读者所在的CPU不能发生上下文切换,spinlock和rwlock都需要这样的前提。写者在访问被RCU保护的共享数据时不需要和读者竞争任何锁,只有在有多于一个写者的情况下需要获得某种锁以与其他写者同步。写者修改数据前首先拷贝一个被修改元素的副本,然后在副本上进行修改,修改完毕后它向垃圾回收器注册一个回调函数以便在适当的时机执行真正的修改操作。等待适当时机的这一时期称为grace period,而CPU发生了上下文切换称为经历一个quiescent state,grace period就是所有CPU都经历一次quiescent state所需要的等待的时间。垃圾收集器就是在grace period之后调用写者注册的回调函数来完成真正的数据修改或数据释放操作的。
比较是很读多写少的情况!

二、代码分析

1、rcu_read_lock 
rcu_read_lock  //读是可以嵌套的
      //禁止抢占
      //这是因为读操作是不允许阻塞的,不然会导致写操作延时太大
     //既然不允许阻塞,当然要禁止抢占
      preempt_disable();

2、 rcu_read_unlock 
rcu_read_unlock
      __rcu_read_unlock();
           preempt_enable();//使能抢占

3、rcu_dereference
功能:获得一个被RCU保护的指针

4、rcu_assign_pointer
写者使用该函数来为被 RCU 保护的指针分配一个新的值. 这样是为了安全从写者到读者更改其值. 这个函数会返回一个新值

5、synchronize_rcu
synchronize_rcu 
      wait_rcu_gp(call_rcu);
           crf(&rcu.head, wakeme_after_rcu);//等待操作完成以后就会调用wakeme_after_rcu
           wait_for_completion(&rcu.completion);//等待条件为真,否则一直阻塞
那么为什么会调用wakeme_after_rcu呢,这个调用是在时钟中断里面实现的,我们来看一看时钟中断处理函数:
s3c2410_timer_interrupt(int irq, void *dev_id)
      timer_tick();
           update_process_times(user_mode(get_irq_regs()));
                 rcu_check_callbacks(cp u, user_tick);
                       rcu_sched_qs(cpu);//表示经过了一次quiescent state,即一次切换,不过这个切换也可以在schedule中标识
                       rcu_preempt_check_callbacks();
                             invoke_rcu_callbacks();
                                     raise_softirq(RCU_SOFTIRQ);
唤醒RCU软中断,很显然我们要看一看RCU软中断注册的是什么处理函数:
open_softirq(RCU_SOFTIRQ, rcu_process_callbacks);
rcu_process_callbacks
      __rcu_process_callbacks
            __rcu_reclaim(rn, list);
                   head->func(head);
这里就是执行call_rcu注册的回调函数了!
上面我们只是顺路走了过来,并没有关注细节的东西,以后用到再说吧,现在我是没有水平去分析!

下面我们接着分析:
wakeme_after_rcu
        complete(&rcu->completion);//标识完成,那么在这个条件下等待的进程就会被唤醒,也就是wait_rcu_gp
也就是说synchronize_rcu被唤醒了!

我们来总结一下synchronize_rcu这个函数,首先会调用call_rcu注册回调函数,然后在某个条件下等待。等待发生一个时钟中断的时候,就会唤醒rcu软中断,ruc软中断处理函数会判断是否经过了一个grace period,也就是是否在写之前的读操作都退出了,如果都退出了那么就会执行回调函数,将条件置为真,等待被唤醒!所以synchronize_rcu这个函数的作用就是等待所有的读退出!

至于这个机制在内核中的使用,我们要去实际中去分析内核了!
<think>我们正在讨论Linux内核中的RCU(Read-Copy-Update)机制。用户要求理解RCU的工作原理及其在内核中的应用。根据之前的对话,用户已经接触过`dev_net_rcu`这样的函数,因此我们可以从实际例子出发,逐步深入。 RCU是一种同步机制,用于在读取操作远多于写入操作的情况下,提供高效、无锁的读取访问。它通过延迟回收旧数据副本来避免写者阻塞读者。 ### 一、RCU核心思想 1. **读取无锁**:读者不需要获取任何锁,只需在RCU读侧临界区内访问数据。 2. **写入延迟更新**:写者更新数据时,先创建数据的副本,修改副本,然后用新副本原子替换旧数据。旧数据不会立即回收,而是等待所有可能访问旧数据的读者离开临界区后再回收。 ### 二、RCU基本操作 1. **读者操作**: - 进入临界区:`rcu_read_lock()` - 访问共享数据 - 退出临界区:`rcu_read_unlock()` ```c rcu_read_lock(); struct net *net = dev_net_rcu(dev); // 安全获取网络命名空间 // 使用net进行操作 rcu_read_unlock(); ``` 2. **写者操作**: - 复制并更新数据 - 替换指针(原子操作) - 安排旧数据回收(通过`call_rcu`或`synchronize_rcu`) ```c // 假设更新网络设备所属的命名空间 struct net *new_net = ...; // 新命名空间 struct net *old_net = dev->nd_net; rcu_assign_pointer(dev->nd_net, new_net); // 原子替换 synchronize_rcu(); // 等待所有读者退出临界区 free_net(old_net); // 安全回收旧命名空间 ``` ### 三、RCU实现原理 1. **宽限期(Grace Period)**: - 指从写者更新数据到所有读者都离开临界区的时间段。 - 内核通过`rcu_gp_kthread`线程管理宽限期,周期性地推进宽限期序号[^1]。 2. **回调处理**: - 写者通过`call_rcu`注册回调函数,在宽限期结束后执行回收操作。 - 回调函数通常由软中断(`RCU_SOFTIRQ`)处理[^1]。 ### 四、RCU在内核中的应用实例 1. **网络设备命名空间管理**(如前所述): - `dev_net_rcu()`用于安全获取设备所属命名空间。 - 设备移动命名空间时使用RCU更新指针。 2. **路由表查询**: - 路由表更新频率低,但查询频率极高,适合RCU。 - 读者无锁查询路由表,写者更新后延迟回收旧路由表。 3. **内核链表遍历**: - `list_for_each_entry_rcu`宏用于安全遍历RCU保护的链表。 - 例如遍历网络协议处理程序列表: ```c list_for_each_entry_rcu(ptype, &ptype_all, list) { // 处理协议 } ``` ### 五、RCU性能优势 - **读者路径零开销**:无锁、无原子操作、无内存屏障(在弱序CPU上需`rcu_read_lock`屏障)。 - **写入开销集中**:写者承担复制和延迟回收的成本,但不阻塞读者。 ### 六、注意事项 1. **临界区限制**: - RCU读临界区内不能阻塞或睡眠。 - 临界区内访问的指针必须通过`rcu_dereference`读取,确保内存可见性[^4]。 2. **内存屏障**: - `rcu_read_lock`/`rcu_read_unlock`隐含内存屏障,防止指令重排。 ### 七、扩展应用 1. **睡眠RCU(SRCU)**:允许读者在临界区内睡眠,但开销更大[^2]。 2. **可抢占RCU(PREEMPT_RCU)**:支持实时内核,允许读者被抢占。 ### 总结 RCULinux内核中高性能读多写少场景的同步基石。其核心在于**无锁读取+延迟回收**,通过宽限期机制保证数据安全回收。正确使用RCU需严格遵循: 1. 读者用`rcu_read_lock/unlock`保护临界区 2. 写者用`rcu_assign_pointer`更新指针 3. 写者用`synchronize_rcu`或`call_rcu`回收旧数据 --- ### §§相关问题§§ 1. RCU宽限期的推进机制是如何实现的? 2. 在哪些场景下RCU比读写锁(rwlock)性能更优? 3. 如何调试内核中与RCU相关的问题(如RCU stall)? [^1]: `rcu_init`初始化RCU软中断处理,是RCU机制的基础设施。 [^2]: `srcu_init`初始化可睡眠RCU(SRCU),扩展了RCU的应用场景。 [^4]: `twsk_unique`等网络协议函数中隐含了RCU保护,确保指针访问安全。</think>### Linux 内核 RCU 机制详解 #### 一、RCU 核心原理 RCU(Read-Copy-Update)是一种**无锁同步机制**,专为读多写少的场景设计,其核心思想基于三个关键阶段: 1. **读取(Read)** - 读者直接访问共享数据**无需加锁** - 通过 `rcu_read_lock()` 和 `rcu_read_unlock()` 标记临界区 ```c rcu_read_lock(); struct net *net = dev_net_rcu(skb->dev); // 安全访问 // 处理数据... rcu_read_unlock(); ``` 2. **复制更新(Copy-Update)** - 写者创建数据副本进行修改 - 通过原子操作替换指针(如 `rcu_assign_pointer()`) 3. **延迟回收(Reclaim)** - 旧数据不会立即删除 - 等待**宽限期(Grace Period)** 结束(确保所有读者退出临界区) - 通过 `call_rcu()` 注册回收回调[^1] #### 二、关键机制实现 1. **宽限期管理** - 内核通过 `rcu_gp_kthread` 线程周期性推进宽限期 - 每个CPU维护**静止状态(Quiescent State)** 标记 - 当所有CPU都报告静止状态时,宽限期结束 2. **回调处理** - 回收操作由软中断 `RCU_SOFTIRQ` 处理 - `rcu_process_callbacks()` 函数执行延迟回收[^1] ```c open_softirq(RCU_SOFTIRQ, rcu_process_callbacks); // 初始化 ``` 3. **内存屏障保证** - `rcu_read_lock()` 插入读屏障(防止指令重排) - `rcu_assign_pointer()` 插入写屏障 #### 三、内核应用场景 1. **网络子系统** - 网络设备命名空间访问(`dev_net_rcu()`) - 协议处理列表遍历(`list_for_each_entry_rcu()`) ```c list_for_each_entry_rcu(ptype, &ptype_all, list) { // 安全遍历协议处理程序 } ``` 2. **虚拟文件系统(VFS)** - 目录项缓存(dentry)的无锁查找 - 文件描述符表的并发访问 3. **内存管理** - 页表项的并行访问 - 反向映射(reverse mapping)更新 4. **工作队列** - 延迟任务的安全回收(结合 `call_rcu()`)[^3] #### 四、性能优势对比 | 机制 | 读者开销 | 写者开销 | 适用场景 | |------------|---------------|---------------|-----------------| | **RCU** | 近乎零开销 | 高(复制+延迟) | 读>>写 | | **读写锁** | 原子操作+缓存同步| 阻塞写者 | 读写均衡 | | **自旋锁** | 忙等待 | 忙等待 | 极短临界区 | 优势分析: 1. **读者完全无锁**:无原子操作/内存屏障开销 2. **线性扩展性**:读者数量增加不影响性能 3. **无死锁风险**:读者不会阻塞写者 #### 五、使用规范 ```c // 读者端 rcu_read_lock(); struct data *p = rcu_dereference(ptr); // 受保护访问 // 使用p... rcu_read_unlock(); // 写者端 struct data *new = kmalloc(...); // 创建副本 copy_data(new, old); // 更新副本 rcu_assign_pointer(ptr, new); // 原子替换 call_rcu(&old->head, reclaim_callback); // 延迟回收 ``` **禁止行为**: - 读者临界区内阻塞/睡眠 - 写者直接修改原始数据(必须复制) - 忽略宽限期直接释放内存 #### 六、典型问题分析 1. **RCU Stall** - 现象:内核告警 "RCU Stall" - 原因:CPU未报告静止状态(如死循环未退出临界区) - 调试:`CONFIG_RCU_TRACE` 跟踪宽限期状态 2. **内存泄漏** - 忘记调用 `call_rcu()` 回收旧数据 - 解决方案:使用 `kfree_rcu()` 简化回收 3. **数据不一致** - 写者未使用 `rcu_assign_pointer()` - 读者未使用 `rcu_dereference()` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值