在非常早曾经,大概是2009年的时候。写过一篇关于Linux RCU锁的文章《RCU锁在linux内核的演变》,如今我承认。那个时候我尽管懂了RCU锁,可是我没有能力用一种非常easy的描写叙述把Linux的实现给展示出来,有道是你能给别人用你自己的方式非常简洁地描写叙述清楚,你才是真正的精通它。否则,无异于背诵。换个说法,假设你在被面试。在短时间内靠嘴说给面试官,且他还要能听明白,就说明自己真的懂了,这样的时候,是不会给你机会分析源码的,也不可能让你背诵源码。
       时隔五年多,最近又碰到了这个话题,我不能自诩自己对RCU锁是多么精通,可是起码,和2009年相比,我确实有所进步,因此在这个台风肆虐的次日,我尝试着用我自己的方式描写叙述一下Linux对RCU锁的一种实现方式,作为《RCU锁在linux内核的演变》这篇文章的补充。本文不配图,没代码,仅仅是文字。
声明:假设你还不知道RCU锁是什么。请自行baidu,本文不再赘述概念。可是这也就等于说,假设我自己有一天忘记了RCU。我也不能指望从本文中得到不论什么帮助。我总是这样,不是吗?能力在忘不在记。得其义而忘其形。

RCU要素

RCU锁的要素包含

读标志

假设一个Reader企图占领一把RCU锁,它是不须要付出不论什么代价的,仅仅须要设置一个标志,让外界知道有Reader在占领这把RCU锁。多个Reader能够共同持有一把RCU锁。

写时拷贝

假设有一个Write企图更新RCU锁所保护的数据,那么它会首先查看该RCU锁的读标志,假设有该标志,说明有最少一个Reader持有了该RCU锁。它须要对原始数据make a copy,写这个副本并将更新过的副本保存在某处,等待时机用该副本更新原始数据。

更新时机

这个时机就是用副本更新原始数据的时间点。这个时间点怎样确定是RCU锁实现的算法核心,它直接能够确定全部的数据结构。确切来讲,Writer必须waitting for all readers leaving,方可Update原始数据,问题是,它是怎么知道全部的Reader都离开了呢?

Linux内核对RCU锁的几种实现

1.原始实现-利用抢占禁止

Linux内核于2.6内核引入了RCU锁的概念。在第一个版本号中,它利用了抢占禁止的方式来标志有Reader持有RCU锁,这意味着期间不能发生task切换(指的是task_struct所代表的sched entity切换)。

那么全部Reader均已经释放RCU锁的标志就是。task切换了,因此非常easy,用副本Update原始数据的时机就是task切换时。
       全部的Write会将自己写的副本挂在一个list上,在task切换的时候会touch这个list,假设该list非空,则遍历每个元素。Update原始数据。

评价[该部分与实现无关,纯形而上的,能够忽略]

这就是第一版的原始实现。它是否合理姑且不论。确实。它能够工作。可是:
a.它这样的实现是否会影响调度子系统的时延
b.因为禁用抢占,抢占粒度变粗,对交互性是否会有影响
c.对CPU间的task负载均衡的影响呢
我们发现,因为RCU的这个实现不是靠自身机制实现的,它不可避免地会影响到系统的核心机制。比方调度,负载均衡等,这意味着它不能长久。也无法经历复杂的演变。因为随着它在这条路上的逐步演进,对系统核心机制的影响将越来越大,故而,它必须从系统层面剥离出来。确实,它也是这么做的。这就是第二代RCU实现-可抢占RCU锁。

2.新实现-利用阶段计数器

须要一种更加有效的方式来标志Reader已经持有锁-第一要素读标志,而且这个标志要尽可能精确,且不能使用系统核心的机制,要做成全然封闭的闭环,不依靠外部当然也就不会影响外部。
       纯天然的想法就是使用计数器,每一把RCU持有一个Reader计数器,一旦有Reader前来持锁。仅仅须要一个原子操作,将该计数器加1就可以,Writer写数据时。发现计数器不是0就意味着须要make a copy了-第二要素写时拷贝(COW-Copy On Write)。

如今的问题是第三要素,Writer怎么知道全部的Reade都已经将锁释放了呢??
       纯天然的想法就是在某个Reader释放锁的时候,计数器减1。当计数器又一次变为0的时候。这就是副本更新原始数据的时机。

确实是这样,可是依照持锁和解锁的分布看,它们应该是均等的,这意味着计数器的值会在一个期望值上下波动,变成0的希望及其渺茫,因此须要引入还有一个參量,即阶段。
       将唯一的那个RCU计数器分裂为两个计数器:old readers和new readers。

太初,任选某一个时刻,将RCU锁当前的计数器(称为原始计数器)值复制一份存入old readers,计数器清0,原始计数器改称为new readers。复制结束的当下,new readers计数器为0,old readers计数器为现阶段持有锁的reader的数量。而且持锁者task(即task_struct)与RCU锁之间保持关联(难道不是一个task_struct字段能够搞定的吗?)。task永远知道自己是new reader还是old reader。

此时,就能够明白定义lock和unlock的行为了:
lock--设置自己的task为new reader,将RCU的new reader计数器加1。

unlock--获取自己的task是new reader还是old reader,将自己所在的reader计数器减1。
此时非常明白的事实是。old reader计数器总是会递减而不会递增,而new reader不但会递增也会递减。这样,选择Update的时机也非常明白了,那就是,old reader计数器变为0,这个时刻,就该将全部的副本覆盖原始数据了。
       如今总结全部的三个要素:

读标志

为该RCU锁的new reader计数器加1

写时拷贝

假设该RCU锁的old reader计数器不为0,则运行写时复制。

更新时机

每次unlock操作,都会将本task的reader计数器(或者是new reader。或者是old reader)减1。一旦该RCU锁的old reader计数器变成0。则运行全部的Update操作。

评价[该部分与实现无关,纯形而上的。能够忽略]

持有RCU锁的reader。能够睡眠,能够被抢占。能够调度到别的CPU上,全然是封闭的,和系统其他的机制无关。

然而。我一直在思考一个更好的实现,仅仅因疯子不给力。!

3.RCU Tree实现(实在是没有2好)

今天实在没有时间了,要出去。兴许补充。

05-11 15:10