Linux内核之RCU机制-分析实战篇

目录

1.源码概况:

1.1 RCU数据结构

struct rcu_head

2. 核心函数

2.1 读端(Reader)

2.1.1 rcu_read_lock() 和 rcu_read_unlock()

2.1.2 rcu_dereference()

2.2 写端(Updater)

2.2.1 rcu_assign_pointer()

2.2.2 synchronize_rcu()

2.3 RCU的核心实现

2.3.1 Grace Period 的检测

3.2.2 回调处理

3.2.2.1 工作流程

3.2.2.2 关键点

2.4. RCU的优化

2.4.1 Tree RCU

2.4.2 RCU加速

2.4 RCU栅栏

2.5 周期性检查

5. 总结

2.例示代码

3.Makefile


1.源码概况:

在Linux内核的源代码中,RCU的实现主要集中在include/linux/rcupdate.hkernel/rcupdate.c文件中。下面是一个简化的RCU机制分析:

1.1 RCU数据结构

struct rcu_head

这是RCU机制中最核心的数据结构,用于标记需要被RCU处理的对象。它包含一个call_entry字段,指向一个回调函数列表,当所有读取者结束时,这个列表中的函数将被执行。

struct rcu_head {
    union {
        struct callback_head callback;
        struct rcu_callback_list list;
    };
};

2. 核心函数

2.1 读端(Reader)

读端主要涉及以下几个关键函数:

2.1.1 rcu_read_lock() 和 rcu_read_unlock()

这两个函数用于标记RCU读临界区的开始和结束。在大多数架构上,它们是空操作,几乎没有开销。rcu_read_lock()读取线程在读取RCU保护的数据前,调用此函数。它并不真正锁定任何资源,而是标记线程为RCU读取者。rcu_read_unlock()读取线程在完成读取操作后,调用此函数。这使得更新线程能够检测到读取操作的完成。

#define rcu_read_lock() __rcu_read_lock()
#define rcu_read_unlock() __rcu_read_unlock()

static inline void __rcu_read_lock(void)
{
    preempt_disable();
}

static inline void __rcu_read_unlock(void)
{
    preempt_enable();
}

这里的 preempt_disable()preempt_enable() 用于禁止和启用内核抢占。

2.1.2 rcu_dereference()

用于安全地读取RCU保护的指针。

#define rcu_dereference(p) \
    __rcu_dereference_check((p), 0, __rcu)

#define __rcu_dereference_check(p, c, space) \
    ({ \
        typeof(*p) *_________p1 = (typeof(*p) *__force)READ_ONCE(p); \
        RCU_LOCKDEP_WARN(!(c), "suspicious rcu_dereference_check() usage"); \
        rcu_dereference_raw(p); \
    })

READ_ONCE() 确保只读取一次内存,防止编译器优化导致的重复读取。

2.2 写端(Updater)

写端主要涉及以下几个关键函数:

2.2.1 rcu_assign_pointer()

用于安全地更新RCU保护的指针。当更新线程需要替换某个由RCU保护的指针时,使用此函数。它并不立即更新指针,而是设置新的值并安排后续的处理。

#define rcu_assign_pointer(p, v) \
    __rcu_assign_pointer((p), (v), __rcu)

#define __rcu_assign_pointer(p, v, space) \
    do { \
        smp_wmb(); \
        (p) = (typeof(*v) __force space *)v; \
    } while (0)

smp_wmb() 是一个内存屏障,确保在更新指针之前,所有之前的写操作都已完成。

2.2.2 synchronize_rcu()

等待所有现有的RCU读临界区完成。更新线程在修改RCU保护的数据前,必须调用此函数。这个函数会检查是否处于RCU读临界区,然后根据情况选择不同的等待策略,确保新值可见于所有后续的读取者。

void synchronize_rcu(void)
{
    RCU_LOCKDEP_WARN(lock_is_held(&rcu_bh_lock_map) ||
             lock_is_held(&rcu_lock_map) ||
             lock_is_held(&rcu_sched_lock_map),
             "Illegal synchronize_rcu() in RCU read-side critical section");
    if (rcu_blocking_is_gp())
        return;
    if (rcu_gp_is_expedited())
        synchronize_rcu_expedited();
    else
        wait_rcu_gp(call_rcu);
}

2.3 RCU的核心实现

2.3.1 Grace Period 的检测

