在Linux内核中,虽然已经有多种互斥机制(如<font style="color:rgb(64, 64, 64);">mutex</font>
、<font style="color:rgb(64, 64, 64);">spinlock</font>
、原子操作、<font style="color:rgb(64, 64, 64);">rwlock</font>
等),但这些机制在某些场景下可能会成为性能瓶颈。RCU(Read-Copy-Update) 是一种特殊的同步机制,主要用于解决读多写少的场景下的性能问题。RCU对于读多写少场景,性能比<font style="color:rgb(64, 64, 64);">rwsem</font>
、<font style="color:rgb(64, 64, 64);">rwlock</font>
等都要高。它的设计目标是最大限度地减少读操作的开销,同时保证数据的一致性。
为什么需要RCU?
传统互斥机制的问题
- 锁的开销:传统的锁机制(如
<font style="color:rgb(64, 64, 64);">mutex</font>
、<font style="color:rgb(64, 64, 64);">spinlock</font>
)在读写冲突时会导致性能下降,尤其是在读多写少的场景中。- 读操作需要加锁,即使没有写操作,也会引入额外的开销。
- 写操作需要阻塞所有读操作,导致读操作的延迟增加。
- 即使是读写锁,读者也是有开销的。
- 扩展性问题:在高并发场景下,锁的争用会显著降低系统的可扩展性。
RCU的优势
- 无锁读操作:RCU允许读操作不加锁,因此读操作的开销非常低。
- 写操作延迟更新:写操作通过复制和延迟更新的方式,避免阻塞读操作。
- 高并发性:RCU特别适合读多写少的场景,能够显著提高系统的并发性能。
RCU的原理
RCU(Read-Copy-Update),读-复制-更新,它是根据原理命名的。写者修改对象的过程是:首先复制生成一个副本,然后更新这个副本,最后使用新的对象替换旧的对象。在写者执行复制更新的时候读者可以读数据。
它的核心思想是通过延迟回收和无锁读来实现高效的并发访问。其基本原理如下:
读操作
- 读操作不需要加锁,直接访问共享数据。
- 读操作可以并发执行,不会被写操作阻塞。
写操作
- 写操作首先创建一个数据的副本,并在副本上进行修改。
- 修改完成后,通过原子操作将指针指向新的副本,替换旧数据。
- 旧的数据不会立即删除,而是等待所有正在使用旧数据的读操作完成后,再回收旧数据。
- 如果有多个写者,多个写者直接需要使用互斥机制。
延迟回收
- RCU通过宽限期(Grace Period)来确保所有读操作完成后再回收旧数据。
- 写者等待所有读者访问结束的时间称为宽限期(Grace Period)
- 宽限期的管理由RCU机制自动完成,开发者无需关心。
RCU的关键技术是怎么判断所有读者已经完成访问,机制比较复杂(使用per-cpu等),此处不进行展开。
RCU的约束
RCU对使用者的一些约束:
- 对共享资源的访问在大部分时间应该是只读的,写访问应该相对很少。
- 在RCU保护的代码范围内,内核不能进入睡眠状态。
- 受保护资源必须通过指针访问。
RCU的API
Linux内核提供了丰富的RCU API,以下是一些常用的函数:
读操作
<font style="color:rgb(64, 64, 64);">rcu_read_lock()</font>
:标记读操作的开始。<font style="color:rgb(64, 64, 64);">rcu_read_unlock()</font>
:标记读操作的结束。
// 读者访问受保护数据
rcu_read_lock();
p = rcu_dereference(gp);
// 访问*p
rcu_read_unlock();
写操作
<font style="color:rgb(64, 64, 64);">rcu_assign_pointer()</font>
:更新指针,指向新的数据。<font style="color:rgb(64, 64, 64);">synchronize_rcu()</font>
:阻塞等待宽限期结束,确保所有读操作完成,之后释放旧数据。<font style="color:rgb(64, 64, 64);">call_rcu()</font>
:非阻塞方式,注册回调,宽限期结束后回调释放旧数据。
// 写者更新数据
old = gp;
new = kmalloc(...);
rcu_assign_pointer(gp, new);
synchronize_rcu(); // 或者使用call_rcu的方式
kfree(old);
测试代码
使用synchronize_rcu
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/rcupdate.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/kthread.h>
#include <linux/err.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("congchp");
MODULE_DESCRIPTION("RCU Test Module with synchronize_rcu()");
// 受RCU保护的数据结构
struct rcu_test_data
{
int value;
};
static struct rcu_test_data __rcu *global_data;
static struct task_struct *reader_thread;
static struct task_struct *writer_thread;
static int stop_threads;
// 读者线程函数
static int rcu_reader_thread(void *arg)
{
struct rcu_test_data *data;
int counter = 0;
while (!kthread_should_stop() && !stop_threads)
{
rcu_read_lock();
data = rcu_dereference(global_data);
if (data)
printk(KERN_INFO "Reader[%d]: read value = %d\n", counter, data->value);
else
printk(KERN_INFO "Reader[%d]: data is NULL\n", counter);
rcu_read_unlock();
msleep(500); // 每500ms读取一次
counter++;
}
return 0;
}
// 写者线程函数
static int rcu_writer_thread(void *arg)
{
struct rcu_test_data *new_data, *old_data;
int value = 0;
while (!kthread_should_stop() && !stop_threads)
{
// 创建新数据
new_data = kmalloc(sizeof(*new_data), GFP_KERNEL);
if (!new_data)
{
printk(KERN_ERR "Failed to allocate new data\n");
return -ENOMEM;
}
new_data->value = value++;
// 替换全局指针
old_data = rcu_dereference_protected(global_data,
lockdep_is_held(&global_data));
rcu_assign_pointer(global_data, new_data);
// 使用synchronize_rcu等待所有读者完成
printk(KERN_INFO "Writer: updating value to %d (waiting for readers...)\n", new_data->value);
synchronize_rcu();
printk(KERN_INFO "Writer: grace period passed, safe to free old data\n");
// 释放旧数据
if (old_data)
{
printk(KERN_INFO "Writer: freeing old data (value=%d)\n", old_data->value);
kfree(old_data);
}
msleep(2000); // 每2秒更新一次
}
return 0;
}
// 模块初始化
static int __init rcu_test_init(void)
{
printk(KERN_INFO "RCU Test Module loaded\n");
// 初始化全局数据
global_data = kmalloc(sizeof(*global_data), GFP_KERNEL);
if (!global_data)
return -ENOMEM;
global_data->value = 0;
rcu_assign_pointer(global_data, global_data);
// 创建读者线程
reader_thread = kthread_run(rcu_reader_thread, NULL, "rcu_reader");
if (IS_ERR(reader_thread))
{
printk(KERN_ERR "Failed to create reader thread\n");
kfree(global_data);
return PTR_ERR(reader_thread);
}
// 创建写者线程
writer_thread = kthread_run(rcu_writer_thread, NULL, "rcu_writer");
if (IS_ERR(writer_thread))
{
printk(KERN_ERR "Failed to create writer thread\n");
kthread_stop(reader_thread);
kfree(global_data);
return PTR_ERR(writer_thread);
}
return 0;
}
// 模块退出
static void __exit rcu_test_exit(void)
{
struct rcu_test_data *data;
printk(KERN_INFO "Stopping RCU test module\n");
stop_threads = 1;
// 停止线程
if (reader_thread)
kthread_stop(reader_thread);
if (writer_thread)
kthread_stop(writer_thread);
// 清理全局数据
data = rcu_dereference_protected(global_data, 1);
if (data)
{
rcu_assign_pointer(global_data, NULL);
synchronize_rcu();
kfree(data);
}
printk(KERN_INFO "RCU Test Module unloaded\n");
}
module_init(rcu_test_init);
module_exit(rcu_test_exit);
使用call_rcu
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/rcupdate.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/kthread.h>
#include <linux/sched.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("congchp");
MODULE_DESCRIPTION("Basic RCU example using call_rcu");
struct my_data
{
int value;
struct rcu_head rcu;
};
static struct my_data __rcu *global_ptr = NULL;
static struct task_struct *reader_task;
static struct task_struct *writer_task;
static void rcu_free_callback(struct rcu_head *rcu)
{
struct my_data *old = container_of(rcu, struct my_data, rcu);
pr_info("RCU callback: freeing value = %d\n", old->value);
kfree(old);
}
static int reader_fn(void *data)
{
while (!kthread_should_stop())
{
struct my_data *p;
rcu_read_lock();
p = rcu_dereference(global_ptr);
if (p)
pr_info("Reader: value = %d\n", p->value);
rcu_read_unlock();
msleep(500);
}
return 0;
}
static int writer_fn(void *data)
{
int val = 100;
while (!kthread_should_stop())
{
struct my_data *new = kmalloc(sizeof(*new), GFP_KERNEL);
struct my_data *old;
if (!new)
continue;
new->value = val++;
old = rcu_dereference(global_ptr);
rcu_assign_pointer(global_ptr, new);
if (old)
call_rcu(&old->rcu, rcu_free_callback);
pr_info("Writer: updated value to %d\n", new->value);
msleep(2000);
}
return 0;
}
static int __init rcu_demo_init(void)
{
pr_info("RCU demo module loaded\n");
writer_task = kthread_run(writer_fn, NULL, "rcu_writer");
reader_task = kthread_run(reader_fn, NULL, "rcu_reader");
return 0;
}
static void __exit rcu_demo_exit(void)
{
kthread_stop(writer_task);
kthread_stop(reader_task);
synchronize_rcu();
if (global_ptr)
{
struct my_data *p = rcu_dereference(global_ptr);
kfree(p);
}
pr_info("RCU demo module unloaded\n");
}
module_init(rcu_demo_init);
module_exit(rcu_demo_exit);
linux内核中RCU主要用户链表访问的保护,对于链表操作有一套专门的RCU方式的API。
用户空间可以使用RCU吗?
RCU最初是为内核空间设计的,但其思想也可以应用于用户空间。用户空间有一些库和工具可以实现类似内核RCU的功能:
用户空间RCU库(Userspace RCU, liburcu)
- l****iburcu 是一个开源的用户空间RCU实现,提供了与内核RCU类似的API。
- 它适用于高性能的用户空间应用程序,例如:
- 多线程服务器。
- 高性能数据库。
- 并发数据结构。
liburcu的API
<font style="color:rgb(64, 64, 64);">rcu_read_lock()</font>
和<font style="color:rgb(64, 64, 64);">rcu_read_unlock()</font>
:标记读操作的开始和结束。<font style="color:rgb(64, 64, 64);">rcu_assign_pointer()</font>
和<font style="color:rgb(64, 64, 64);">rcu_dereference()</font>
:更新和访问指针。<font style="color:rgb(64, 64, 64);">synchronize_rcu()</font>
:等待宽限期结束。
RCU与rwlock,rwsem比较
RCU,rwlock及rwsem,三者都是内核中用于读写并发控制的机制。 但它们各自的设计目标和使用场景有显著区别。
特性 | RCU | 读写锁(rwlock) | 读写信号量(rwsem) |
---|---|---|---|
读者开销 | 极低(无锁) | 中等(原子操作) | 较高(可能睡眠) |
写者开销 | 很高(宽限期等待) | 高(排他锁) | 高(排他锁+可能睡眠) |
阻塞行为 | 非阻塞(读者/写者) | 非阻塞(自旋等待) | 阻塞(可睡眠) |
内存占用 | 较高(延迟释放) | 低 | 低 |
优先级反转风险 | 无 | 有 | 有 |
扩展性 | 极佳(随CPU线性扩展) | 差(锁竞争) | 中等 |
适用最大读者数 | 理论上无限(无count) | 受计数器(count)限制 | 受计数器(count)限制 |
参考资料
- Professional Linux Kernel Architecture,Wolfgang Mauerer
- Linux内核深度解析,余华兵
- Linux设备驱动开发详解,宋宝华
- linux kernel 4.12