rcu linux?linux内核启动过程详解

其实rcu linux的问题并不复杂,但是又很多的朋友都不太了解linux内核启动过程详解,因此呢,今天小编就来为大家分享rcu linux的一些知识,希望可以帮助到大家,下面我们一起来看看这个问题的分析吧!

Linux内核 RCU机制详解

RCU(Read-Copy Update)是一种数据同步方式,在Linux内核中扮演着重要角色。它主要针对链表数据对象,旨在提升遍历读取数据的效率。使用RCU机制读取数据时,不对链表进行耗时加锁操作,允许多个线程同时读取,同时允许一个线程进行修改(修改时需加锁)。RCU适用于频繁读取数据但修改较少的场景,例如文件系统中频繁查找目录,但修改相对较少。

Linux内核源码中的RCU文档较为丰富,可在/Documentation/RCU/目录下找到相关文件。Paul E. McKenney是RCU源码的主要实现者,他撰写了大量关于RCU的文章,并将相关论文链接整理在www2.rdrop.com/users/pa...。

RCU实现过程中需解决以下问题:

1.在读取过程中,另一线程删除了节点。删除线程可将其从链表中移除,但不能直接销毁,必须等待所有读取线程完成后才能销毁。RCU中称此为宽限期。

2.在读取过程中,另一线程插入新节点,读线程读取到该节点,需保证读取到的节点完整。这涉及发布-订阅机制。

3.保证读取链表的完整性。新增或删除节点,防止遍历链表时从中间断开。但RCU不保证一定能读到新增节点或不读到要删除的节点。

宽限期:以下例子修改自Paul的文章。

以下程序针对全局变量gbl_foo的操作。假设场景:两个线程同时运行foo_read和foo_update,当foo_read执行完赋值操作后线程切换;此时另一个线程开始执行foo_update并完成。当foo_read运行的进程切换回来后,运行dosomething时,fp已被删除,这将危害系统。为防止此类事件,RCU增加宽限期概念。如图所示:

图中每行代表一个线程,最下面一行是删除线程,执行完删除操作后进入宽限期。宽限期的意义是,删除动作发生后,必须等待所有宽限期开始前已经开始的读线程结束,才能进行销毁操作。这样做的原因是这些线程可能读取到要删除的元素。图中的宽限期需等待1和2结束;而读线程5在宽限期开始前已结束,无需考虑;而3、4、6也不需考虑,因为在宽限期结束后开始后的线程不可能读取到已删除的元素。为此,RCU机制提供了相应的API来实现这个功能。

宽限期是RCU实现中最复杂的部分,原因是在提高读数据性能的同时,删除数据的性能也不能太差。

订阅-发布机制:当前编译器会对代码进行一定程度的优化,CPU也会对执行指令进行优化调整,以提高代码执行效率。但这样的优化有时会带来不期望的结果。如例:

这段代码中,我们期望6、7、8行代码在第10行代码之前执行。但优化后的代码并不保证执行顺序。在这种情况下,一个读线程可能读取到new_fp,但new_fp的成员赋值尚未完成。当读线程执行dosomething(fp->a, fp->b, fp->c)时,就有可能出现不确定的参数传入,可能造成不期望的结果,甚至程序崩溃。可以通过优化屏障来解决该问题,RCU机制对优化屏障进行了封装,提供了专用的API来解决该问题。此时,第十行不再是直接的指针赋值,而应改为:

rcu_assign_pointer(gbl_foo, new_fp);

rcu_assign_pointer的实现较为简单,如下:

我们可以看到它的实现只是在赋值之前加上了优化屏障smp_wmb,以确保代码的执行顺序。另外,宏中使用的__rcu只是作为编译过程的检测条件。

在DEC Alpha CPU机器上还有一种更强的优化,如下所示:

第六行的fp->a, fp->b, fp->c会在第三行还没执行的时候就预先判断运行,当它与foo_update同时运行时,可能导致传入dosomething的一部分属于旧的gbl_foo,而另一部分属于新的。这样可能导致运行结果的错误。为了避免此类问题,RCU还是提供了宏来解决该问题:

这段代码中加入了调试信息,去除调试信息可以是以下的形式(其实这也是旧版本中的代码):

在赋值后加入优化屏障smp_read_barrier_depends()。

我们之前的第四行代码改为foo*fp= rcu_dereference(gbl_foo);,就可以防止上述问题。

数据读取的完整性:以下例子说明这个问题。

如图,我们在原list中加入一个节点new到A之前,第一步是将new的指针指向A节点,第二步是将Head的指针指向new。这样做的目的是当插入操作完成第一步时,对于链表的读取并不产生影响;而执行完第二步时,读线程如果读到new节点,也可以继续遍历链表。如果把这个过程反过来,第一步head指向new,而这时一个线程读到new,由于new的指针指向的是Null,这将导致读线程无法读取到A、B等后续节点。从以上过程中,可以看出RCU并不保证读线程读取到new节点。如果该节点对程序产生影响,那么就需要外部调用做相应的调整。如在文件系统中,通过RCU定位后,如果查找不到相应节点,就会进行其他形式的查找,相关内容等分析到文件系统时再进行叙述。

我们再看一下删除一个节点的例子:

如图,我们希望删除B,这时候要做的就是将A的指针指向C,保持B的指针,然后删除程序将进入宽限期检测。由于B的内容并没有变更,读到B的线程仍然可以继续读取B的后续节点。B不能立即销毁,必须等待宽限期结束后才能进行相应销毁操作。由于A的节点已经指向了C,当宽限期开始之后所有的后续读操作通过A找到的是C,而B已经隐藏了,后续的读线程都不会读到它。这样就确保宽限期过后,删除B并不对系统造成影响。

