RCU锁在Linux内核的演变

2.6内核引入了RCU锁,这种锁十分高效,总的说来就是读时加锁,写时拷贝,,读后更新。具体的流程可以参照 rcu的相关文档。本文主要谈一下rcu在Linux2.6内核的演变过程,它分别经历了三个阶段,分别是传统rcu锁,可抢占rcu锁以及2.6.29 中将要引入的树形分层rcu锁。

Linux中最早引入的rcu锁十分的粗糙,实现原理也是非常简单,毕竟Linux中不管多复杂的机制一开始的时候都是十分简单的,这一点可以看看 Linux0.01到Linux2.6.28的演变。传统的rcu锁就是在有读者读数据的时候加上rcu锁,注意这里的“加锁”仅仅是逻辑意义上的加锁, 至于真正实现可以很灵活,实际上Linux的实现就没有用到所谓的锁,而是简单的禁用抢占。为何可以这么实现呢?因为在一个cpu上禁用了抢占也就禁用了 这个cpu上的进程切换,该cpu上的当前进程将一直运行,除非重新使能抢占,那么什么时候使能抢占呢?当然是在读者释放rcu锁的时候了,而Linux 中传统rcu实现规定在进程切换的时候才会运行更新回调函数,这就是本质。每当一个cpu经历进程上下文切换时,rcu就说此cpu经过了一个 quiescent state,而所有的cpu都经过一个quiescent state的时间称为一个grace period,在这个grace period点之后,该grace period的更新回调函数就可以被安全的执行了,因为此时,系统已经确定所有的读者都不再持有rcu锁了。我们再次理一下这是为什么,读者释放锁就是使 能抢占,也就是可以切换进程,而且换进程的时候rcu就认为经过了一个quiescent state,所有的cpu都经过了一个quiescent state意味着所有的cpu都可以进程切换了,就是意味着所有的cpu上的读者都不再持有rcu锁了。就是这么简单,但是我们看一下这个实现会存在什么 问题,只要读者加锁,就意味着禁用抢占,就是禁止切换,这会很大地减低系统的交互性能,当然也会降低实时任务的性能,这一切都不是想要的,单独一个rcu 把大家都拖下水实在不应该,难道要为了保护读者保护的数据而冻住整个系统不让切换进程吗?传统的rcu不允许占有rcu的进程睡眠,睡眠了进程又不允许切 换,系统就相当于死掉了。于是下一个版本的rcu就要出来了,它试图将rcu对系统的影响减小到最小,也就是说将rcu单独做成一个可以自洽自治的模块, 它本身就可以处理好数据的保护而不再需要通过系统其它的机制来确保rcu读者数据在允许更新前被保护,这就是preemptible-RCU锁。其实 Linux最开始引入的所谓传统的rcu锁是一个很失败的实现,操作系统的任务就是管理各种复杂的模块以模拟真实世界,优秀的操作系统可以做到模块化的管 理各种机制,比如进程,内存,设备,以及各种锁和并发机制等等,在管理它们的时候各个模块低耦合地通信而不至于互相依赖互相影响性能,如果说要想不出问 题,最简单也是最失败的方式就是只让一个进程跑,没有切换没有共享当然也就没有了竞争,没有竞争也就意味着没有竞态,也就意味着没有了并发问题,这很简 单,但是很不灵活,也让人感觉到这不是一个优秀的操作系统,就好像人不能说怕淋雨就不出门,正确的解决方案就是发明出雨伞和雨衣。Linux第一代rcu 就是这种很差劲的实现。

既然第二代rcu是preemptible的rcu,那么就是说在持有rcu锁期间不再需要禁用抢占了,这个实现看到这里就知道它是一个很优秀的实现,因 为它不再需要抢占机制帮忙来实现数据保护,rcu的内部机制已经可以实现数据保护了。那么它是怎么实现的呢?如果你不想用别的机制实现rcu数据保护,那 么就要自己实现,当然代价就是引入新的数据结构和逻辑控制机制,在preemptible-rcu中,一个grace period被分解为了两个阶段而不是所有cpu完成quiescent state了所有的数据保护机制都是在这两个阶段大做文章而实现的。这两个阶段通过一系列的软件计数器来实现了rcu,之所以分为两个阶段是因为rcu有 lock和unlock两个动作,我们能不能像实现传统rcu一样,不用真正的锁就实现逻辑上的rcu锁呢?当然可以了,引入一个每cpu变量:

