浅谈linux读写同步机制RCU

RCU是linux系统的一种读写同步机制,说到底他也是一种内核同步的手段,本问就RCU概率和实现机制,给出笔者的理解。

【RCU概率】

我们先看下内核文档中对RCU的定义:

RCU is a synchronization mechanism that was added to the Linux kernel during the 2.5 development effort that is optimized for read-mostly situations.

翻译:RCU是在2.5版本内核引入的一种同步机制,目的在于优化数据读取较多之场景下的效率。

说道读读多写少的场景,我们能自然联想到读写锁,不错,RCU正是和读写锁相似的一种提高读多写少场景下代码执行效率的机制,它的核心思想就是“订阅发布”机制。

实际上,我们使用锁来保护互斥资源,无非就是防止这两种情况:

1)读者在读取数据时,写者对数据同时进行改写,导致读者读到不完整的数据
2)写者在写数据时,有另一写者同时写数据,导致数据被写脏
由此我们很早久已经使用了各种锁机制来保护互斥资源,而且针对读多写少的情况,我们还专门优化出读写锁,使得在没有写者的情况下,多个读者可以并行持锁,从而可以并行读取数据,提高效率。那么有没有一种去锁的办法实现对互斥资源的保护呢?所以这里RCU机制就登场了。它的核心思想是:互斥数据采用指针来访问,当写者想要更新数据时,先将数据复制一份,对复制的数据进行修改,这样可以不干扰同一时间正在读取数据的读者。当修改完毕后,通过指针赋值,将旧数据指针更新指向到新的数据。最后再完成对旧数据的释放,释放时需要等待正在使用之前旧数据的读者退出临界区,而等待的这段时间在RCU机制中被称作“宽限期”。这里几个重要的概念就是“写时复制”、“指针赋值”、以及“宽限期”。它就像杂志订阅和发布,读者读取数据就好比订阅杂志,写者
复制并修改数据好比杂志的编辑,最后通过指针赋值更新数据久好比杂志的发布,而宽限期等待就好比期刊的发布周期,所以这是一个形象的比喻。通过这种机制,我们可以实现读者的去锁,它有如下几个特点:

1)读者读取数据不需要枷锁,因为数据时通过指针赋值更新的,而现代CPU处理器基本都可以保证指针赋值的原子性,另外写者保证在指针赋值前数据已经修改好,所以读者读到的数据始终是完整的,无需加锁
2)写者必须通过“写时复制”和“指针赋值”的方式更新数据,而对旧数据释放前需要等待数据更新前已经读取了旧数据的读者完成对旧数据的使用。
3)写者和写者直接仍然需要锁来互斥同步,但由于RCU的使用场景时多读写少,所以开销是可以接受的。

内核文档明确指出了一个RCU数据更新的典型步骤:

a. Remove pointers to a data structure, so that subsequent
readers cannot gain a reference to it.

b. Wait for all previous readers to complete their RCU read-side
critical sections.

c. At this point, there cannot be any readers who hold references
to the data structure, so it now may safely be reclaimed
(e.g., kfree()d).

翻译:

a. (通常是从链表中)移除指向数据结构(通常是链表节点)的指针, 使得后续读者无法再(通过链表)引用这个数据

b. 等待移除数据之前已经读取并正在使用该数据的读者退出临界区

c. 此时,已经没有读者在使用这个数据结构了,因此它可以被安全的回收

举个例子,比如有如下这样一个链表:

      ____       ____       ____
-->|__A_|-->|__B_|-->|__C_|-->...

现需要将B链表回收,那么:

a. 先将B节点从链表中移除,此后则不会再有读者能访问到B节点了,移除后情况如下:

      ____       ____                       ____
-->|__A_|-->|__C_|-->...     N-->|__C_|

其中“N”表示此时正在使用C节点的N个读者,虽然C已经不在链表当中,但仍有读者持有指向C的指针,所以暂时C的内存还不能回收

b. 等待所以正在使用C节点的读者使用完毕,即退出临界区,此时情况如下:

      ____       ____                      ____ 