RCU的原理并不复杂,应用也很简单。但代码的实现并不容易,难点都集中在宽限期的检测上,后续分析源代码时,我们可以看到一些极富技巧的实现方式。

Linux 内核:RCU机制与使用

在学习Linux源码时,遇到带有__rcu后缀的数据结构,引发对RCU机制的好奇。RCU(Read-Copy Update)是数据同步机制,主要用于优化链表遍历读取效率,避免锁竞争和内存延迟,适用于读多写少的场景,如文件系统中频繁查找定位目录而目录修改相对较少的情况。

RCU机制通过在读取数据时不对链表加锁,允许多线程同时读取,但当线程尝试修改数据时,必须加锁以保证数据一致性。这种机制显著提升了性能,尤其在大量读取少量修改的场景中。

在Linux内核源码中,RCU的详细文档和实现源码可于Documentation/RCU/目录下找到。Paul E. McKenney为主要实现者,并整理了相关文章和链接供参考。

RCU解决了多个关键问题:如在读取过程中,另一个线程可能删除或插入节点,RCU通过宽限期确保数据的完整性和一致性;发布-订阅机制确保插入的节点在读取时得到完整引用;并保证链表遍历的一致性,避免中间断开。

RCU基于读-拷贝修改原理,允许读者无锁访问数据,而写者在进行修改时,先复制数据结构副本,修改副本后,通过回调函数在适当时机完成数据结构的更新或释放。这个适当时机由内核自动确定。

RCU的核心在于允许并行的读取操作,同时对写操作进行延迟处理,通过读者信号和垃圾收集器确保数据的一致性和安全性。与传统锁机制相比,RCU减少了锁竞争和内存延迟,提升了性能。

RCU通过grace period(宽限期)和quiescent state(静默状态)机制,确保写操作在所有读操作完成后执行,从而避免了数据不一致问题。RCU的实现包括rcu_read_lock和rcu_read_unlock,用于管理读操作的临界区,以及synchronize_rcu用于挂起写操作,直到所有读操作结束。

在使用RCU保护共享数据结构时,读者可以自由访问,无需加锁;而写者在访问数据时,先复制副本进行修改,最后通过回调函数在适当时机执行真正的修改操作。这种机制确保了数据的一致性和安全性,同时避免了锁竞争的性能开销。

RCU通过一系列核心API,如rcu_read_lock和rcu_read_unlock,以及synchronize_rcu,实现了读操作的并发性和写操作的延迟处理。读者通过这些API进入读临界区,而写者通过synchronize_rcu挂起,直到所有读操作完成。

在实际应用中,RCU允许在不牺牲性能的情况下,处理大量读取和少量写入的操作。例如,在系统调用审计、路由表维护等场景中,使用RCU可以显著提升性能,同时减少锁竞争和内存延迟的问题。

RCU机制虽然提升了性能,但也存在内存占用和写操作开销等问题。在考虑使用RCU时,需要权衡其带来的性能提升与内存使用和写操作的复杂性。

Linux内核RCU实现简析

RCU,即Read-Copy-Update,是一种并发控制技术,旨在解决并发读写时的读阻塞问题。其核心思想是当写线程进行操作时,不直接修改原数据,而是先复制数据,进行修改后,等待读线程的访问结束,再替换原数据,从而避免阻塞读线程。

引入了Quiescent State和Quiescent Period的概念。Quiescent State表示线程没有访问数据的静止状态,Quiescent Period是等待时间窗口,确保在此窗口内没有线程访问旧数据,以便释放资源。

在Linux内核中,RCU接口丰富多样,适用于不同场景和子系统,包括rcu_read_lock/unlock、call_rcu、synchronize_rcu等。其中,call_rcu用于写线程注册回调函数,等待Quiescent Period结束后执行,而synchronize_rcu则封装了call_rcu,同步等待Quiescent Period完成。

Linux内核的RCU实现是作者Paul Mckenney完成和维护的,位于内核源代码的kernel/rcu目录下,详细文档在Documentation/RCU中。内核使用的是树形结构的RCU(tree-RCU),支持多核扩展。

树形结构中,每个节点为rcu_node,每个CPU对应一个rcu_data,共同维护一个全局的rcu_state变量,记录整个系统RCU状态。RCU的生命周期包括注册Grace Period回调、开始Grace Period、CPU进入Quiescent State、Grace Period完成等阶段。

在注册Grace Period回调时,回调函数被放入本CPU的rcu_data中。Grace Period开始时,内核线程rcu_gp_kthread被唤醒,执行一系列初始化操作,包括设置系统中所有CPU的Quiescent State。CPU进入Quiescent State后,内核线程rcu_cpu_kthread检查并上报Quiescent State,直到所有CPU都完成上报,表明系统整体进入Quiescent State,Grace Period完成。

当Grace Period完成时,注册的回调函数执行。这发生在每个CPU的rcu_cpu_kthread中,其中内核线程rcu_gp_kthread仅更新了rcu_node的gp_seq,未直接触发回调执行。rcu_cpu_kthread在检查函数rcu_check_quiescent_state中确认Grace Period已完成,通过内部检查判断当前gp序列是否已完成,若已完成,则将所有callback移动到RCU_DONE_TAIL列表上,后续在rcu_do_batch函数中执行每个callback。

阅读剩余
THE END