08、Read-Copy Update(RCU)
一、RCU的概念
Linux内核已经有很多锁机制(比如自旋锁、读写锁、互斥锁),但为什么还要设计更复杂的RCU呢?原因有以下几点:
1.1. 现有锁机制的性能问题
- 原子操作的代价:自旋锁、信号量等锁机制都依赖CPU的原子指令。当多个CPU同时竞争同一个共享变量时,所有CPU的缓存必须频繁同步,就像多个厨师同时修改同一本菜谱,厨房里的所有人都得停下来看菜谱更新——这会严重拖慢系统。
- 读者和写者冲突:比如读写信号量虽然允许多个读者同时读,但一旦有写者要修改数据,所有读者必须立刻被阻塞。如果写操作频繁,写者可能一直等待,导致整体效率低下。
1.2. RCU的设计目标
RCU的目标是:
- 让读者“零负担”:读者访问数据时,不需要任何锁、不需要原子指令、不需要等待(就像直接看一本公开的菜谱,没人拦着你)。
- 写者承担所有同步成本:写者修改数据时,先复制一份数据再修改,等所有当前读者都离开后,再切换指针指向新数据,并删除旧数据(就像悄悄准备新菜谱,等所有厨师都放下旧菜谱后,再把旧菜谱扔掉)。
1.3. RCU的工作原理
写者要修改数据时:
- 先复制一份数据,修改新副本(比如把旧菜谱复印一份,再改新复印件)。
- 等所有正在读旧数据的读者都完成操作后(比如等所有厨师都放下旧菜谱),再把全局指针指向新数据,并删除旧数据。
读者访问数据时:
- 直接读当前指向的数据,完全不需要等待或加锁(就像直接拿菜谱看,没人拦你)。
1.4. RCU的局限性
- 写者竞争时仍需锁:如果多个写者同时想修改数据,RCU本身无法解决谁先谁后的问题,这时候可能需要额外的锁(比如互斥锁)来协调。
- 需要“等待读者完成”:写者需要等待所有读者离开,这依赖于内核的机制(比如延时处理或跟踪读者状态),可能有一定延迟。
总结
RCU的精髓是:把同步的“麻烦”全部交给写者,让读者畅通无阻。这对“读多写少”的场景(比如系统配置、路由表等)特别高效,但需要权衡写操作的延迟问题。
二、RCU提供的接口
- rcu_read_lock() / rcu_read_unlock() 这两个函数标记一个"安全区域"。读者线程在访问共享数据时,必须先调用rcu_read_lock()进入区域,结束后调用rcu_read_unlock()离开。这能保证在读取期间,数据不会被其他线程突然删除。
- rcu_dereference() 这是一个保护指针的工具。当读者线程需要访问被RCU保护的共享数据时,必须用这个函数获取指针。它会确保你拿到的指针是有效的,不会因为其他线程的修改而突然失效。
- rcu_assign_pointer() 写者线程修改数据时使用这个函数。当新数据准备就绪后,用它把共享指针指向新数据。这会让其他线程能看到更新后的内容,但旧数据暂时不会被立刻删除。
- synchronize_rcu() 写者线程调用这个函数后,会一直等待,直到所有正在执行的读者线程都结束对旧数据的访问。等待完成后,旧数据就可以安全删除了。
- call_rcu() 这个函数用来注册一个"清理任务"。当所有读者线程完成当前访问后,系统会自动执行这个任务(比如释放旧数据占用的内存)。写者线程不需要等待,可以立即继续执行其他操作。
三、RCU案例
RCU(读拷贝更新)在链表中的核心作用是:让写操作(比如删除节点)不立即回收节点内存,而是等所有正在读的线程都安全退出后,再释放节点。这样就能保证读写操作同时进行时,数据访问不会出错。
具体过程:
- 读线程的简单操作 读线程只需要执行
rcu_read_lock()
进入读临界区,然后遍历链表。这个过程中不需要任何锁,所以多个读线程可以同时读取链表,效率很高。 - 写线程修改链表 当写线程要删除一个节点时,它只是把节点从链表中移除(比如断开指针),但不会立刻释放节点内存。此时节点仍然留在内存中,只是不再被链表直接引用。
- 等待所有读线程完成 RCU 会跟踪所有正在读的线程。当所有读线程都退出了它们的
rcu_read_lock()
临界区后,RCU 才会通知系统安全回收被删除的节点。
C
<关于RCU的一个简单例子>
0 #include <linux/kernel.h>
1 #include <linux/module.h>
2 #include <linux/init.h>
3 #include <linux/slab.h>
4 #include <linux/spinlock.h>
5 #include <linux/rcupdate.h>
6 #include <linux/kthread.h>
7 #include <linux/delay.h>
8
9 struct foo {
10 int a;
11 struct rcu_head rcu;
12 };
13
14 static struct foo *g_ptr;
15 static void myrcu_reader_thread(void *data) //读者线程
16 {
17 struct foo *p = NULL;
18
19 while (1) {
20 msleep(200);
21 rcu_read_lock();
22 p = rcu_dereference(g_ptr);
23 if (p)
24 printk("%s: read a=%d\n", __func__, p->a);
25 rcu_read_unlock();
26 }
27 }
28
29 static void myrcu_del(struct rcu_head *rh)
30 {
31 struct foo *p = container_of(rh, struct foo, rcu);
32 printk("%s: a=%d\n", __func__, p->a);
33 kfree(p);
34 }
35
36 static void myrcu_writer_thread(void *p) //写者线程
37 {
38 struct foo *new;
39 struct foo *old;
40 int value = (unsigned long)p;
41
42 while (1) {
43 msleep(400);
44 struct foo *new_ptr = kmalloc(sizeof (struct foo), GFP_KERNEL);
45 old = g_ptr;
46 printk("%s: write to new %d\n", __func__, value);
47 *new_ptr = *old;
48 new_ptr->a = value;
49 rcu_assign_pointer(g_ptr, new_ptr);
50 call_rcu(&old->rcu, myrcu_del);
51 value++;
52 }
53 }
54
55 static int __init my_test_init(void)
56 {
57 struct task_struct *reader_thread;
58 struct task_struct *writer_thread;
59 int value = 5;
60
61 printk("BEN: my module init\n");
62 g_ptr = kzalloc(sizeof (struct foo), GFP_KERNEL);
63
64 reader_thread = kthread_run(myrcu_reader_thread, NULL, "rcu_reader");
65 writer_thread = kthread_run(myrcu_writer_thread, (void *)(unsigned long)value,
"rcu_writer");
66
67 return 0;
68 }
69 static void __exit my_test_exit(void)
70 {
71 printk("goodbye\n");
72 if (g_ptr)
73 kfree(g_ptr);
74 }
75 MODULE_LICENSE("GPL");
76 module_init(my_test_init);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
该例子的目的是通过RCU机制保护my_test_init()函数分配的共享数据结构g_ptr,并创建一个读者线程和一个写者线程来模拟同步场景。