#define GP_STAGES 2 struct rcu_data { spinlock_t lock; //保护此结构中字段的自旋锁 long completed; //总的阶段计数器 int waitlistcount; struct rcu_head *nextlist; //一些链表,记录更新回调函数 struct rcu_head **nexttail; struct rcu_head *waitlist[GP_STAGES]; struct rcu_head **waittail[GP_STAGES]; struct rcu_head *donelist; struct rcu_head **donetail; long rcu_flipctr[2]; //这个很重要 struct rcu_head *nextschedlist; struct rcu_head **nextschedtail; struct rcu_head *waitschedlist; struct rcu_head **waitschedtail; int rcu_sched_sleeping; }; rcu_flipctr 这个字段十分重要,它是一个数组,每个元素其实就是一个计数器,第一个元素记录的是在本阶段中的本cpu的rcu锁持有者数量,而第二个元素表示上个阶段 的还没有释放的本cpu的rcu锁数量,注意,只有unlock操作可以递减第二个元素,lock操作只能递增第一个元素,另外unlock操作也可以递 减第一个元素,一旦所有cpu的第二个元素的和变为0了,那么就可以向前推进一个阶段了,在下一个阶段中,第一个元素成了只有unlock才能递减的元 素,而第二个元素lock和unlock都可触及,记录着当前阶段的新rcu锁数量,往下依次类推。每个阶段都要等待所有cpu的rcu_flipctr 的同一个元素的和成为0,然后进入下一个阶段,进入下一个阶段同时交换rcu_flipctr的两个元素的意义。系统维持一个状态机,为rcu设置了好几 种状态,其中包括一个等待所有cpu的前一个阶段的rcu计数器降到0的状态,这一个状态过去以后,rcu状态机就可以向前推进一个阶段了,注意,可抢占 的rcu锁机制的重点执行点不再是grace period,而是一个grace period的两个阶段,可抢占的rcu锁也是以阶段为基准的,也就是说,在每一个阶段结束时都会有一个计数器的和降为0,这时同步将这个计数器对应的回 调函数链表推进到最前面,然后就可以安全地执行这些更新回调函数了,这里执行回调函数链表的时间是每个阶段结束,而不必等到一个grace period的第二个阶段结束时。原先的传统rcu的实现在机进程切换时更新quiescent state状态然后判断是否过了一个grace period,现在的可抢占的rcu中,仅在时钟中断里面执行rcu状态机,状态机根据其前一个状态和当前的rcu状态采取不同的措施以维持状态机继续运 转,这么看来可抢占的rcu就和抢占没有关系了,cpu可以随便被抢占而不会破坏rcu读者保护的数据,于是就说rcu模块和抢占模块解耦合了,各自可以 通过各自的方式进行设计,调试,优化以提高性能而不用像原来那样互相依赖杂糅在一起。 我们看一下lock和unlock函数: void __rcu_read_lock(void) { … } else { unsigned long flags; local_irq_save(flags); idx = ACCESS_ONCE(rcu_ctrlblk.completed) & 0x1; ACCESS_ONCE(RCU_DATA_ME()->rcu_flipctr[idx])++; ACCESS_ONCE(t->rcu_read_lock_nesting) = nesting + 1; ACCESS_ONCE(t->rcu_flipctr_idx) = idx; local_irq_restore(flags); } } void __rcu_read_unlock(void) { … } else { unsigned long flags; local_irq_save(flags); idx = ACCESS_ONCE(t->rcu_flipctr_idx); ACCESS_ONCE(t->rcu_read_lock_nesting) = nesting – 1; ACCESS_ONCE(RCU_DATA_ME()->rcu_flipctr[idx])–; local_irq_restore(flags); } } 然后看一幅图:

人生就是要感受美丽的、善良的,丑恶的、病态的。

RCU锁在Linux内核的演变

相关文章:

你感兴趣的文章:

标签云: