0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

基于Linux内核源码的RCU实现方案

454398 来源: Chinaunix 作者:hiyachen 2020-09-25 15:10 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

RCU(Read-Copy Update)是数据同步的一种方式,在当前的Linux内核中发挥着重要的作用。RCU主要针对的数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的使用RCU机制读取数据的时候不对链表进行耗时的加锁操作。这样在同一时间可以有多个线程同时读取该链表,并且允许一个线程对链表进行修改(修改的时候,需要加锁)。RCU适用于需要频繁的读取数据,而相应修改数据并不多的情景,例如在文件系统中,经常需要查找定位目录,而对目录的修改相对来说并不多,这就是RCU发挥作用的最佳场景。

Linux内核源码当中,关于RCU的文档比较齐全,你可以在 /Documentation/RCU/ 目录下找到这些文件。Paul E. McKenney 是内核中RCU源码的主要实现者,他也写了很多RCU方面的文章。他把这些文章和一些关于RCU的论文的链接整理到了一起。http://www2.rdrop.com/users/paulmck/RCU/

在RCU的实现过程中,我们主要解决以下问题:

1,在读取过程中,另外一个线程删除了一个节点。删除线程可以把这个节点从链表中移除,但它不能直接销毁这个节点,必须等到所有的读取线程读取完成以后,才进行销毁操作。RCU中把这个过程称为宽限期(Grace period)。

2,在读取过程中,另外一个线程插入了一个新节点,而读线程读到了这个节点,那么需要保证读到的这个节点是完整的。这里涉及到了发布-订阅机制(Publish-Subscribe Mechanism)。

3, 保证读取链表的完整性。新增或者删除一个节点,不至于导致遍历一个链表从中间断开。但是RCU并不保证一定能读到新增的节点或者不读到要被删除的节点。

宽限期

通过例子,方便理解这个内容。以下例子修改于Paul的文章。

[cpp]view plaincopy

structfoo{

inta;

charb;

longc;

};

DEFINE_SPINLOCK(foo_mutex);

structfoo*gbl_foo;

voidfoo_read(void)

{

foo*fp=gbl_foo;

if(fp!=NULL)

dosomething(fp->a,fp->b,fp->c);

}

voidfoo_update(foo*new_fp)

{

spin_lock(&foo_mutex);

foo*old_fp=gbl_foo;

gbl_foo=new_fp;

spin_unlock(&foo_mutex);

kfee(old_fp);

}

如上的程序,是针对于全局变量gbl_foo的操作。假设以下场景。有两个线程同时运行 foo_ read和foo_update的时候,当foo_ read执行完赋值操作后,线程发生切换;此时另一个线程开始执行foo_update并执行完成。当foo_ read运行的进程切换回来后,运行dosomething 的时候,fp已经被删除,这将对系统造成危害。为了防止此类事件的发生,RCU里增加了一个新的概念叫宽限期(Grace period)。如下图所示:

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

[cpp]view plaincopy

voidfoo_read(void)

{

rcu_read_lock();

foo*fp=gbl_foo;

if(fp!=NULL)

dosomething(fp->a,fp->b,fp->c);

rcu_read_unlock();

}

voidfoo_update(foo*new_fp)

{

spin_lock(&foo_mutex);

foo*old_fp=gbl_foo;

gbl_foo=new_fp;

spin_unlock(&foo_mutex);

synchronize_rcu();

kfee(old_fp);

}

其中foo_read中增加了rcu_read_lock和rcu_read_unlock,这两个函数用来标记一个RCU读过程的开始和结束。其实作用就是帮助检测宽限期是否结束。foo_update增加了一个函数synchronize_rcu(),调用该函数意味着一个宽限期的开始,而直到宽限期结束,该函数才会返回。我们再对比着图看一看,线程1和2,在synchronize_rcu之前可能得到了旧的gbl_foo,也就是foo_update中的old_fp,如果不等它们运行结束,就调用kfee(old_fp),极有可能造成系统崩溃。而3,4,6在synchronize_rcu之后运行,此时它们已经不可能得到old_fp,此次的kfee将不对它们产生影响。

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

订阅——发布机制

