为了保护共享数据,需要一些同步机制,如自旋锁(spinlock),读写锁(rwlock),它们使用起来非常简单,而且是一种很有效的同步机制,在UNIX系统和Linux系统中得到了广泛的使用。但是随着计算机硬件的快速发展,获得这种锁的开销相对于CPU的速度在成倍地增加,原因很简单,CPU的速度与访问内存的速度差距越来越大,而这种锁使用了原子操作指令,它需要原子地访问内存,也就说获得锁的开销与访存速度相关,另外在大部分非x86架构上获取锁使用了内存栅(Memory Barrier),这会导致处理器流水线停滞或刷新,因此它的开销相对于CPU速度而言就越来越大。一些锁在多CPU情况下, 由于加锁的频度变高,性能反倒比一个CPU时性能差。正是在这种背景下,一个高性能的锁机制RCU呼之欲出,它克服了以上锁的缺点,具有很好的扩展性,但是这种锁机制的使用范围比较窄,它只适用于读多写少的情况,如网络路由表的查询更新、设备状态表的维护、数据结构的延迟释放以及多径I/O设备的维护等。
Read Copy Update
读(Read):读者不需要获得任何锁就可访问RCU保护的临界区;
拷贝(Copy):写者在访问临界区时,写者“自己”将先拷贝一个临界区副本,然后对副本进行修改;
更新(Update):RCU机制将在在适当时机使用一个回调函数把指向原来临界区的指针重新指向新的被修改的临界区,锁机制中的垃圾收集器负责回调函数的调用。(时机:所有引用该共享临界区的CPU都退出对临界区的操作。即没有CPU再去操作这段被RCU保护的临界区后,这段临界区即可回收了,此时回调函数即被调用)
quiescent state(静默状态过程),它表示为CPU发生上下文切换的过程
grace period(即“适当时机”),它表示为所有CPU都经历一次quiescent state所需要的等待的时间,也即系统中所有的读者完成对共享临界区的访问
RCU的结构体定义,只有一个用于串接链表的next指针和一个函数指针,这个函数指针即是上述提及的回调函数,这个需使用RCU机制的用户向链表注册,即挂接到链表下,从而在适当时机下得到调用
示例:写者从链表中删除元素B。
写者首先遍历该链表得到指向元素B的指针
然后修改元素B的前一个元素的next指针指向元素B的next指针指向的元素C,修改元素B的next指针指向的元素C的prep指针指向元素B的prep指针指向的元素A。在此期间可能有读者访问该链表,由于修改指针指向的操作是原子的,因此这个过程不需要同步,而元素B的指针并没有去修改,因为读者可能正在使用B元素来得到链表的下一个或前一个元素,即A或C。当写者完成上述操作后便向系统注册一个回调函数func以便在 grace period之后能够删除元素B,注册完毕后写着便可认为它已经完成删除操作(实际上并未完成)。
垃圾收集器在检测到所有的CPU不在引用该链表后,即所有的CPU已经经历了一次quiescent state(即grace period),当grace period完成后,系统便会去调用先前写者注册的回调函数func,从而真正的删除了元素B。这便是RCU机制的一种使用范例。
#define rcu_read_lock() __rcu_read_lock()
#define rcu_read_unlock() __rcu_read_unlock()
#define __rcu_read_lock()
do {
preempt_disable();
__acquire(RCU);
rcu_read_acquire();
} while (0)
#define __rcu_read_unlock()
do {
rcu_read_release();
__release(RCU);
preempt_enable();
} while (0)
用来保持一个读者的RCU临界区.在该临界区内不允许发生上下文切换
#define rcu_dereference(p) rcu_dereference_check(p, 0)
#define rcu_dereference_check(p, c)
__rcu_dereference_check((p), rcu_read_lock_held() || (c), __rcu)
#define __rcu_dereference_check(p, c, space)
({
typeof(*p) *_________p1 = (typeof(*p)*__force )ACCESS_ONCE(p);
rcu_lockdep_assert(c, "suspicious rcu_dereference_check()"
" usage");
rcu_dereference_sparse(p, space);
smp_read_barrier_depends();
((typeof(*p) __force __kernel *)(_________p1));
})
该宏用于在RCU读端临界区获得一个RCU保护的指针,该指针可以在以后安全地引用,内存栅只在alpha架构上才使用
#define rcu_assign_pointer(p, v)
__rcu_assign_pointer((p), (v), __rcu)
#define __rcu_assign_pointer(p, v, space)
do {
smp_wmb();
(p) = (typeof(*v) __force space *)(v);
} while (0)
写者使用该函数来为被RCU保护的指针分配一个新的值.这样是为了安全从写者到读者更改其值.这个函数会返回一个新值
void synchronize_rcu(void)
{
struct rcu_synchronize rcu;
init_completion(&rcu.completion);
/* Will wake me after RCU finished */
call_rcu(&rcu.head, wakeme_after_rcu);
/* Wait for it */
wait_for_completion(&rcu.completion);
}
static void wakeme_after_rcu(struct rcu_head *head)
{
struct rcu_synchronize *rcu;
rcu = container_of(head, struct rcu_synchronize, head);
complete(&rcu->completion);
}
在RCU中是一个最核心的函数,写者用来等待之前的读者全部退出。该函数由RCU写端调用,它将阻塞写者,直到经过grace period后,即所有的读者已经完成读端临界区,写者才可以继续下一步操作。如果有多个RCU写端调用该函数,他们将在一个grace period之后全部被唤醒。
void call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *rcu))
{
unsigned long flags;
struct rcu_data *rdp;
head->func = func;
head->next = NULL;
local_irq_save(flags);
rdp = &__get_cpu_var(rcu_data);
*rdp->nxttail = head;
rdp->nxttail = &head->next;
if (unlikely(++rdp->qlen > qhimark)) {
rdp->blimit = INT_MAX;
force_quiescent_state(rdp, &rcu_ctrlblk);
}
local_irq_restore(flags);
}
call_rcu()用来等待之前的读者操作完成之后,就会调用函数func,用在不可睡眠的条件中,如中断上下文。而synchronize_rcu()用在可睡眠的环境下。
static inline void list_add_rcu(struct list_head *new, struct list_head *head)
该函数把链表项new插入到RCU保护的链表head的开头。使用内存栅保证了在引用这个新插入的链表项之前,新链表项的链接指针的修改对所有读者是可见的。
static inline void list_add_tail_rcu(struct list_head *new, struct list_head *head)
该函数类似于list_add_rcu,它将把新的链表项new添加到被RCU保护的链表的末尾。
static inline void list_del_rcu(struct list_head *entry)
该函数从RCU保护的链表中移走指定的链表项entry,并且把entry的prev指针设置为LIST_POISON2,但是并没有把entry的next指针设置为LIST_POISON1,因为该指针可能仍然在被读者用于遍历该链表。
static inline void list_replace_rcu(struct list_head *old, struct list_head *new)
该函数是RCU新添加的函数,并不存在非RCU版本。它使用新的链表项new取代旧的链表项old,内存栅保证在引用新的链表项之前,它的链接指针的修正对所有读者可见。
list_for_each_rcu(pos, head)
该宏用于遍历由RCU保护的链表head,只要在读端临界区使用该函数,它就可以安全地和其它_rcu链表操作函数(如list_add_rcu)并发运行。
list_for_each_safe_rcu(pos, n, head)
该宏类似于list_for_each_rcu,但不同之处在于它允许安全地删除当前链表项pos。
list_for_each_entry_rcu(pos, head, member)
该宏类似于list_for_each_rcu,不同之处在于它用于遍历指定类型的数据结构链表,当前链表项pos为一包含struct list_head结构的特定的数据结构。
list_for_each_continue_rcu(pos, head)
该宏用于在退出点之后继续遍历由RCU保护的链表head。
static inline void hlist_del_rcu(struct hlist_node *n)
它从由RCU保护的哈希链表中移走链表项n,并设置n的ppre指针为LIST_POISON2,但并没有设置next为LIST_POISON1,因为该指针可能被读者使用用于遍利链表。
static inline void hlist_add_head_rcu(struct hlist_node *n, struct hlist_head *h)
该函数用于把链表项n插入到被RCU保护的哈希链表的开头,但同时允许读者对该哈希链表的遍历。内存栅确保在引用新链表项之前,它的指针修正对所有读者可见。
hlist_for_each_rcu(pos, head)
该宏用于遍历由RCU保护的哈希链表head,只要在读端临界区使用该函数,它就可以安全地和其它_rcu哈希链表操作函数(如hlist_add_rcu)并发运行。
hlist_for_each_entry_rcu(tpos, pos, head, member)
类似于hlist_for_each_rcu,不同之处在于它用于遍历指定类型的数据结构哈希链表,当前链表项pos为一包含struct list_head结构的特定的数据结构。
struct foo {
int a;
char b;
long c;
};
DEFINE_SPINLOCK(foo_mutex);
struct foo *gbl_foo;
void foo_update_a(int new_a)
{
struct foo *new_fp;
struct foo *old_fp;
new_fp = kmalloc(sizeof(*new_fp), GFP_KERNEL);
spin_lock(&foo_mutex);
old_fp = gbl_foo;
*new_fp = *old_fp;
new_fp->a = new_a;
rcu_assign_pointer(gbl_foo, new_fp);
// gbl_foo = new_fp;
spin_unlock(&foo_mutex);
synchronize_rcu();
kfree(old_fp);
}
int foo_get_a(void)
{
int retval;
foo *fp;
rcu_read_lock();
//fp = gbl_foo;
fp = rcu_dereference(gbl_foo);
retval = fp->a;
rcu_read_unlock();
return retval;
}
如上代码所示,RCU被用来保护全局指针struct foo *gbl_foo。foo_get_a()用来从RCU保护的结构中取得gbl_foo的值。而foo_update_a()用来更新被RCU保护的gbl_foo的值(更新其a成员)。
为什么要在foo_update_a()中使用自旋锁foo_mutex呢? 假设中间没有使用自旋锁.那foo_update_a()的代码如下:
void foo_update_a(int new_a)
{
struct foo *new_fp;
struct foo *old_fp;
new_fp = kmalloc(sizeof(*new_fp), GFP_KERNEL);
old_fp = gbl_foo;
1:-------------------------
*new_fp = *old_fp;
new_fp->a = new_a;
rcu_assign_pointer(gbl_foo, new_fp);
synchronize_rcu();
kfree(old_fp);
}
假设A进程在上图1:----标识处被B进程抢点.B进程也执行了foo_update_a().等B执行完后,再切换回A进程.此时,A进程所持的old_fd实际上已经被B进程给释放掉了.此后A进程对old_fd的操作都是非法的。所以在此我们得到一个重要结论:RCU允许多个读者同时访问被保护的数据,也允许多个读者在有写者时访问被保护的数据(但是注意:是否可以有多个写者并行访问取决于写者之间使用的同步机制)。
struct foo {
int a;
char b;
long c;
};
DEFINE_SPINLOCK(foo_mutex);
struct foo *gbl_foo;
void foo_read (void)
{
foo *fp = gbl_foo;
if ( fp != NULL )
dosomething(fp->a, fp->b , fp->c );
}
void foo_update( foo* new_fp )
{
spin_lock(&foo_mutex);
foo *old_fp = gbl_foo;
gbl_foo = new_fp;
spin_unlock(&foo_mutex);
kfee(old_fp);
}
可以通过在24行kfree函数之前插入synchronize_rcu函数,执行删除操作后,先进入grace period。下图中每行代表一个线程,最下面的一行是foo_update删除线程,当它执行完删除操作后,线程进入了宽限期。宽限期的意义是,在一个删除动作发生后,它必须等待所有在宽限期开始前已经开始的读线程结束,才可以进行销毁操作。这样做的原因是这些线程有可能读到了要删除的元素。图中的宽限期必须等待1和2结束;而读线程5在宽限期开始前已经结束,不需要考虑;而3,4,6也不需要考虑,因为在宽限期开始后的线程不可能读到已删除的元素
void foo_read(void)
{
rcu_read_lock();
foo *fp = gbl_foo;
if ( fp != NULL )
dosomething(fp->a,fp->b,fp->c);
rcu_read_unlock();
}
void foo_update( foo* new_fp )
{
spin_lock(&foo_mutex);
foo *old_fp = gbl_foo;
gbl_foo = new_fp;
spin_unlock(&foo_mutex);
synchronize_rcu();
kfee(old_fp);
}
void foo_update( foo* new_fp )
{
spin_lock(&foo_mutex);
foo *old_fp = gbl_foo;
new_fp->a = 1;
new_fp->b = ‘b’;
new_fp->c = 100;
gbl_foo = new_fp;
spin_unlock(&foo_mutex);
synchronize_rcu();
kfee(old_fp);
}
void foo_read(void)
{
rcu_read_lock();
foo *fp = gbl_foo;
if ( fp != NULL )
dosomething(fp->a, fp->b ,fp->c);
rcu_read_unlock();
}
适合用于同步基于指针实现的数据结构(例如链表,哈希表等)。因为指针赋值是一条单指令.也就是说是一个原子操作. 因它更改指针指向没必要考虑它的同步.只需要考虑cache的影响
适用用读操作远远大与写操作的场景。RCU实际上是一种改进的rwlock,读者几乎没有什么同步开销,它不需要锁,不使用原子指令,而且在除alpha的所有架构上也不需要内存栅(Memory Barrier),因此不会导致锁竞争,内存延迟以及流水线停滞。不需要锁也使得使用更容易,因为死锁问题就不需要考虑了。写者的同步开销比较大,它需要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其它写者的修改操作。
https://www.ibm.com/developerworks/cn/linux/l-rcu/index.html
https://www.cnblogs.com/wuchanming/p/3816103.html
http://abcdxyzk.github.io/blog/2015/07/31/kernel-sched-rcu/
http://blog.jobbole.com/106856/
https://blog.csdn.net/junguo/article/details/8244530#