RCU通过检测所有CPU是否都经过了一个quiescent state来判断一个grace period是否结束。

static void rcu_process_callbacks(struct rcu_state *rsp)
{
    unsigned long flags;
    bool needwake;

    rcu_do_batch(rsp);
    needwake = rcu_prepare_for_idle(rsp);
    if (needwake)
        invoke_rcu_callbacks(rsp);
}

rcu_do_batch() 函数处理回调队列,rcu_prepare_for_idle() 检查是否所有CPU都进入了quiescent state。

3.2.2 回调处理

RCU使用回调函数来处理旧数据的释放。

  • call_rcu

    • 作用:注册一个回调函数,该函数将在未来的某个时间点(下一个 grace period 结束后)被调用。
    • 调用时机:通常在更新 RCU 保护的数据结构后立即调用。
    • 执行位置:在当前进程的上下文中执行。
  • invoke_rcu_callbacks

    • 作用:执行之前通过 call_rcu 注册的回调函数。
    • 调用时机:在 grace period 结束后,由 RCU 核心机制触发。
    • 执行位置:通常在软中断上下文或专门的 RCU 线程中执行。
3.2.2.1 工作流程
  1. 注册回调:
    • 当一个 RCU 保护的数据结构被更新时,更新者调用 call_rcu
    • call_rcu 将回调函数和相关数据添加到当前 CPU 的 RCU 回调队列中。
  2. 等待 grace period:
    • RCU 核心机制负责检测 grace period 的结束。
    • 在这期间,读者可以安全地访问旧版本的数据。
  3. 执行回调:
    • 当 grace period 结束时,RCU 核心机制调用 invoke_rcu_callbacks
    • invoke_rcu_callbacks 遍历回调队列,执行每个注册的回调函数。

Grace period在 RCU (Read-Copy-Update) 机制中是一个关键概念。它指的是一段时间间隔,在这段时间结束后,可以保证所有正在进行的 RCU 读操作都已经完成。

RCU回调函数通常在一个特殊的上下文(如NMI或软中断)中执行,以避免干扰正常的内核执行流。call_rcu()函数负责调度这些回调。call_rcu 的简化实现:

void call_rcu(struct rcu_head *head, rcu_callback_t func)
{
    unsigned long flags;
    struct rcu_data *rdp;

    head->func = func;
    local_irq_save(flags);
    rdp = this_cpu_ptr(&rcu_data);
    __call_rcu(head, func, rdp, false, 0);
    local_irq_restore(flags);
}

这个函数主要做了以下几件事:

  1. 将回调函数 func 保存到 rcu_head 结构中。
  2. 禁用本地中断以确保操作的原子性。
  3. 获取当前 CPU 的 RCU 数据结构。
  4. 调用 __call_rcu 函数来实际注册回调。
  5. 恢复本地中断。

__call_rcu 函数是实际执行回调注册的地方:

static void
__call_rcu(struct rcu_head *head, rcu_callback_t func,
           struct rcu_data *rdp, bool lazy, unsigned long flags)
{
    unsigned long js;
    struct rcu_node *rnp;

    // 检查是否设置了debug标志和是否处于禁止回调的状态
    RCU_LOCKDEP_WARN(rcu_scheduler_active == RCU_SCHEDULER_INACTIVE,
                     "call_rcu() before rcu_init_sched()");
    if (debug_rcu_head_queue(head)) {
        // 处理调试相关的操作
        return;
    }

    // 设置回调函数
    head->func = func;
    head->next = NULL;

    // 获取当前的 grace period 计数
    js = rdp->jiffies_snap;

    // 选择适当的回调队列并将回调添加到队列中
    if (lazy)
        // 对于懒惰RCU,使用不同的队列
        rcu_segcblist_add_cblist_tail(&rdp->lazy_segcblist, head, js);
    else
        rcu_segcblist_add_cblist_tail(&rdp->cblist, head, js);

    // 如果需要加速处理,则触发RCU核心机制
    if (__is_kfree_rcu_offset((unsigned long)func))
        rcu_accelerate_cbs(rdp->mynode, rdp);

    // 如果当前CPU被离线,则将回调转移到在线CPU
    if (unlikely(!rcu_segcblist_completely_offloaded(&rdp->cblist))) {
        rcu_lockdep_assert_cblist_protected(rdp);
        rcu_seg_inv_notify(rdp);
    }

