问题理解

  • 关于RCU 读-复制-更新,这类锁我有点不理解的是为什么已经说了RCU不同于自旋锁,使用RCU的读端没有锁、内存屏障、原子指令类的开销,那为什么还要用 rcu_read_lock()rcu_read_unlock() 函数呢?
  • rcu_read_lock()rcu_read_unlock() 到底是在做什么?

名字误导

  • rcu_read_lock()rcu_read_unlock() 可能是 Linux 内核中命名最具有误导性的函数之一。
    1. 它们并不是“锁”(in the sense of a spinlock or mutex)。
    2. 它不会让任何人在等待中“自旋”(spin)。
    3. 它不会导致睡眠或上下文切换。
    4. 它不会阻止其他 CPU 上的读或写操作。
    5. 它不会执行昂贵的原子操作或内存总线同步。

rcu_read_lock() 真正的工作

  • 它真正的、也是唯一的工作是:充当一个“标记”,告知 RCU 系统:“这个 CPU 现在正处于 RCU 读临界区”。
  • 在绝大多数“经典”的 Linux 内核实现中 (例如 PREEMPT_RCU),这两个函数做的事情极其简单:
    • rcu_read_lock() -> preempt_disable() (禁用内核抢占)
    • rcu_read_unlock() -> preempt_enable() (恢复内核抢占)

为什么要禁用内核抢占?

  • 这解决了我们问题的核心:既然 RCU 这么快,为什么还需要做“禁用抢占”这个动作?
  • 我们必须理解 RCU 更新的“契约”是什么。RCU 的写端(更新者)在删除旧数据时,必须等待一个所谓的“宽限期” (Grace Period) 结束。
  • “宽限期”的定义是:等待所有 CPU 都至少发生过一次“上下文切换”或“进入空闲”。(这被称为“静止状态”,Quiescent State)。
  • RCU 系统以此来保证:只要宽限期结束,那么所有在宽限期开始前 就已经 在读取旧数据的 CPU,现在 一定 已经读取完毕了。

举例

  • 现在,假设没有 rcu_read_lock()
    1. CPU 0 开始读取一个 RCU 保护的数据 p。(比如 my_data = p->value;
    2. 刚执行完 my_data = p->value;,CPU 0 发生“内核抢占”(比如一个更高优先级的任务来了),当前任务被换下。
    3. CPU 1 (写端) 此时发起一个更新,它用 p_new 替换了 p,并要求系统在宽限期后释放 p(旧数据)。
    4. 宽限期开始…
    5. 系统检测到所有 CPU(包括 CPU 0)都发生了上下文切换(因为 CPU 0 刚刚在第 2 步被抢占了)。
    6. 宽限期结束! RCU 系统认为:“所有 CPU 都已经‘登记’过了,现在释放 p 是安全的。”
    7. p 的内存被释放。
    8. 过了很久
    9. CPU 0 上的那个被抢占的任务终于被调度回来了。它继续执行 rcu_read_lock 临界区里的下一行代码,比如 next_value = p->next;
    10. BOOM! 💥 p 已经被释放了。CPU 0 访问了一个“野指针”,导致系统崩溃(Use-After-Free)。

如何解决的

  • rcu_read_lock() (即 preempt_disable()) 如何解决这个问题:
    • rcu_read_lock() 禁用了抢占。
    • CPU 0 在读取 p 期间,绝不可能被其他任务抢占。
    • 它必须执行完 rcu_read_unlock() (即 preempt_enable()) 之后,才可能发生上下文切换。
    • RCU 的宽限期必须等到 CPU 0 执行完 rcu_read_unlock() 并真正发生上下文切换后,才能结束。
    • 这就保证了,只要 rcu_read_lock() 临界区还没有退出,宽限期就一定不会结束,旧数据 p 就一定不会被释放。

这是否违背了“零开销”的承诺

  • 完全没有!这也正是 RCU 设计的精妙之处。
  • preempt_disable()preempt_enable() 的开销是什么?
  • 在大多数架构上,它只是修改当前 CPU 的一个本地变量(比如 preempt_count 计数器)。它不需要昂贵的原子操作,更不需要和任何其他 CPU 通信。这个开销几乎为零,比一个 spin_lock()(需要总线锁定的原子 cmpxchg)快了几个数量级。
  • 所以,我们最初的理解是对的: RCU 读端确实没有“锁、内存屏障、原子指令”(在“与其他 CPU 同步”的意义上)。它所做的 rcu_read_lock(),只是一个极其廉价的“本地标记”(禁用抢占),用于和 RCU 的垃圾回收机制(宽限期)进行“沟通”。
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