当前使用的编译器大多会对代码做一定程度的优化,CPU也会对执行指令做一些优化调整,目的是提高代码的执行效率,但这样的优化,有时候会带来不期望的结果。如例:

[cpp]view plaincopy

voidfoo_update(foo*new_fp)

{

spin_lock(&foo_mutex);

foo*old_fp=gbl_foo;

new_fp->a=1;

new_fp->b=‘b’;

new_fp->c=100;

gbl_foo=new_fp;

spin_unlock(&foo_mutex);

synchronize_rcu();

kfee(old_fp);

}

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

rcu_assign_pointer(gbl_foo,new_fp);

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

[cpp]view plaincopy

#definercu_assign_pointer(p,v)

__rcu_assign_pointer((p),(v),__rcu)

#define__rcu_assign_pointer(p,v,space)

do{

smp_wmb();

(p)=(typeof(*v)__forcespace*)(v);

}while(0)

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

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

[cpp]view plaincopy

voidfoo_read(void)

{

rcu_read_lock();

foo*fp=gbl_foo;

if(fp!=NULL)

dosomething(fp->a,fp->b,fp->c);

rcu_read_unlock();

}

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

[cpp]view plaincopy

#definercu_dereference(p)rcu_dereference_check(p,0)

#definercu_dereference_check(p,c)

__rcu_dereference_check((p),rcu_read_lock_held()||(c),__rcu)

#define__rcu_dereference_check(p,c,space)

({

typeof(*p)*_________p1=(typeof(*p)*__force)ACCESS_ONCE(p);

rcu_lockdep_assert(c,"suspiciousrcu_dereference_check()"

"usage");

rcu_dereference_sparse(p,space);

smp_read_barrier_depends();

((typeof(*p)__force__kernel*)(_________p1));

})

staticinlineintrcu_read_lock_held(void)

{

if(!debug_lockdep_rcu_enabled())

return1;

if(rcu_is_cpu_idle())

return0;

if(!rcu_lockdep_current_cpu_online())

return0;

returnlock_is_held(&rcu_lock_map);

}

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

[cpp]view plaincopy

#definercu_dereference(p)({

typeof(p)_________p1=p;

smp_read_barrier_depends();

(_________p1);

})

在赋值后加入优化屏障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
    +关注

    关注

    88

    文章

    11849

    浏览量

    219759
  • 数据同步
    +关注

    关注

    0

    文章

    18

    浏览量

    8376
  • rcu
    rcu
    +关注

    关注

    0

    文章

    21

    浏览量

    5782
