1. 什么是RCU
RCU(Read-Copy-Update),是一种同步机制,它虽然对内存有一定的开销,但是它的性能非常好。在Linux内核中随处可见它的身影。
2. RCU的工作机制
RCU下,会记录指向共享结构体指针的所用使用者。这个结构体将要被修改时,首相将会创建一个副本,然后把改动写入副本当中。当所有的读操作使用者访问结束之后,指针指向新的修改后副本的指针,副本就这么上位了,修改也就是最新的了。基本原理是很简单的,一个备胎而已,空闲时上位而已。要是支持多任务读写并发,就复杂些了。
3. 适用场景
RCU不是万能的,它也只是某些特定场景。如果同时的写入者有多个,那么RCU是不是需要多个副本?显然这种情况它的效率是很低的。RCU是有自己的约束的,大致有以下三条
- 受保护的资源必须通过指针访问
- RCU保护范围内的,内核不能进入休眠状态
- 对共享区的访问,应该是读远远大于写,或者说写很少的场景
4. 就这么简单吗?核心问题,怎么解决读写冲突的?
如下图,当“修改"线程进行操作时,读线程1和读线程2还未结束。这时候"删除"线程会进入宽限期,这是非常重要的一个概念。宽限期截止到最后一个读线程结束,之后"修改"线程完成事实更新。宽限期内开始的线程3,4,6 也不会读到修改后的数据,它读的是副本的数据。
5. RCU的核心API
5.1 读操作相关
首先ptr指向被共享区,p必须通过rcu_dereference才能反引用返回的结果,并且必须在rcu_read_lock和rcu_read_unlock之间做非写访问,否则不被保护。
rcu_read_lock();
p = rcu_dereference(ptr);
if(p){
awesome_function(p);
}
rcu_read_unlock();
5.2 修改相关操作
rcu_assign_pointer 公布这个指针之后,后续的操作(比如线程三四六)将看到新的结构,而不是原来的。在所有读访问结束之后,内核释放旧实例内存。如果想在所有读结束之后,访问共享资源,可以调用synchronize_rcu,它将阻塞直到所有读完成。
struct super_duper *new_ptr = kmalloc(...)
new_ptr->data = 0xA5A5;
rcu_assign_pointer(ptr, new_ptr);
6. 在内核链表当中的应用
RCU保护的链表,只有在遍历、修改删除元素时,调用RCU的变体,如上图也就是标准函数后加_rcu后缀。
static inline void list_add_rcu(struct list_head *new,struct list_head *head);
static inline void list_add_tail_rcu(struct list_head *new,struct list_head *head);
static inline void list_del_rcu(struct list_head *entry);
static inline void list_replace_rcu(struct list_head *old,struct list_head *new);
增加元素
#define list_next_rcu(list) (*((struct list_head __rcu **)(&(list)->next)))
static inline void __list_add_rcu(struct list_head *new,
struct list_head *prev, struct list_head *next)
{
new->next = next;
new->prev = prev;
rcu_assign_pointer(list_next_rcu(prev), new);
next->prev = new;
}
访问链表要用以下方式
rcu_read_lock();
list_for_each_entry_rcu(pos, head, member) {
// do something with `pos`
}
rcu_read_unlock();
参考:
[0] 深入Linux内核架构
[1] https://blog.csdn.net/junguo/article/details/8244530