RCU(Read-Copy Update)

在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)限制

参考资料

  1. Professional Linux Kernel Architecture,Wolfgang Mauerer
  2. Linux内核深度解析,余华兵
  3. Linux设备驱动开发详解,宋宝华
  4. linux kernel 4.12
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值