深入理解并行编程-读书笔记 rcu

 

在当前的Linux内核中发挥着重要的作用。RCU主要针对的数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的使用RCU机制读取数据的时候不对链表进行耗时的加锁操作。这样在同一时间可以有多个线程同时读取该链表,并且允许一个线程对链表进行修改(修改的时候,需要加锁)。RCU适用于需要频繁的读取数据,而相应修改数据并不多的情景,例如在文件系统中,经常需要查找定位目录,而对目录的修改相对来说并不多,这就是RCU发挥作用的最佳场景。

RCU(Read-Copy Update),是 Linux 中比较重要的一种同步机制。顾名思义就是“读,拷贝更新”,再直白点是“随意读,但更新数据的时候,需要先复制一份副本,在副本上完成修改,再一次性地替换旧数据”。这是 Linux 内核实现的一种针对“读多写少”的共享数据的同步机制。

RCU机制解决了什么

在RCU的实现过程中,我们主要解决以下问题:

1、在读取过程中,另外一个线程删除了一个节点。删除线程可以把这个节点从链表中移除,但它不能直接销毁这个节点,必须等到所有的读取线程读取完成以后,才进行销毁操作。RCU中把这个过程称为宽限期(Grace period)。

2、在读取过程中,另外一个线程插入了一个新节点,而读线程读到了这个节点,那么需要保证读到的这个节点是完整的。这里涉及到了发布-订阅机制(Publish-Subscribe Mechanism)。

3、保证读取链表的完整性。新增或者删除一个节点,不至于导致遍历一个链表从中间断开。但是RCU并不保证一定能读到新增的节点或者不读到要被删除的节点。

RCU(Read-Copy Update),顾名思义就是读-拷贝修改,它是基于其原理命名的。对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。那么这个“适当的时机”是怎么确定的呢?这是由内核确定的,也是我们后面讨论的重点。

 

 

 

 

 

 

 

编写一个需要访问随时变化的数据的并行实时程序的问题

读-复制-更新(RCU)是一种同步机制

RCU允许读操作可以与更新操作并发执行,这一点提升了程序的可 扩展性。常规的互斥锁让并发线程互斥执行,并不关心该线程是读者 还是写者,而读/写锁在没有写者时允许并发的读者,相比于这些常规 锁操作,RCU在维护对象的多个版本时确保读操作保持一致,同时保证 只有所有当前读端临界区都执行完毕后才释放对象。RCU定义并使用了 高效并且易于扩展的机制,用来发布和读取对象的新版本,还用于延 后旧版本对象的垃圾收集工作。这些机制恰当地在读端和更新端分布 工作,让读端非常快速。在某些场合下(比如非抢占式内核里),RCU 读端的函数完全是零开销。

不幸的是,这块代码无法保证编译器和CPU会按照顺序执行最后4 条赋值语句。如果对gp的赋值发生在初始化p的各字段之前,那么并发 的读者会读到未初始化的值。这里需要内存屏障来保证事情按顺序发 生,可是内存屏障又向来以难用而闻名。所以这里我们用一句 rcu_assign_pointer()原语将内存屏障封装起来,让其拥有发布的 语义。最后4行代码如下。

p->a = 1;

rcu_assign_pointer(gp , p);

 

 

本节由一系列小问题组成,让你可以尝试运用本书之前提到的各 种RCU例子。每个小问题的答案都会给出一些提示,后续章节会给出详 细 的 解 决 办 法 。 rcu_read_lock ( ) 、 rcu_read_unlock ( ) 、 rcu_dereference ( ) 、 rcu_assign_pointer ( ) 和 synchronize_rcu()原语足以应付这些练习了。 小问题9.76:图5.9(count_end.c)中实现的统计计数用一把全 局锁来保护read_count()中的累加过程,这对性能和可扩展性影响 很大。该如何用RCU改造read_count(),让其拥有良好的性能和极佳 的可扩展性呢?(请注意,read_count()的可扩展性受到统计计数 需要扫描所有线程计数的限制。) 小问题9.77:5.5节给出了一段奇怪的代码,用于统计在可移除设 备上发生的I/O访问次数。这段代码的快速路径(启动一次I/O)开销 较大,因为需要获取一把读/写锁。该如何用RCU来改造这个例子,让 其拥有良好的性能和极佳的可扩展性呢?(请注意,一般情况下,I/O 访问代码的性能要比设备移除代码的性能重要。)

 

posted @ 2024-03-21 17:33  skyycj  阅读(5)  评论(0编辑  收藏  举报