当前位置:首页 > 公众号精选 > Linux阅码场
[导读]内容基本原理使用方法GP的生命周期QS的判定与标记优势何在存在的问题RCU机制是自内核2.5版本引入的(2002年10月),而后不断完善,其在Linux的locking机制中的使用占比也是逐年攀升。1.基本原理RCU的基本思想是这样的:先创建一个旧数据的copy,然后writer...

内容


  1. 基本原理

  2. 使用方法

  3. GP 的生命周期

  4. QS 的判定与标记

  5. 优势何在

  6. 存在的问题



RCU机制是自内核2.5版本引入的(2002年10月),而后不断完善,其在Linux的locking机制中的使用占比也是逐年攀升。

1.基本原理RCU的基本思想是这样的:先创建一个旧数据的copy,然后writer更新这个copy,最后再用新的数据替换掉旧的数据。这样讲似乎比较抽象,那么结合一个实例来看或许会更加直观。
假设有一个单向链表,其中包含一个由指针p指向的节点:

现在,我们要使用RCU机制来更新这个节点的数据,那么首先需要分配一段新的内存空间(由指针q指向),用于存放这个copy。

然后将p指向的节点数据,以及它和下一节点[11, 4, 8]的关系,都完整地copy到q指向的内存区域中。

接下来,writer会修改这个copy中的数据(将[5, 6, 7]修改为[5, 2, 3])。

修改完成之后,writer就可以将这个更新“发布”了(publish),对于reader来说就“可见”了。因此,pubulish之后才开始读取操作的reader(比如读节点[1, 2, 3]的下一个节点),得到的就是新的数据[5, 2, 3](图中红色边框表示有reader在引用)。

而在publish之前就开始读取操作的reader则不受影响,依然使用旧的数据[5, 6, 7]。

等到所有引用旧数据区的reader都完成了相关操作,writer才会释放由p指向的内存区域。

可见,在此期间,reader如果读取这个节点的数据,得到的要么全是旧的数据,要么全是新的数据,反正不会是「半新半旧」的数据,数据的一致性是可以保证的。
重要的是,RCU中的reader不用像rwlock中的reader那样,在writer操作期间必须spin等待了。

RCU的全称是"read copy update",可以这样来理解:read和进行copy的线程并行,目的是为了update。好像有点"copy on write"的意思?反正有人觉得RCU的命名不够准确,宁愿叫它"publish protocol"(比如 Fedor Pikus)。不管怎样,RCU的命名已经成了业界默认的,我们还是就叫它RCU吧。
那RCU具体应该如何使用呢?这得走进真正的代码,才能一探究竟。2.使用方法前面的例子为了简化,采用的是单向链表来演示,这里我们切换到Linux中常用的双向链表上来。由于RCU可理解为是基于rwlock演进而来的,所以笔者将结合上文讲解的rwlock的用法,来对比讨论RCU的使用。
假设现在reader正在遍历/查询一个链表,而writer正在删除该链表中的一个节点。那么,使用rwlock(左)和RCU(右)来实现的读取一侧的代码分别是这样的:

同rwlock类似,rcu_read_lock()和rcu_read_unlock()界定了RCU读取一侧的critical section。如果在内核配置时选择了"CONFIG_PREEMPT",那么这2个函数实际要做的工作仅仅是分别关闭和打开CPU的可抢占性而已,等同于preempt_disable()和preempt_enable()。
这种命名,体现了RCU和rwlock的「一脉相承」,但在RCU的读取一侧,其实并没有什么"lock",所以可能命名为rcu_enter()和rcu_exit()之类的更加贴切。
写入一侧的RCU实现中,为了防止多个writer对链表的同时操作,使用了一个标准的spinlock。

list_del_rcu()的实现和普通的list_del()基本一致,但多了一个对"prev"指针的"poison"处理,以避免接下来reader再通过该节点访问前向节点。
static inline void list_del_rcu(struct list_head *entry){
__list_del_entry(entry);
entry
->prev = LIST_POISON2;}
此外,在调用kfree()释放节点之前,多了一个synchronize_rcu()函数。synchronize就是「同步」,那它在和谁同步呢?
就是前面说的那些“引用旧数据区的reader”啦,因为此时它们可能还在引用指针p。这相当于给了这些reader一个优雅退出的宽限区,因此这段同步等待的时间被称为Grace Period(简称GP)。

不过,必须是在synchronize之前就已经进入critical section的reader才可以,至于之后的reader么,直接读新的数据就可以了,用不着writer来等待。
比如下面这个场景中,作为writer的CPU 1只会等待CPU 0,CPU 0离开critical section后就结束同步,而不会理会CPU 2。

也许,把synchronize_rcu()改名成wait_for_readers_to_leave()会更加直观。
等待与回调
如果grace period的时间比较长,writer这么干等着,岂不是会影响这个CPU上更高优先级的任务执行?在这种情况下,可以使用基于callback机制的call_rcu()来替换synchronize_rcu()。void call_rcu(struct rcu_head *head, rcu_callback_t func);call_rcu()会注册一个回调函数"func",当所有的reader都退出critical section后,该回调函数将被执行。
第一个参数的类型是struct rcu_head,它的定义是这样的:struct callback_head {
struct callback_head *next;
void (*func)(struct callback_head *head);} __attribute__((aligned(sizeof(void *))));
#define rcu_head callback_headCPU调用call_rcu()后就可以离开去做其他事情了,之后它完全可能再次调用call_rcu(),所以它每次注册的回调函数,需要通过"next"指针排队串接起来,等grace period结束后,依次执行。如果需要处理的回调函数比较多,可能需要分批进行
第二个参数就是前面讲的回调函数,其功能主要就是释放掉“旧指针”指向的内存空间。
来看一个使用call_rcu()的具体实例:call_rcu(
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
关闭
关闭