原文网址:https://lwn.net/Articles/262464
原文作者:Paul E. McKenney 和 Jonathan Walpole
介绍
2002年10月Linux内核实现了RCU同步机制(RCU读-复制的更新)。对竞争对象存在少量写操作而大量读操作的场景下,RCU可以获得很好的可扩展性。传统互斥型的锁原语,完全没有考虑线程的读/写特点;而读写锁机制虽然允许多个读操作可以并行,但写操作依然要求是独占型【即有一个写操作时,不允许其他的读/写操】;但是RCU机制允许一个写操作和多个读操作可以并行执行。
为了支持多个读操作可并行执行, RCU机制给“竞争对象”维护了多个版本【副本】,只要对“竞争对象”的读操作还没有完成【我理解为,没有退出“关键代码”部分】,“竞争对象”的版本就不会被全部释放掉。RCU定义和使用了高效可扩展机制,用于发布和读取对象的新版本,并延迟回收旧版本。RCU的读/更新机制使得读操作非常快。在某些情况下(非抢断内核条件下),RCU机制的读操作原语开销几乎为零。
快速测试1:seqlock是否允许读操作和写更新操作并行执行呢?【回答:不允许】
为什么RCU允许读操作和写更新操作并行执行呢? RCU的基本工作原理是什么? 【or, not infrequently, the assertion that RCU cannot possibly work】这篇文档解释了RCU的基本原理;第二篇文档从API接口角度来介绍RCU的使用。最后一篇文档还介绍了参考文献。
RCU由三部分机制组成:插入、删除、以及允许读操作能容忍插入/删除操作。
插入操作【Publish-Subscribe Mechanism】
删除操作【Wait For-Existing RCU Readers to Complete】
读访问【Maintain Multiple Versions of Recently Updated Objects】
Publish-Subscribe机制
RCU的一个关键属性就是,可以安全地扫描数据,即使这个数据正在被修改。为了提供并行插入能力,RCU采用了Publish-Subuscribe机制。举例说明,一个全局的struct foo结构体指针gp,静态定义为NULL;在动态运行过程中,新分配一个struct foo数据结构,紧接着对这个新分配的struct foo数据结构的各个域进行设置,最后再将gp指向该struct foo数据结构;参见下面代码。
1 struct foo {
2 int a;
3 int b;
4 int c;
5 };
6 struct foo *gp = NULL;
7
8 /* . . . */
9
10 p = kmalloc(sizeof(*p), GFP_KERNEL);
11 p->a = 1;
12 p->b = 2;
13 p->c = 3;
14 gp = p;
上述代码并没有强制要求编译器和CPU去保证最后四行代码按照顺序执行。
如果最后一行代码gp = p在代码p->c/b/a = ....之前执行,此时如果有利用gp并行访问,就可能读到新分配struct foo数据结构中未初始化的数值。因此最后一行代码gp = p 之前必须有存障指令,来保证最后一行代码在执行之前前面所有的代码必须执行完成;但是存障指令的使用对程序员要求比较高。因此,我们把它封装到rcu_assign_point()这个函数中,这就是Publication。因此最后四行改写为:
1 p->a = 1;
2 p->b = 2;
3 p->c = 3;
4 rcu_assign_pointer(gp, p);
rcu_assign_pointer()发布了新的数据结构,这个函数会强制编译器和CPU保证最后一行代码在执行之前,前面所有的代码必须执行完成,即struct foo数据结构中的各个域都初始化完成后,才将gp指向该数据结构。
然而这还不够,还需要读访问也必须保证正确的顺序,考虑如下代码:
1 p = gp;
2 if (p != NULL) {
3 do_something_with(p->a, p->b, p->c);
4 }
尽管上述代码看上去好像不会乱序,不幸的是,DEC Alpha处理器和采用数据预测优化机制的编译器会在指针p更新之前就获取p->a/b/c。采用数据预测优化机制的编译器会提前猜测指针p的数值,并取出p->a/b/c的数值,然后再取出指针p的数值看看它前面的猜测是否正确。这种优化相当激进,甚至有点疯狂,但却真实存在。
因此,我们需要防范处理器和编译器造成的欺骗。rcu_dereference()原语采用内存屏障指令或编译器指令来防止上述问题发生。
1 rcu_read_lock();
2 p = rcu_dereference(gp);
3 if (p != NULL) {
4 do_something_with(p->a, p->b, p->c);
5 }
6 rcu_read_unlock();
rcu_dereference()原语可以看作是,对特定指针值的订阅,从而保证后续引用操作能够完全看到发布rcu_assign_pointer()
之前的初始化结果。
rcu_read_lock()
和 rcu_read_unlock()
必须要加上,它们之间定义了RCU读方的临界范围。rcu_read_lock不既不会自旋不会阻塞,也不会阻止list_add_rcu的并行执行。在非抢断内核中,他们就是空代码。
理论上,rcu_assign_pointer()和rcu_dereference()可以用于需要被RCU保护的任何数据结构,实际应用中,最好还是用更高级的封装形式。也就是说,rcu_assign_pointer()和rcu_dereference()原语已经被封装到Linux链表操作API的特定RCU变量当中了。
(未完待续)