收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    openKylin受邀参加Linux内核领域顶级技术峰会

    近日,Linux内核领域顶级技术峰会——LSF/MM/BPF 2026在克罗地亚萨格勒布举行。大会聚焦Linux内核存储、文件系统、内存管理以及BPF等关键技术方向,吸引了来自全球开源
    的头像 发表于 05-14 11:08 496次阅读

    Linux Kernel 6.1 tools目录全解析 | RK平台ARM64交叉编译实战指南

      在瑞芯微( RK ) ARM64 平台嵌入式 Linux 开发、内核调试与硬件外设测试中, Linux 内核源码自带的 tools 目录
    的头像 发表于 04-16 18:42 7723次阅读
    <b class='flag-5'>Linux</b> Kernel 6.1 tools目录全解析 | RK平台ARM64交叉编译实战指南

    如何理解Linux内核中的PCIe驱动

    我们习惯了用 Verilog 去死磕 PCIe 的底层协议状态机。但一旦越过硬件边界来到操作系统层面,Linux 内核是如何接管并驱动这些 PCI/PCIe 设备的呢?由于不同的 CPU 架构实现
    的头像 发表于 04-11 17:22 1368次阅读

    Linux内核驱动开发的技术核心精要

    ;initcall_debug用于启动优化;kgdb支持源码级调试;动态调试(dynamic debug)灵活开启日志。内核移植新板时需实现时钟树、中断控制器、GPIO、早期串口等基础支撑,并正确编写设备树,最终使能
    发表于 03-10 13:56

    送4本!206 张手绘图+源码+视频!Linux 老兵呕心之作,让小白也能吃透底层逻辑

    作为开发者,你是否也有过这样的困扰?想学Linux网络编程,却被枯燥的理论、复杂的内核源码劝退;照搬别人的代码能跑通项目,但遇到问题不知如何排查,更谈不上架构优化;市面上的教材要么只讲API用法,要么满
    的头像 发表于 02-05 08:08 211次阅读
    送4本!206 张手绘图+<b class='flag-5'>源码</b>+视频!<b class='flag-5'>Linux</b> 老兵呕心之作,让小白也能吃透底层逻辑

    Linux内核的“心跳”:jiffies如何为系统计时?

    Linux 内核的世界里,有一个默默工作的 "计时器"——jiffies。它不像我们手机上的时钟那样显示年月日,却掌控着内核中绝大多数时间相关的操作:从进程调度到设备驱动的定时检查,都离不开它的身影。
    的头像 发表于 02-04 16:27 974次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>的“心跳”:jiffies如何为系统计时?

    Linux系统内核参数调优实战指南

    Linux 内核参数调优是系统性能优化的核心环节。随着云原生架构的普及和硬件性能的飞速提升,默认的内核参数配置往往无法充分发挥系统潜力。在高并发 Web 服务、大数据处理、容器化部署等场景下,合理的
    的头像 发表于 01-28 14:27 829次阅读

    【「Linux 设备驱动开发(第 2 版)」阅读体验】Linux内核开发基础

    ()、msleep_interruptible()这些API用于简单睡眠 内核延迟或忙等待 可以调用ndelay()、udelay()、mdelay()这些API实现 Linux内核
    发表于 01-12 22:45

    【「Linux 设备驱动开发(第 2 版)」阅读体验】+读内核处理的核心辅助函数

    “处理内核的核心辅助函数”进行学习。 第3章又是以5个主题展开讨论学习,①、Linux内核加锁机制和共享资源;②、处理内核等待、睡眠和延迟机制;③、深入理解
    发表于 01-10 22:08

    深入Linux内核:进程调度的核心逻辑与实现细节

    ,背后都离不开内核调度算法的精准操控。今天,我们就从优先级、调度算法、时间片分配到底层实现,全方位拆解Linux内核进程调度的核心逻辑。 一、进程调度的“身份标识”:优先级与分类 要理
    的头像 发表于 12-24 07:05 4696次阅读
    深入<b class='flag-5'>Linux</b><b class='flag-5'>内核</b>:进程调度的核心逻辑与<b class='flag-5'>实现</b>细节

    基于 DR1M90 的 Linux-RT 内核开发:从编译配置到 GPIO / 按键应用实现(1)

    本手册由创龙科技研发,针对 DR1M90,详述 Linux-RT 实时内核开发:含实时性测试(LinuxLinux-RT 对比、CPU 空载 / 满负荷 / 隔离状态测试)、
    的头像 发表于 12-02 10:38 1401次阅读
    基于 DR1M90 的 <b class='flag-5'>Linux</b>-RT <b class='flag-5'>内核</b>开发:从编译配置到 GPIO / 按键应用<b class='flag-5'>实现</b>(1)

    如何将 GPIO PWM 和 GPIO Capture 驱动程序导入 Linux 内核实现 PWM 输出并检测引脚的变化状态?

    如何将 GPIO PWM 和 GPIO Capture 驱动程序导入 Linux 内核实现 PWM 输出并检测引脚的变化状态
    发表于 08-20 08:20

    Linux内核参数调优方案

    在高并发微服务环境中,网络性能往往成为K8s集群的瓶颈。本文将深入探讨如何通过精细化的Linux内核参数调优,让你的K8s节点网络性能提升30%以上。
    的头像 发表于 08-06 17:50 1233次阅读

    迅为RK3568开发板OpeHarmony学习开发手册1.1-内核移植优化

    ;copy 到 out 目录-->打新补丁 那么优化思路是什么呢? out/kernel/src_tmp/linux-5.10/ linux-5.10/ 下的内核源码是已经打过原
    发表于 07-26 10:37

    如何配置和验证Linux内核参数

    Linux系统运维和性能优化中,内核参数(sysctl)的配置至关重要。合理的参数调整可以显著提升网络性能、系统稳定性及资源利用率。然而,仅仅修改参数是不够的,如何验证这些参数是否生效同样关键。
    的头像 发表于 05-29 17:40 1475次阅读