rcu入门
是什么
-
可参考官方文档
-
Read-copy update,可以理解为,先读数据,修改之后,一次性替换旧数据
-
是linux内核的同步机制,提供线程安全的并发访问
应用场景
- 典型应用场景
- 链表
- 读多写少
实现
链表插入节点
- 在A之前插入节点,分为3步
-
1.new 新节点
-
2.新节点next指针指向A
-
3.前置节点的next指针,指向新节点
分析
- 在2步之后时,所有遍历链表操作正常
- 在3步之后,所有遍历链表操作也均正常
- 因为改变指针是原子的,所以不会有问题
链表删除节点
- 删除B节点,分为3步
- A节点的next指针,指向C
- 等待宽限期过去
- delete B节点
分析
- 1步之后,访问到A节点的线程,可以继续访问后续C节点;访问到B节点的线程,可以继续访问C节点
- 2步,宽限期的含义是,等待所有访问B节点的线程,释放对B节点的访问
- 3步,当没有线程访问B节点时,可以删除B节点
宽限期详解
- 写需要加锁
- 读不需要加锁
参考下面的例子
- 两个线程,一个读,一个写
- 假设,当读线程获取gbl_foo之后,线程切换
- 此时写线程更新gbl_foo,销毁旧gbl_foo
- 之后,读线程切换回来,发现gbl_foo已经是空指针
1 struct foo{
2 int a;
3 char b;
4 long c;
5 };
6
7 DEFINE_SPINLOCK(foo_mutex);
8
9 void foo_read(void)
10 {
11 foo *fp = gbl_foo;
12 if( fp != NULL )
13 {
14 dosomthing(fp->a, fp->b, fp->c);
15 }
16 }
17
18 void foo_update(foo * new_fp)
19 {
20 spin_lock(&foo_mutex);
21 foo *old_fp = gbl_foo;
22 gbl_foo = new_fp;
23 spin_unlock(&foo_mutex);
24 }
宽限期,即用来解决该问题
-
写线程更新完gbl_foo之后,调用synchronize_rcu(),阻塞等待至宽限期结束,之后再释放旧指针
-
读线程加rcu_read_lock()和 rcu_read_unlock(),并不实际加锁,而是进入宽限期
-
synchronize_rcu() 函数需要等待,在此之前所有调用rcu_read_lock() 函数的线程,进行rcu_read_unlock()
这意味着,所有可能持有旧指针的线程不在使用旧指针
-
实现上设计复杂的状态机等原理
-
简单可理解为,rcu_read_lock() 和 rcu_read_unlock()之间不允许线程切换,当synchronize_rcu()之后,CPU都进行至少一次线程切换即可认为宽限期已过
1 void foo_read(void)
2 {
3 rcu_read_lock();
4 foo *fp = gbl_foo;
5 if( fp != NULL )
6 dosomthing(fp->a, fp->b, fp->c);
7 rcu_read_unlock();
8 }
9
10 void foo_update(foo *new_fp)
11 {
12 spin_lock(&foo_mutex);
13 foo *old_fp = gbl_foo;
14 gbl_foo = new_fp;
15 spin_unlock(&foo_mutex);
16 synchronize_rcu();
17 kfree(old_fp);
18 }