RCU锁基本概念

文章目录

  • 设计动机
  • 临界资源访问
  • 实现演化
    • 使用全局位图
    • 强制进入静默态
    • 使用全局位图的拷贝
    • 经典的RCU实现
    • SRCU锁 - Sleepable RCU

设计动机

在CPU核数很多的情况下,核间同步需要的硬件代价越来越大。频繁地数据同步会限制多核的性能,因为核间同步必然涉及所有CPU核。提高每个CPU核的使用率,比降低CPU执行延时更能发挥多核优势。全局锁比如spinlock的实现涉及核间同步,多核情况下会愈发耗性能。可以针对特定的场景涉及性能损耗更低的并发控制机制,RCU就是针对读多写少,设计的进程数据同步机制。RCU的读者不加锁,只有写者加锁,因此性能比锁机制更高。

临界资源访问

访问原则有两个:

  1. 读者访问临界资源时不加锁,只标记自己进入了临界资源区。
  2. 写者如果要更新临界资源,首先拷贝一份要更新的数据,在拷贝上进行数据修改,修改完之后,将更新的数据作为之后进入临界区的写者访问的数据,然后检查临界区是否有其它写者正在访问资源,如果有,等到所有读者资源访问结束,再删除旧的数据

写者等待所有读者资源访问结束的这段时间,叫做宽限期。RCU访问原则的最终目的有两个:

  1. 在写者更新临界前已经再访问临界区的读者,看到的临界区资源在整个更新过程中时不变的
  2. 在写者更新完,访问临界区的读者,看到的临界区资源是更新后的
    RCU允许宽限期内的读者看到的临界区资源不一致。

实现演化

使用全局位图

RCU锁基本概念_第1张图片
原理:每个cpu维护一个per-cpu计数器,计数器维护锁计数,一旦这个cpu进入静默态(计数器变成0),在全局的位图相应位清零。当全局位图都是0,表示宽限期结束。
优点:设计简单
缺点:

  1. 读操作慢,每个cpu进入静默态时都要去更新全局的位图
  2. 只有在进入静默态,cpu才去更新全局,如果cpu在统计的时间段内一直处于静默,全局位图不会更新。
  3. 更新全局位图的需要加锁,保证多核并发操作时的数据一致,这个违背了写者不加锁的原则。降低了性能。

强制进入静默态

RCU锁基本概念_第2张图片
RCU锁基本概念_第3张图片
原理:使用一个daemon来实现宽限期,当写者要求更新临界资源时,daemon主动抢占每个cpu,让每个cpu都执行一次daemon程序,目的是使cpu上下文发生切换,让cpu进入静默态。当每个cpu都执行了一次daemon程序,就可以宣称宽限期结束。
优点:设计简单
缺点:写者更新的效率比较低,需要维护一个daemon

使用全局位图的拷贝

RCU锁基本概念_第4张图片
原理:维护一个全局的位图,每当有cpu进入静默态,就拷贝一份全局的位图,在对应bit上清零
优点:相比使用全局位图,用拷贝的话不需要加锁,相比于第一种用锁实现数据同步,这里用MESI实现数据同步。
缺点:频繁访问全局位图代价比较大,CPU要频繁使用MESI协议保证所有核上的cache数据的一致性,同样涉及硬件。

经典的RCU实现

上面的实现效率低,原因有两个:

  1. cpu静默态是主动发起的,这总需要额外的工作,但kernel其实一直有统计信息可以表明静默态的特征。当kernel发生系统调用,陷入,上下文切换时,都是静默态的临界状态。同时cpu空闲和暂停,也间接表示了静默态的临界状态,这些状态中,只要一个状态向另外一个状态转换,就表明cpu一定是进入了静默态。所以只需要统计这些状态的计数,对比当前计数和一段时间前的计数,就可以知道这段时间内cpu是否有进入静默态。
  2. 在统计进入静默态的cpu时,需要保证多核看到的数据一致,要么就加锁,要么就拷贝。经典的实现是把静默态的统计放到per-cpu上,由于per-cpu变量是cpu的私有变量,只有本地cpu可以修改,所以不存在数据同步。

因此经典RCU算法实现如下:

  1. 等待宽限期的写者,注册一个callback到per-cpu的链表
  2. 在一个合适的时间点后,通知所有CPU,现在开始统计宽限期
  3. 当每个CPU收到通知后,知道自己要进入新的宽限期后,将自己的静默态统计次数的快照保存下来
  4. 每个CPU周期性的对比当前静默态次数和快照的值,如果相同表示没有进入静默态,如果不同,表示进入了静默态,记录下来。当最后一个CPU进入静默态后,表明宽限期结束
  5. 每个CPU都有手段知道宽限期的结束,所以可以任意触发一个CPU上的callback,用于通知写者

每个CPU上都注册一个callback,虽然浪费了内存,但是cpu不会有同步信息的操作,因此节约了cpu的成本,相当于用内存换CPU。
算法具体实现中还要考虑以下问题:

  1. 写者注册callback的时机是随即的,当一个CPU正处在一个宽限期的统计中,又来了一个callback,要求统计一个宽限期。这种情况需要考虑。
    解决方法:每个CPU不止维护一个当前宽限期的callback(curlist),还维护下一个宽限期的callback(nextlist),每个宽限期通过一个id(generation number)区别。当前CPU可能统计的是当前宽限期,但另一个CPU已经进入静默态,就可以统计下一个宽限期,不同CPU可能统计的宽限期不同,可以通过id区分,但不管怎样,所有CPU,只能统计最多两个宽限期。
  2. 宽限期的统计开始通知和结束通知,需要知会到每个cpu,需要高效
    解决方法:对每个CPU的静默态次数检查,放在了软中断中,当CPU进入静默态,在软中断上下文触发callback。

SRCU锁 - Sleepable RCU

  • 正常情况下,如果在临界区睡眠,相当于CPU进入了静默态,但RCU宽限期统计中不允许有静默态出现在读者访问的临界区里面,否则会影响宽限器的检测,最后造成宽限期无限延长。由于宽限期无限延长,对同一个CPU上的写者来说,注册宽限期结束时的callback是异步的,放到了软中断中,因此一个CPU上的一个进程就可以多次注册宽限期结束的callback,如此迭代最后会造成系统内存资源耗尽然后崩溃。因此RCU规定读者在访问临界区资源不允许睡眠。
  • 但在实时系统中,必须实现高优先级的进程抢占低优先级的进程,如果要考虑读者进程访问临界区时CPU被抢占,不得不睡眠的情况。因此提出了可睡眠的RCU。
  • SRCU不提倡在临界区睡眠,只考虑如果在临界区睡眠了,把其影响降到最低。通过两个手段来降低睡眠的影响:
  1. SRCU只提供宽限期检测的同步接口,当一个CPU上进程的写者发起宽限期检查后,只能同步地等待这个宽限期结束。这样保证了一个CPU上一个进程只能注册一个callback。
  2. 将宽限期检查和统计限制在一个子系统中而非整个系统上的进程。这样保证睡眠只影响这个子系统中的写者进程。

你可能感兴趣的:(内核)