    // 如果需要唤醒RCU核心机制,则执行唤醒操作
    if (rcu_segcblist_need_wake(&rdp->cblist))
        rcu_ring_wake(rdp);
}

这个函数完成了以下主要任务:

  1. 进行一些调试和安全检查。
  2. 将回调函数设置到 rcu_head 结构中。
  3. 将回调添加到适当的回调队列中。
  4. 如果需要,触发 RCU 核心机制来加速处理。
  5. 处理 CPU 离线情况下的回调转移。
  6. 如果需要,唤醒 RCU 核心机制。

invoke_rcu_callbacks 的简化实现:

static void invoke_rcu_callbacks(struct rcu_state *rsp)
{
    struct rcu_data *rdp;
    struct rcu_head *rhp;
    long n;

    for_each_online_cpu(n) {
        rdp = per_cpu_ptr(rsp->rda, n);
        while ((rhp = rcu_callback_dequeue(rdp)) != NULL)
            rhp->func(rhp);
    }
}

这个函数遍历所有在线CPU,处理每个CPU的回调队列。

3.2.2.2 关键点
  • 解耦:call_rcuinvoke_rcu_callbacks 的分离允许 RCU 更新操作快速完成,而不需要等待所有读者完成。
  • 延迟执行:call_rcu 注册的回调不会立即执行,而是等到安全的时机(grace period 结束后)才由 invoke_rcu_callbacks 执行。
  • 性能优化:这种机制允许批量处理回调,减少了同步开销,提高了系统整体性能。
  • 灵活性:回调机制为 RCU 用户提供了极大的灵活性,允许他们定义在 grace period 结束后需要执行的清理操作。

call_rcuinvoke_rcu_callbacks 共同构成了 RCU 的回调处理系统。call_rcu 负责注册回调,而 invoke_rcu_callbacks 负责在适当的时机执行这些回调。这种设计使得 RCU 能够在保证数据一致性的同时,实现高效的无锁读取和低延迟的更新操作。

2.4. RCU的优化

2.4.1 Tree RCU

为了提高可扩展性,Linux内核实现了Tree RCU。它使用树状结构来组织CPU,减少了全局同步的需求。

struct rcu_node {
    raw_spinlock_t lock;
    unsigned long gp_seq;
    unsigned long gp_seq_needed;
    unsigned long qsmask;
    u8 wait_blkd_tasks;
    u8 child_overflow;
    u8 forced_restart;
    struct rcu_node *parent;
    unsigned long grpmask;
    int level;
    struct rcu_node *rcu_node_tree;
    ...
};

rcu_node 结构体代表树中的一个节点,管理一组CPU的RCU状态。

2.4.2 RCU加速

Linux内核还实现了一些RCU的加速机制,比如 Expedited RCU grace periods。

void synchronize_rcu_expedited(void)
{
    bool need_full_sync = false;
    struct rcu_node *rnp;
    unsigned long flags;
    int ret;

    ret = synchronize_rcu_expedited_wait();
    if (ret > 0)
        return;  // 快速路径成功

    // 如果快速路径失败,回退到完整同步
    synchronize_rcu();
}

这个函数首先尝试一个快速路径,如果失败则回退到标准的 synchronize_rcu()

2.4 RCU栅栏

RCU栅栏的主要目的是确保所有先前的RCU读取临界区都已经完成。它的核心实现是synchronize_rcu()函数。

void synchronize_rcu(void)
{
    RCU_LOCKDEP_WARN(!rcu_is_watching(),
                     "synchronize_rcu() called with preemption or interrupts disabled");
    if (rcu_expedited)
        synchronize_rcu_expedited();
    else
        wait_rcu_gp(call_rcu);
}

这个函数的主要步骤:

  1. 首先进行锁依赖检查,确保调用环境正确。
  2. 如果启用了快速模式(rcu_expedited为true),调用synchronize_rcu_expedited()
  3. 否则,调用wait_rcu_gp(call_rcu)等待一个完整的宽限期(grace period)。
void wait_rcu_gp(call_rcu_func_t crf)
{
    struct rcu_synchronize rcu;

    init_rcu_head_on_stack(&rcu.head);
    init_completion(&rcu.completion);
    crf(&rcu.head, wakeme_after_rcu);
    wait_for_completion(&rcu.completion);
    destroy_rcu_head_on_stack(&rcu.head);
}