-->|__A_|-->|__C_|-->...     0-->|__C_|

“0”表示已经没有读者使用C节点了,因此可以安全回收

c. 销毁C节点,回收内存:

      ____       ____            
-->|__A_|-->|__C_|-->...   

d. 如果不想删除B,而只是想更新B的内容,那么此时便以安全的修改,修改完毕后果再将B节点以原子的方式插回队列中,如下:

      ____       ____       ____ 
-->|__A_|-->|__B_|-->|__C_|-->...

那么,这里有几个关键点没有讲清楚:

1. 如何知道当前有那些读者进程正在使用C节点呢?

2. 读者全部退出临界区的时候,如果通知出来呢?

所以,内核要给我们提供API去完成这些事情,请继续往下看。

 

【RCU的核心API】

内核文档列出了如下几个核心API函数:

a. rcu_read_lock()
b. rcu_read_unlock()
c. synchronize_rcu() / call_rcu()
d. rcu_assign_pointer()
e. rcu_dereference()
就是说这5个API时最基本的,还有其他一些API,但是都可以通过这5个API的组合来实现,下面一一讲解:

a. void rcu_read_lock(void);
翻译:用于通知回收者当前读者已进入临界区,在读者的临界区里时不允许阻塞的。

b. void rcu_read_unlock(void);
用于通知回收者当前读者已经退出临界区。

c. void synchronize_rcu(void);
synchronize_rcu用于等待在synchronize_rcu调用之前通过rcu_read_lock进入临界区的读者(在synchronize_rcu调用之后进入临界区的并不关心),在此之前函数会一直阻塞,当返回时,旧数据可以被安全的释放。

内核文档还给了一个例子,自己体会:
      CPU 0                            CPU 1                             CPU 2
-----------------           -------------------------              ---------------
1. rcu_read_lock()
2.                         enters synchronize_rcu()
3.                                                                       rcu_read_lock()
4. rcu_read_unlock()
5.                          exits synchronize_rcu()
6.                                                                      rcu_read_unlock()

d.typeof(p) rcu_assign_pointer(p, typeof(p) v);

这是一个宏实现,也只能是宏,自己体会下(提示:typeof。。。)
引用一段内核文档原话:The updater uses this function to assign a new value to an RCU-protected pointer, in order to safely communicate the change in value from the updater to the reader. This function returns the new value, and also executes any memory-barrier instructions required for a given CPU architecture.
这个函数就是用来完成前面提到的“指针赋值”的动作的,它会处理一些内存屏障的情况,否则我们直接赋值就是了,何必用这个宏呢?

e. typeof(p) rcu_dereference(p);
同样时通过宏实现的, 内核文档的解释:
The reader uses rcu_dereference() to fetch an RCU-protected pointer, which returns a value that may then be safely dereferenced. Note that rcu_deference() does not actually dereference the pointer, instead, it protects the pointer for later dereferencing. It also executes any needed memory-barrier instructions for a given CPU architecture.

这段话比较难懂,但说白了就是,当你想获取一个指向某个RCU数据时,rcu_dereference能返回一个安全的引用。 这里dereference是个很有意思的词,大家可以查下reference和dereference的区别,很好玩。

 

 

【总结】

理解RCU机制的关键点就是如何去理解“订阅发布”,确实如此,我们在APP商店购买应用的时候,用户得到的都是一个完整可用的APK,即最终产品的样子,而应用的开发过程是不会让用户看到的。作者要更新软件时,会线下修改,改好之后推送更新,即发布。同理,RCU机制在更新数据时,先将数据从链表中移除(类似商品下架),然后等待正在使用该数据的读者使用完毕,这段时间我们叫“宽限期”(类似以下架应用仍然继续提供客服,但会有一个期限),等宽限期过后,便修改跟新,然后重新插回链表中(类似应用重新上架)。这是一个非常巧妙的设计,需要花些时间去理解,但是一旦理解, 就很容易掌握这些概念了,甚至不需要任何记忆。

 

posted @ 2017-01-14 23:09  泡面狂  阅读(2250)  评论(0编辑  收藏  举报