看的是 2.6.18.3 版本的 rcu 代码, 这个版本其实还没有区分什么 tiny rcu, preempt rcu, 这些都出现在以后的版本, 之所以选择这个, 完全是因为刚开始看 rcu 时搜索到的网文引用的代码是这个版本的, 而且也算是早期的实现, 代码比较少, 容易看清脉络。 rcu 的原理大概就相当于一个读写锁, 不同的是实现方式: rcu 要求被读的数据被一个指针指向, 读者先读这个指针, 而后读指针指向的数据; 写者也先读这个指针及其数据, 不同的是它而后分配出一块新内存, 做一次拷贝, 而后修改在这个拷贝上进行, 等修改完毕, 将指针指向这块新内存;而后将旧内存释放掉。 它凭借的是指针赋值是原子操作这个事实。
rcu API 相当容易使用, 实现原理却是不太好理解, 原因在于几个状态的变迁, 如果有了全地图, 自然就没问题, 但如果从代码开始理清它思路的, 很容易被这些状态弄晕, 主要涉及的函数:
307/* 308 * cpu went through a quiescent state since the beginning of the grace period. 309 * Clear it from the cpu mask and complete the grace period if it was the last 310 * cpu. Start another grace period if someone has further entries pending 311 */ 312static void cpu_quiet(int cpu, struct rcu_ctrlblk *rcp) 313{ 314 cpu_clear(cpu, rcp->cpumask); 315 if (cpus_empty(rcp->cpumask)) { 316 /* batch completed ! */ 317 rcp->completed = rcp->cur; 318 rcu_start_batch(rcp); 319 } 320} 321 322/* 323 * Check if the cpu has gone through a quiescent state (say context 324 * switch). If so and if it already hasn't done so in this RCU 325 * quiescent cycle, then indicate that it has done so. 326 */ 327static void rcu_check_quiescent_state(struct rcu_ctrlblk *rcp, 328 struct rcu_data *rdp) 329{ 330 if (rdp->quiescbatch != rcp->cur) { 331 /* start new grace period: */ 332 rdp->qs_pending = 1; 333 rdp->passed_quiesc = 0; 334 rdp->quiescbatch = rcp->cur; 335 return; 336 } 337 338 /* Grace period already completed for this cpu? 339 * qs_pending is checked instead of the actual bitmap to avoid 340 * cacheline trashing. 341 */ 342 if (!rdp->qs_pending) 343 return; 344 345 /* 346 * Was there a quiescent state since the beginning of the grace 347 * period? If no, then exit and wait for the next call. 348 */ 349 if (!rdp->passed_quiesc) 350 return; 351 rdp->qs_pending = 0; 352 353 spin_lock(&rcp->lock); 354 /* 355 * rdp->quiescbatch/rcp->cur and the cpu bitmap can come out of sync 356 * during cpu startup. Ignore the quiescent state. 357 */ 358 if (likely(rdp->quiescbatch == rcp->cur)) 359 cpu_quiet(rdp->cpu, rcp); 360 361 spin_unlock(&rcp->lock); 362}
419/* 420 * This does the RCU processing work from tasklet context. 421 */ 422static void __rcu_process_callbacks(struct rcu_ctrlblk *rcp, 423 struct rcu_data *rdp) 424{ 425 if (rdp->curlist && !rcu_batch_before(rcp->completed, rdp->batch)) { 426 *rdp->donetail = rdp->curlist; 427 rdp->donetail = rdp->curtail; 428 rdp->curlist = NULL; 429 rdp->curtail = &rdp->curlist; 430 } 431 432 if (rdp->nxtlist && !rdp->curlist) { 433 local_irq_disable(); 434 rdp->curlist = rdp->nxtlist; 435 rdp->curtail = rdp->nxttail; 436 rdp->nxtlist = NULL; 437 rdp->nxttail = &rdp->nxtlist; 438 local_irq_enable(); 439 440 /* 441 * start the next batch of callbacks 442 */ 443 444 /* determine batch number */ 445 rdp->batch = rcp->cur + 1; 446 /* see the comment and corresponding wmb() in 447 * the rcu_start_batch() 448 */ 449 smp_rmb(); 450 451 if (!rcp->next_pending) { 452 /* and start it/schedule start if it's a new batch */ 453 spin_lock(&rcp->lock); 454 rcp->next_pending = 1; 455 rcu_start_batch(rcp); 456 spin_unlock(&rcp->lock); 457 } 458 } 459 460 rcu_check_quiescent_state(rcp, rdp); 461 if (rdp->donelist) 462 rcu_do_batch(rdp); 463}
rcu 从整体来说, 就是一个个 grace period 组成的块, 每个块有起始有结束, 起始的时候将系统内所有(暂且认为是所有) cpu 加入到一个 set 内, 等待各个 cpu 发生一次抢占, 叫做经历一次 quiesc, 最后一个 cpu 经历 quiesc 后, 则 grace period 结束。 再细说, 各个 cpu 也可以认为 quiesc 有开始有结束, quiesc 开始于 cpu 注意到 grace period 开始的时候, 结束于 cpu 注意到发生了一次抢占的时候, 这个过程不知道有没有术语, 我们暂且将它叫做 quiesc period。
代码从实现上来讲, 有个非常讲究的地方, 却常常被 rcu 大体思路给掩盖, 很难被注意到, 我要是不是尝试将 rcu 在用户空间实现一遍, 恐怕也不会注意。 那就是, struct rcu_data* rdp 是 per cpu 变量。 他的含义比咋看起来要重要些, 因为这个保证了当 cpu 访问这些变量时, 总是没有缓存一致性问题,因为更改这些变量的和以后访问这些变量的总是同一个 cpu, 而且, read_lock_rcu/read_unlock_rcu 实际操作的也是 per cpu 变量 preempt, 标识 quiesc period 结束的操作 rcu_qsctr_inc 修改的也是 rdp, 因此这些操作不用担心缓存一致性问题, 只要修改了, 则必定会在以后的读中获得最新值。 因此, 必要的内存屏障操作被安全的省略了, read_lock_rcu read_unlock_rcu 与位于两者中间的读操作, 仅仅需要一个优化屏障即可保证正常运行。 而用户空间程序却做不到这一点, 从而无法达到内核 rcu 相同的读性能。 这让我写用户空间 rcu 的时候, 如鲠在喉, 无计可施。
只有 2 处是在 spin lock 保护之外访问的全局变量 rcp, 这两处地方也是让我困惑了几天的问题: 就是在多核下, 这两处无缓存一致性保证, 那怎么保证整个逻辑的正确性呢, 比如 rdp->batch = rcp->cur + 1; 这行代码, 很容易让人心里不安, 如果在之前 cpu 0 将 rcp->cur 修改成 1, 那么 cpu 1 执行这段代码时, rdp->batch 最终值有可能是 1, 也有可能是 2, 视 cpu 0 的修改有没有被 cpu 1 看到而异。 为了安慰心中这个不安, 几天来不断的思考, 代码虽然不多, 但心里演算的情景常常让我自己不知身在何处, 幸好的是, 随着思考不断进行, 终于让我理清了脉络, 并且 rcu 整体思路开始清晰起来, 原来与我纠缠的各种细节突然间不重要了, 似乎是物理中的所谓分离法与综合法, 纠缠了几天的分离法不得要领, 却偶尔在综合法上让我理解了。
关键一点在于 quiesc period 与 grace period 的关系, 就是 quiesc 从来都是在 grace period 开始之后开始, 在 grace period 结束之前结束。 这是在 rcu_check_quiescent_state 中保证的, 如下代码:
330 if (rdp->quiescbatch != rcp->cur) { 331 /* start new grace period: */ 332 rdp->qs_pending = 1; 333 rdp->passed_quiesc = 0; 334 rdp->quiescbatch = rcp->cur; 335 return; 336 } 337 338 /* Grace period already completed for this cpu? 339 * qs_pending is checked instead of the actual bitmap to avoid 340 * cacheline trashing. 341 */ 342 if (!rdp->qs_pending) 343 return; 344
rcu 初始化后, rdp->qs_pending == 0, rdp->quiescbatch == rcp->cur, 而 grace period 开始的标志就是 rcp->cur++, 因此, 在 grace period 开始之前, 第一个 if 语句恒为 false, 那么就导致 rdp->qs_pending 恒为 0, 从而在第二个 if 直接返回, 什么也不做。 就算 grace period 开始了, 由于缓存一致性, 也有可能在这里看不到, 但没关系, 看不到就和往常一样什么事情也不做, 多等几次, 缓存最终还是要更新的, 到时候第一个 if 可以命中, 就可以开始 quiesc period 了(注释将其也称为grace period, 有些混淆, 我仍将它叫做 quiesc period)。 后面几行非常重要,rdp->quiescbatch = rcp->cur 标识 quiesc period 的开始, rdp->qs_pending = 1, 标识一个quiesc period 尚未结束; rdp->passed_quiesc = 0 则是 cpu queisc 结束指示符, 当内核代码执行诸如抢占动作前, 会将这个变量置 1, 在后面的时钟中断处理中, 如果发现这个值为 1, 则将 rdp->qs_pending 置 0 标识quiesc period 的结束, 并将 cpu 在 rcp->cpumask 中的位清除, 以表示本 cpu 同意 grace period 结束, 至于能否结束, 则要看其他的 cpu 是否同意。 rdp->passed_quiesc = 0 还有一个作用是将 quiesc period 开始之前就 (多次) 置位的 passed_quiesc 的动作取消。
首先说一下 grace period 的含义, grace period 的开始代表的是 rcu 观察到至少一个 cpu 已经完成了至少一次数据更新操作,但可能善后工作还没有做, 例如上面说的释放旧数据; 而 grace period 的结束, 则代表者 rcu 认为所有的其他 cpu 都不再引用写者持有的旧数据了, 因此, 可以安全释放这份数据。 grace period 是上一轮 grace period 结束后, 第一次观察到一个 cpu 完成数据更新操作为开始的, 这时所有 cpu 上一轮 quiesc period 都已经结束, 都处于停止状态。 grace period 一旦启动, 则后继的肯定是各个 cpu 各自的 quiesc period 的启动, 停止; grace period 结束于最后一个 quiesc period 的结束之后。 在 grace period 执行期间, 如果其他 cpu 上也出现了写者数据更新操作, 则视这个 cpu 上的 quiesc period 启动与否而有不同行为, 如果没有启动, 则可以认为这个更新操作属于这个已启动的 grace period, 否则, 则将其归类于下一个 grace period。 但这个分类并不是 100% 正确, 会将本该分在这个 grace period 的结果分在了下一个 grace period, 这是 smp 系统的缓存一致性决定的, 存在偶然因素, 但产生的结果不是不可接受的 ---- 即不会引起旧数据的提前释放, 只会引起其释放延迟, 这个在 rcu 目标应用场景中, 是可以接受的。 相关代码如下:
433 local_irq_disable(); 434 rdp->curlist = rdp->nxtlist; 435 rdp->curtail = rdp->nxttail; 436 rdp->nxtlist = NULL; 437 rdp->nxttail = &rdp->nxtlist; 438 local_irq_enable(); 439 440 /* 441 * start the next batch of callbacks 442 */ 443 444 /* determine batch number */ 445 rdp->batch = rcp->cur + 1; 446 /* see the comment and corresponding wmb() in 447 * the rcu_start_batch() 448 */ 449 smp_rmb();
其实就是 rdp->batch = rcp->cur + 1; 这一句, 由于 smp 缓存一致性, 执行这个语句时, rcp->cur 有可能是最新值, 也有可能是 grace period 开始前的那个值 (grace period 的开始代码执行时 rcp->cur 会自加)。
归类似乎容易让人误解, 更准确的说法或者说是注册, 注册包含两步: 1. 提供 grace period 结束时需要的动作 (433 --- 438), 2. 注册到哪个 grace period, 即哪个 grace period 结束后被调用 (445 行)。注册到本次 grace period 的标识为 rdp->batch == rcp->cur, 这类操作有个共同特点, 就是当执行 rdp->batch = rcp->cur + 1 时, quiesc period 都还没有启动, 对于第一个实际引起 grace period 的更新操作这是显然的, 对于其他操作, 可以由由 rcp->cur 没有及时更新推导出来, 因为 quiesc period 的开始操作是在 spin lock 下执行的, 而 spin lock 隐含一个内存读写屏障, 因此, 一旦 quiesc period 已经启动, 则 rcp->cur 不可能仍然读不到最新值。
只要注册时 quiesc period 尚未启动, 则注册的动作在 grace period 结束后就可以安全的执行。rcu 对 quiesc period 是基于保守推断的, 意思是, 当启动 quiesc period 的时候, 他他并不判断运行在自己上面的代码是否引用了已经注册的写更新操作的旧数据, 而是保守的假设自己全部引用了; 同时他也不相信自己的 passed_quiesc 值, 因为很有可能在引用这些旧数据之前就已经设置了这个值。 基于这个推断, 它将自己的 passed_quiesc 值设为 0, 然后开始 quiesc period, 这个 period 的唯一目的就是等待再次发生一次抢占。
这里有一个基本结论, 就是对于任意 cpu 来说, 抢占只可能发生在它退出所有的读操作以后才能发生; 因为 rcu_read_lock / rcu_read_unlock 是基于引用计数的。 那么如果一个 cpu 发现自己发生了一次抢占, 则他会知道自己至少曾经有一瞬间, 他是处于任何读操作临界区之外的; 那么, 在 quiesc period 启动的时候所注册的所有的写操作的旧数据, 在退出所有读操作的那一瞬间, 那就不再引用了, 那么, 在那一瞬间之后呢, 会不会再次引用这些旧数据? 不会, 因为它就算再读相同的数据, 读到的也不过是写者已经更新的新数据。 最终的结果就是, 只要发生了一次抢占, 则本 cpu 就会同意 grace period 的结束, 也就是说, 所有写者不用再担心运行在这个 cpu 上的代码引用它的旧数据。 那么当最后所有 cpu 都退出 quiesc period 的时候, 只会剩下写者自己持有旧数据, 它可以任意操作旧数据了。
整整写了三天。。。。。。 不清楚到底是陷入了思维混乱还是本身逻辑就是这么复杂, 每每觉得想通的时候, 过一会儿就又迷糊了, 这一次貌似没太大问题了吧, 记下来是正确的, 照这样子, 估计过不了几天我该自己对着这文章思考再说什么了。