排队自旋锁

(译自http://lwn.net/Articles/267968/,作者Jonathan Corbet)
自旋锁是Linux内核中最底层的互斥机制。因此,它们对内核的安全和性能有很大的影响,人们花很大力气去优化各种自旋锁的实现(不同的硬件体系结构会有不同的实现)也就不足为奇了。但是我们的优化之路还没有走到终点,一个合并到2.6.25版本内核的patch告诉我们能做的还有很多。
在x86架构上的2.6.24版本内核中,自旋锁用一个整数来表示。当为1时表示这个锁是可被获得的。spin_lock()函数被调用时,首先原子地对这个整数减1,然后看结果是否为0;如果为0,则锁被调用者获得;如果为负数,这说明这个锁已经被别人得到了,然后它就会进入忙等/自旋状态,一直循环到锁的值变为正,然后再从头开始,再次尝试获得锁。
当临界区的代码执行完毕后,锁的所有者会将锁的值设为1来释放这个锁。
这种实现效率很高,尤其在没有竞争的情况下(实际在大多数情况下都没有竞争)。想要看锁的争用有多激烈也很简单——锁的值越小,对锁进行争用的处理器越多。但是这种方法有一个弊端:它并不公平。当锁被释放后,第一个对这个锁执行减1操作的处理器将会获得这个锁。我们并不能保证等待时间最长的处理器获得这个锁;实际上,刚刚释放了锁的处理器更容易再次获得这个锁,因为这个锁正好在它的cache中。
有些人希望锁的不公平不会造成问题;在许多情况下,当锁的争用很激烈时,即使不考虑锁的公平性,争用也会产生性能问题。Nick Piggin最近重新思考了这个问题,他注意到:

对于一个8核Opteron处理器,自旋锁的不公平性是无法忽视的,在用户空间对锁的测试中,不同的线程的运行时间可能会相差两倍,有些线程在尝试1000000次后才能获得锁。

这样的运行时间的差异自然是无法接受的。锁的不公平性也会造成延迟问题,因为获得锁的时间可能任意长,也就无法对延迟长短做一个保证了。
Nick的回应是一种新的自旋锁实现,他将其称为“排队自旋锁”。在它的最初版本中,自旋锁占用16位,分为两个字节,分别是“Next”字段和“Owner”字段。
每个字节都可以认为是一个入场号,就像商店里的叫号机,Next字段表示叫号机即将打印分发的号,Owner字段表示正在窗口前服务的号,商店的做法能保证顾客按照到达的顺序被提供服务。
因此,在新方案中,锁的初始值(两部分)被初始化为0。spin_lock()函数首先记录下锁的值,然后将Next字段加1——一次原子操作就能完成这些事情。如果Next字段在加1之前等于Owner字段,锁就会被当前处理器获得,剩余工作就能继续进行。否则处理器就会自旋,直到Owner字段增长到正确的值(译注:即函数一开始记录下的Next字段的值)。在这种方案中,释放锁时只需要将Owner字段加1即可。
上述的实现也有一些小小的弊端,那就是处理器的数目不能超过256,处理器数量超过这个值的话,当发生激烈地锁争用时,不同的处理器可能会获得相同的排队号。由此产生的潜在问题是不可容忍的,在很多大型系统中,处理器数量早已超过了256。因此有一个附加的“big ticket”补丁也被合并到2.6.25版本内核中,它使用一个16位的排队号,这使处理器数目上限达到了65536。
在旧版本的自旋锁实现中,发生锁争用时所有处理器都要去抢这个锁。现在他们可以按照到达的顺序排队等候来获得这个锁了。多个线程的运行时间变得平均了,最大延迟也减小了(而且延迟也变得确定而不是任意长了)。Nick认为,新实现可能会带来轻微的开销,但是这个开销在当代的处理器上非常小,而且相对于发生锁争用时出现的cache不命中带来的开销可以忽略。内核x86维护者们显然认为消除激烈争用带来的收益高于这小小的开销,别人大概也会认可吧。

猜你喜欢

转载自blog.csdn.net/huanchankuang3257/article/details/82951827