Linux内核里的同步机制众多,RCU(read-copy update)可以说是实现上比较复杂的一种了,比较适合用在读操作很多而写操作极少的情况,可以用来替代读写锁。RCU的实现本质上是一种trade-off,为了让读操作的开销尽可能的小,写操作的完成可以被推迟。
我们先通过几个内核中的例子来了解一下。
首先来看RCU在读操作方面是如何替代读锁的,观察下面的代码我们发现在使用上几乎没有什么差别,只是把read_lock()/read_unlock()换成了rcu_read_lock()/rcu_read_unlock()。
/* 读操作的例子 */
/* 读锁 */
static enum audit_state audit_filter_task(struct task_struct *tsk)
{
struct audit_entry *e;
enum audit_state state;
read_lock(&auditsc_lock);
/* Note: audit_netlink_sem held by caller. */
list_for_each_entry(e, &audit_tsklist, list) {
if (audit_filter_rules(tsk, &e->rule, NULL, &state)) {
read_unlock(&auditsc_lock);
return state;
}
}
read_unlock(&auditsc_lock);
return AUDIT_BUILD_CONTEXT;
}
/* RCU */
static enum audit_state audit_filter_task(struct task_struct *tsk)
{
struct audit_entry *e;
enum audit_state state;
rcu_read_lock();
/* Note: audit_netlink_sem held by caller. */
list_for_each_entry_rcu(e, &audit_tsklist, list) {
if (audit_filter_rules(tsk, &e->rule, NULL, &state)) {
rcu_read_unlock();
return state;
}
}
rcu_read_unlock();
return AUDIT_BUILD_CONTEXT;
}
在删除list元素的例子里我们看到了一点不同,在RCU的版本里并不需要从循环开始上锁,也就是说上RCU在写操作时是不会阻塞读操作的,当然读操作也不会被写操作阻塞。由于需要释放内存,调用了call_rcu()函数,这个函数的作用是推迟释放内存的时机,当所有已进入临界区的读操作完成之后再释放内存。
在插入list元素的例子中同样也不用从循环开始上锁,但是插入元素的函数换成了list_add_rcu(),如果我们跟踪这个函数的实现的话会发现list_add_rcu里使用了rcu_assign_pointer()这个函数,是用来更新指针指向的内容的,这里我们发现RCU主要是用来同步指针指向的数据,通常是通过kmalloc申请的内存。
/* 写操作的例子,删除和插入list元素 */
/* 写锁 */
static inline int audit_del_rule(struct audit_rule *rule,
struct list_head *list)
{
struct audit_entry *e;
write_lock(&auditsc_lock);
list_for_each_entry(e, list, list) {
if (!audit_compare_rule(rule, &e->rule)) {
list_del(&e->list);
write_unlock(&auditsc_lock);
return 0;
}
}
write_unlock(&auditsc_lock);
return -EFAULT; /* No matching rule */
}
static inline int audit_add_rule(struct audit_entry *entry,
struct list_head *list)
{
write_lock(&auditsc_lock);
if (entry->rule.flags & AUDIT_PREPEND) {
entry->rule.flags &= ~AUDIT_PREPEND;
list_add(&entry->list, list);
} else {
list_add_tail(&entry->list, list);
}
write_unlock(&auditsc_lock);
return 0;
}
/* RCU */
static inline int audit_del_rule(struct audit_rule *rule,
struct list_head *list)
{
struct audit_entry *e;
/* Do not use the _rcu iterator here, since this is the only
* deletion routine. */
list_for_each_entry(e, list, list) {
if (!audit_compare_rule(rule, &e->rule)) {
list_del_rcu(&e->list);
call_rcu(&e->rcu, audit_free_rule);
return 0;
}
}
return -EFAULT; /* No matching rule */
}
static inline int audit_add_rule(struct audit_entry *entry,
struct list_head *list)
{
if (entry->rule.flags & AUDIT_PREPEND) {
entry->rule.flags &= ~AUDIT_PREPEND;
list_add_rcu(&entry->list, list);
} else {
list_add_tail_rcu(&entry->list, list);
}
return 0;
}
下面这个更新list元素的例子很好的体现了RCU read-copy update的涵义,在RCU版本的代码里首先为新的元素申请内存,并把旧元素的值copy到新元素,更新新元素的值之后,调用list_replace_rcu(),实际也是调用了rcu_assign_pointer()更新了元素,最后调用call_rcu()函数把旧元素的内存释放。
大家可能注意到,list_replace_rcu()函数之前发生的读操作读取的都是旧元素的值,在list_replace_rcu()函数之后如果有读操作则读到的是新元素的值,也就是说在list_replace_rcu()之前的读操作结束之前,实际上内存中存在新旧两个元素。
/* 写操作的例子,更新list元素的值 */
/* 写锁 */
static inline int audit_upd_rule(struct audit_rule *rule,
struct list_head *list,
__u32 newaction,
__u32 newfield_count)
{
struct audit_entry *e;
struct audit_newentry *ne;
write_lock(&auditsc_lock);
/* Note: audit_netlink_sem held by caller. */
list_for_each_entry(e, list, list) {
if (!audit_compare_rule(rule, &e->rule)) {
e->rule.action = newaction;
e->rule.file_count = newfield_count;
write_unlock(&auditsc_lock);
return 0;
}
}
write_unlock(&auditsc_lock);
return -EFAULT; /* No matching rule */
}
/* RCU */
static inline int audit_upd_rule(struct audit_rule *rule,
struct list_head *list,
__u32 newaction,
__u32 newfield_count)
{
struct audit_entry *e;
struct audit_newentry *ne;
list_for_each_entry(e, list, list) {
if (!audit_compare_rule(rule, &e->rule)) {
ne = kmalloc(sizeof(*entry), GFP_ATOMIC);
if (ne == NULL)
return -ENOMEM;
audit_copy_rule(&ne->rule, &e->rule);
ne->rule.action = newaction;
ne->rule.file_count = newfield_count;
list_replace_rcu(&e->list, &ne->list);
call_rcu(&e->rcu, audit_free_rule);
return 0;
}
}
return -EFAULT; /* No matching rule */
}
通过上面几个例子我们对RCU有了大概的了解:
-
在读操作的代码段,RCU和读写锁几乎一样,但不会被写操作阻塞
-
RCU主要用于保证指针数据的同步
-
在写操作时,RCU分为两步,将指针指向新内存,推迟回收旧内存
-
由于在某一时间段内会同时存在新数据和旧数据,所以只适用于不严格要求数据同步的场景
进一步的,我们看一下RCU提供了哪些接口,以下是所有核心API:
-
上锁函数,rcu_read_lock/rcu_read_unlock
-
指针赋值和解引用,rcu_assign_pointer/rcu_dereference
-
异步或同步回收内存,call_rcu/synchronize_rcu
只有这么几个,足够的简洁,在代码示例中看到的list相关的接口也都是这几个API封装得到的。
这张图是对RCU的一个总结,里面的updater是写操作的一方,reader是读操作的一方,grace period(宽限期),也就是updater等待已经开始的reader退出临界区的时间,grace period的确定在RCU实现里是很重要的话题,我们下篇文章进一步讨论。