wait_rcu_gp()函数的工作流程:

  1. 初始化一个rcu_synchronize结构体。
  2. 设置一个RCU回调(wakeme_after_rcu)。
  3. 等待回调完成,这保证了所有先前的RCU读取临界区都已结束。

2.5 周期性检查

RCU的周期性检查主要通过rcu_check_callbacks()函数实现,它被定期调用来处理RCU的各种任务。这通常通过软中断处理程序完成,例如rcu_sched_qs()rcu_bh_qs()

void rcu_check_callbacks(int cpu, int user)
{
    struct rcu_state *rsp;

    if (user || rcu_is_cpu_rrupt_from_idle()) {
        *this_cpu_ptr(&rcu_dynticks.dynticks) |= RCU_DYNTICK_CTRL_MASK;
        rcu_dynticks_task_enter();
    }
    for_each_rcu_flavor(rsp)
        __rcu_check_callbacks(rsp, cpu);
}

这个函数的主要任务:

  1. 更新当前CPU的动态时钟状态。
  2. 对每种RCU风格(如RCU-bh、RCU-sched等)调用__rcu_check_callbacks()
static void __rcu_check_callbacks(struct rcu_state *rsp, int cpu)
{
    struct rcu_data *rdp = per_cpu_ptr(rsp->rda, cpu);

    if (check_for_new_grace_period(rsp, rdp))
        invoke_rcu_core();
    if (rcu_pending(cpu))
        invoke_rcu_core();
}
  • 检查是否需要开始新的宽限期。
  • 检查是否有待处理的RCU操作。
  • 如果需要,调用invoke_rcu_core()来处理RCU核心逻辑。

5. 总结

RCU是一个复杂而强大的同步机制,它通过允许读操作几乎无开销地进行,同时保证写操作的正确性,极大地提高了系统的并发性能。其核心在于巧妙地利用了硬件缓存一致性协议和操作系统调度特性,实现了一种"等待"所有读操作完成的机制,而无需显式地跟踪每个读操作。

这个分析涵盖了RCU的基本概念、主要组成部分、核心实现以及一些优化技术。然而,由于RCU的复杂性,实际的内核实现还包含了大量的细节和特殊情况处理,这里只是概述了其中的核心内容。

2.例示代码

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/init.h>

struct RCUStruct {

	int a;
	struct rcu_head rcu;
};

static struct RCUStruct* Global_pointer;

static struct task_struct* RCURDThread1, *RCURDThread2, *RCUWTThread;

static int RCURDThreadFunc1(void* argc) {

	struct RCUStruct* pointer = NULL;

	while(1) {

		msleep(5);

		rcu_read_lock();

		mdelay(10);
		pointer = rcu_dereference(Global_pointer);
		if(pointer)
			printk("%s : read a = %d\n", __func__, pointer->a);

		rcu_read_unlock();
	}

	return 0;
}

static int RCURDThreadFunc2(void* argc) {

	struct RCUStruct* pointer = NULL;

	while(1) {

		msleep(5);

		rcu_read_lock();

		mdelay(10);
		pointer = rcu_dereference(Global_pointer);
		if(pointer)
			printk("%s : read a = %d\n", __func__, pointer->a);

		rcu_read_unlock();
	}

	return 0;
}

static void MyRCUDel(struct rcu_head* rcuh) {

	struct RCUStruct* p = container_of(rcuh, struct RCUStruct, rcu);
	printk("%s : a = %d\n", __func__, p->a);
	kfree(p);
}

static int RCUWTThreadFunc(void* argc) {

	struct RCUStruct* old_pointer;
	struct RCUStruct* new_pointer;

	int value = (unsigned long)argc;

	while(1) {

		msleep(10);

		new_pointer = kmalloc(sizeof(struct RCUStruct), GFP_KERNEL);

		old_pointer = Global_pointer;
		*new_pointer = *old_pointer;
		new_pointer->a = value;

		rcu_assign_pointer(Global_pointer, new_pointer);
		call_rcu(&old_pointer->rcu, MyRCUDel);
		printk("%s : write to new %d\n", __func__, value);
		value++;
	}

	return 0;
}

