0 前言
之前转载过一篇阐述spin_lock的博文,但始终理解不深入,记忆不深刻。今天,花了半天阅读完LDD3的相关章节,有种顿悟的感觉,遂简要记下自己的理解。Btw,LDD3真心是字字珠玑,没有半句废话,而且从这边书可以看出外国人写书非常注重前后的呼应,而国内大多数书都是就事论事,没有体系的感觉,也难以启发人深入思考。(一不小心又吐嘈了一遍,罪过罪过……)另外,关于LDD3真心适合有一定基础的人阅读和思考。
1 正文
为什么设计spin_lock?
我认为主要是考虑到使用sem的机制去保护临界区,实时性太差,不能满足对实时性有严格要求的场景。子问题,为什么实时性低,这还要从sem实现原理上分析,sem如果不允许进入临界区,那么会放弃CPU切换到其它进程,待进入临界区的进程释放锁后,再切换到该进程。那么一来一回切换进程的时间开销,对于实时性来说是种伤害。所以也就有了需要设计一套具有高实时性且能够支持对临界区保护的机制方法来取带sem的机制。
Spin_lock是怎么做到的?
Spin_lock实现的方法,既然需要提高实时性,那么试图进入临界区的进程必然不能放弃CPU,它需要不停地去查找询问CPU是否可以进入,以便第一时间进入。所以spin_lock就是用这么一种busy_loop的方式去申请进入。当然这肯定会带来CPU的使用率下降,但是我们目前需要的实时性,这种sem不能带来给我们的特点,牺牲点CPU性能又有什么关系。况且它在使用建议上做了一定约束(后文会提到),做到最大化地减小这种busy loop带来的影响。
如何设计实现Spin_lock?
虽然上面说的非常简单,就是简单的busy_loop,但是很多细节需要考虑,为应用者提出了新的要求,不比使用sem简单。
首先,我们从处理器架构上考虑,如果是多核系统,处理器核1运行的进程p1在t1时刻进入了临界区,此时恰巧处理器核2上运行的进程p2在t2时刻,时间方面t1<t2<t1+临界区运行时间t3,那么p2只需要静静等待,同时不停try即可,待p1出了临界区,p2会得到立即响应,非常完美。但是,这里有一个非常重要的假设,整个系统运行在多核处理上,且p1与p2这两个近乎同一时间需要访问临界区的进程又运行在两个不同的处理器上,这是多么的巧合啊。那我们随便考虑一个场景在单核上运行的抢占式调度策略的内核,那么假设p1进入临界区,在还没有退出时,p2由于优先级更高,进行抢占,获得了CPU资源,不停在busy-loop,整个系统锁死,尴尬!
为了解决上述问题,一方面,我们要剥夺进入spin_lock的进程运行所在的CPU被抢占的权利(linux内核本质上是可以被抢占的),注意强调只需要禁止进入临界区进程所运行的CPU被抢占即可。另一方面,我们不能主动放弃CPU,虽然不能被内核其它进程抢占,但如果该进程主动释放CPU请求调度,那也是不行的。前者,可以由spin_lock的实现者完成,而后者则是他们对应用的人提出的需求,即不允许spin_lock保护的临界区代码有任何休眠释放CPU的操作,例如copy_from_user、kmalloc等存在风险。
因此为了解决死锁,是需要设计者和开发者共同努力完成的。
然后,上述我们只是从处理器架构和内核调度进程策略上分析的结果。接着,我们要从中断的角度出发去挖掘新的约束。假设进程p1进入临界区,此时发生中断,而中断服务程序也需要进入临界区,而中断服务只可能被中断,所以中断永远不可能进入临界区,再次死锁。So…解决办法还是由spin_lock的设计人员来完成,它们会禁止该CPU的中断功能,同样需要注意,这里是以CPU为单位,而不是所有CPU中断会被禁止。
这里稍微总结下:为了实现可用的不会出现死锁现象的spin_lock,spin_lock的实现者做了两件事情,一是禁止了进入临界区进程被剥夺的权利,二是禁止了运行该进程所在CPU的中断。而spin_lock的使用者,则需要保证临界区代码不调用可能放弃CPU的函数。
如何提高spin_lock的效率?
如上所述可以确定由于spin_lock的使用会浪费CPU资源(因为busy_loop),所以为了尽可能地消除负面影响,要求使用者spin_lock所保护的临界区代码尽可能精炼简单。