本文简介
本文介绍Linux RCU的基本概念。这不是一篇单独的文章,这是《谢宝友:深入理解Linux RCU》系列的第3篇,前序文章:
谢宝友: 深入理解Linux RCU之一——从硬件说起
作者简介
谢宝友,在编程一线工作已经有20年时间,其中接近10年时间工作于Linux操作系统。在中兴通讯操作系统产品部工作期间,他作为技术总工参与的电信级嵌入式实时操作系统,获得了行业最高奖----中国工业大奖。同时,他也是《深入理解并行编程》一书的译者。
联系方式: mail:scxby@163.com 微信:linux-kernel
稿件征集
欢迎您给Linuxer投稿,赢得人民邮电异步社区任意在售技术图书。您随便挑,详情:
Linuxer-"Linux开发者自己的媒体"首月稿件录取和赠书名单
Linuxer-"Linux开发者自己的媒体"第二月稿件录取和赠书名单
Linuxer-"Linux开发者自己的媒体"第三月稿件录取和赠书名单
![](https://i-blog.csdnimg.cn/blog_migrate/2d4fb7482c3fd5a12dd6361bada1c1ea.png)
一、RCU有什么用?
![](https://i-blog.csdnimg.cn/blog_migrate/c31caf7f74efb7c0f1defa371e6dd9e5.png)
RCU主要用于对性能要求苛刻的并行实时计算。例如:天气预报、模拟核爆炸计算、内核同步等等。
假设你正在编写一个并行实时程序,该程序需要访问随时变化的数据。这些数据可能是随着温度、湿度的变化而逐渐变化的大气压。这个程序的实时响应要求是如此严格,需要处理的数据量如此巨大,以至于不允许任何自旋或者阻塞,因此不能使用任何锁。
幸运的是,温度和压力的范围通常变化不大,因此使用默认的数据集也是可行的。当温度、湿度和压力抖动时,有必要使用实时数据。但是温度、湿度和压力是逐渐变化的,我们可以在几分钟内更新数据,但没必要实时更新值。
在这种情况下,可以使用一个全局指针,即gptr,通常为NULL,表示要使用默认值。偶尔也可以将gptr指向假设命名为a、b和c的变量,以反映气压的变化。
传统的软件可以使用自旋锁这样的同步机制,来保护gptr指针的读写。一旦旧的值不被使用,就可以将旧指针指向的数据释放。这种简单的方法有一个最大的问题:它会使软件效率下降数个数量级(注意,不是下降数倍而是下降数个数量级)。
在现代计算系统中,向gptr写入a、b、c这样的值,并发的读者要么看到一个NULL指针要么看到指向新结构gptr的指针,不会看到中间结果。也就是说,对于指针赋值来说,某种意义上这种赋值是原子的。读者不会看到a、b、c之外的其他结果。并且,更好的一点,也是更重要的一点是:读者不需要使用任何代价高昂的同步原语,因此这种方法非常适合于实时使用。
真正的难点在于:在读者获得gptr的引用时,它可能看到a、b、c这三个值中任意一个值,写者何时才能安全的释放a、b、c所指向的内存数据结构?
引用计数的方案很有诱惑力,但正如锁和顺序锁一样,引用计数可能消耗掉数百个CPU指令周期,更致命的是,它会引用缓存行在CPU之间的来回颠簸,破坏各个CPU的缓存,引起系统整体性能的下降。很明显,这种选择不是我们所期望的。
想要理解Linux经典RCU实现的读者,应当认真阅读下面这段话:
一种实现方法是,写者完全不知道有哪些读者存在。这种方法显然让读者的性能最佳,但留给写者的问题是:如何才能确定所有的老读者已经完成。
最简单的实现是:让线程不会被抢占,或者说,读者在读RCU数据期间不能被抢占。在这种不可抢占的环境中,每个线程将一直运行,直到它明确地和自愿地阻塞自己(现实世界确实有这样的操作系统,它由线程自己决定何时释放CPU。例如大名鼎鼎的Solaris操作系统)。这要求一个不能阻塞的无限循环将使该CPU在循环开始后无法用于任何其他目的,还要求还要求线程在持有自旋锁时禁止阻塞。否则会形成死锁。
这种方法的示意图下所示,图中的时间从顶部推移到底部,CPU 1的list_del()操作是RCU写者操作,CPU2、CPU3在读端读取list节点。
Linux经典RCU的概念即是如此。虽然这种方法在生产环境上的实现可能相当复杂,但是玩具实现却非常简单。
1 for_each_online_cpu(cpu)
2 run_on(cpu);
for_each_online_cpu()原语遍历所有CPU,run_on()函数导致当前线程在指定的CPU上执行,这会强制目标CPU执行上下文切换。因此,一旦for_each_online_cpu()完成,每个CPU都执行了一次上下文切换,这又保证了所有之前存在的读线程已经完成。
请注意,这个方法不能