static int __init RCUFuncInit(void) {

	int value = 2;
	printk("Prompt:Successfully initialized the kernel module.\n");

	Global_pointer = kzalloc(sizeof(struct RCUStruct), GFP_KERNEL);

	RCURDThread1 = kthread_run(RCURDThreadFunc1, NULL, "RCURD1");
	RCURDThread2 = kthread_run(RCURDThreadFunc2, NULL, "RCURD2");

	RCUWTThread = kthread_run(RCUWTThreadFunc, (void*)(unsigned long)value, "RCUWT");

	return 0;
}

static void __exit RCUFuncExit(void) {

	printk("Prompt:Successfully uninstalled kernel module!\n");

	kthread_stop(RCURDThread1);
	kthread_stop(RCURDThread2);
	kthread_stop(RCUWTThread);

	if(Global_pointer)
		kfree(Global_pointer);
}

module_init(RCUFuncInit);
module_exit(RCUFuncExit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("lenn louis");

3.Makefile

obj-m:=rcu.o	

CURRENT_PAHT:=$(shell pwd) 
LINUX_KERNEL:=$(shell uname -r)   

LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
all:

	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PAHT) modules

clean:

	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PAHT) cleals

相关链接

Linux源码阅读笔记12-RCU案例分析_rcu机制详解,代码案例(linuxc语言)-CSDN博客

Linux内核中的RCU stall是指在RCU(Read-Copy-Update)机制中,由于某些原因导致读操作被长时间阻塞的情况。当RCU stall发生时,CPU无法进入RCU的快速路径,影响了系统的性能和响应时间。 触发RCU stall的完整堆栈如下: ``` rcu_bh_kthread kthread ret_from_fork (idle) schedule rcu_gp_kthread kthread ret_from_fork (idle) schedule rcu_sched_clock_irq rcu_check_callbacks __rcu_process_callbacks rcu_process_gp_end rcu_do_batch __rcu_pending rcu_pending rcu_sched_qs rcu_stall_common rcu_stall_kick_cpu native_safe_halt cpu_idle ``` 下面是对触发RCU stall的完整堆栈的详细解释: 1. `rcu_bh_kthread`:这是一个内核线程,用于处理RCU的底半部(bottom half)。它负责处理一些需要被延迟执行的任务,如内存回收等。 2. `kthread`:这是内核中的线程管理机制,用于创建和管理内核线程。 3. `ret_from_fork`:这是从内核线程返回用户空间的过程。 4. `(idle)`:这表示系统处于空闲状态,没有正在执行的任务。 5. `schedule`:这是内核中的调度器函数,用于选择下一个要运行的任务。 6. `rcu_gp_kthread`:这是一个内核线程,用于处理RCU的Grace Period(GP)。GP是一个时间间隔,在这个时间间隔内,所有已经开始进行读操作的线程都可以继续执行,而不会被阻塞。 7. `kthread`、`ret_from_fork`、`(idle)`和`schedule`:同上述解释。 8. `rcu_sched_clock_irq`:这是一个中断处理函数,用于处理定时器中断,并触发RCU的回调函数。 9. `rcu_check_callbacks`:这是一个RCU检查回调函数的函数,用于确定是否有需要执行的RCU回调函数。 10. `__rcu_process_callbacks`:这是处理RCU回调函数的函数。 11. `rcu_process_gp_end`:这是RCU GP结束的处理函数,用于标记GP结束,并进行一些清理工作。 12. `rcu_do_batch`:这是执行RCU回调函数的函数。 13. `__rcu_pending`:这是检查是否有待处理的RCU回调函数的函数。 14. `rcu_pending`:这是等待RCU回调函数处理完成的函数。 15. `rcu_sched_qs`:这是一个RCU调度器函数,用于处理短期的RCU stall情况。 16. `rcu_stall_common`:这是一个通用的RCU stall处理函数,用于处理长期的RCU stall情况。 17. `rcu_stall_kick_cpu`:这是一个处理RCU stall的函数,当检测到长时间的阻塞时,会尝试唤醒被阻塞的CPU。 18. `native_safe_halt`:这是一个使CPU进入低功耗状态的函数。 19. `cpu_idle`:这是一个空闲CPU进入睡眠状态的函数。 以上堆栈描述了触发RCU stall的情况下,内核中涉及的关键函数和线程。当RCU stall发生时,会通过RCU的回调函数处理机制RCU GP的处理和RCU stall的检测机制来尝试解决或减轻RCU stall的影响。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

甜航一直在

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值