Linux中的RCU机制
时间:2021-11-12 14:00:15
手机看文章
扫描二维码
随时随地手机看文章
[导读]内容基本原理使用方法GP的生命周期QS的判定与标记优势何在存在的问题RCU机制是自内核2.5版本引入的(2002年10月),而后不断完善,其在Linux的locking机制中的使用占比也是逐年攀升。1.基本原理RCU的基本思想是这样的:先创建一个旧数据的copy,然后writer...
内容
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()会更加直观。
第一个参数的类型是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(
基本原理
使用方法
- GP 的生命周期
QS 的判定与标记
优势何在
存在的问题
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(