linux内核--自旋锁的理解

自旋锁:如果内核配置为SMP系统,自旋锁就按SMP系统上的要求来实现真正的自旋等待,但是对于UP系统,自旋锁仅做抢占和中断操作,没有实现真正的“自旋” 。如果配置了CONFIG_DEBUG_SPINLOCK,那么自旋锁按照SMP系统来编译 。
但是为什么在UP系统中不需要真正的“带有自旋的”自旋锁呢?其实在理解了自旋锁的概念和由来,这个问题就迎刃而解了 。所以我重新查找了关于自旋锁的资料,认真研究了自旋锁的实现和相关内容 。
一、自旋锁spinlock的由来众所周知,自旋锁最初就是为了SMP系统设计的,实现在多处理器情况下保护临界区 。所以在SMP系统中,自旋锁的实现是完整的本来面目 。但是对于UP系统,自旋锁可以说是SMP版本的阉割版 。因为只有在SMP系统中的自旋锁才需要真正“自旋” 。
二、自旋锁的目的自旋锁的实现是为了保护一段短小的临界区操作代码,保证这个临界区的操作是原子的,从而避免并发的竞争冒险 。在linux内核中,自旋锁通常用于包含内核数据结构的操作,你可以看到在许多内核数据结构中都嵌入有spinlock,这些大部分就是用于保证它自身被操作的原子性,在操作这样的结构体时都经历这样的过程:上锁-操作-解锁 。
如果内核控制路径发现自旋锁“开着”(可以获取),就获取锁并继续自己的执行 。相反,如果内核控制路径发现锁由运行在另一个CPU上的内核控制路径“锁着”,就在原地“旋转”,反复执行一条紧凑的循环检测指令,直到锁被释放 。自旋锁是循环检测“忙等”,即等待时内核无事可做(除了浪费时间),进程在CPU上保持运行,所以它保护的临界区必须小,且操作过程必须短 。不过,自旋锁通常非常方便,因为很多内核资源只锁1毫秒的时间片段,所以等待自旋锁的释放不会消耗太多CPU的时间 。
三、自旋锁需要做的工作从保证临界区访问原子性的目的来考虑,自旋锁应该阻止在代码运行过程中出现的任何并发干扰 。这些“干扰”包括:
1、中断,包括硬件中断和软件中断(仅在中断代码可能访问临界区时需要)
这种干扰存在于任何系统中,一个中断的到来导致了中断例程的执行,如果在中断例程中访问了临界区,原子性就被打破了 。所以如果在某种中断例程中存在访问某个临界区的代码,那么就必须用spinlock保护 。对于不同的中断类型(硬件中断和软件中断)对应于不同版本的自旋锁实现,其中包含了中断禁用和开启的代码 。但是如果你保证没有中断代码会访问临界区,那么使用不带中断禁用的自旋锁API即可 。
2、内核抢占(仅存在于可抢占内核中)
在2.6以后的内核中,支持内核抢占,并且是可配置的 。这使UP系统和SMP类似,会出现内核态下的并发 。这种情况下进入临界区就需要避免因抢占造成的并发,所以解决的方法就是在加锁时禁用抢占(preempt_disable(); ),在开锁时开启抢占(preempt_enable();注意此时会执行一次抢占调度) 。
3、其他处理器对同一临界区的访问(仅SMP系统)
在SMP系统中,多个物理处理器同时工作,导致可能有多个进程物理上的并发 。这样就需要在内存加一个标志,每个需要进入临界区的代码都必须检查这个标志,看是否有进程已经在这个临界区中 。这种情况下检查标志的代码也必须保证原子和快速,这就要求必须精细地实现,正常情况下每个构架都有自己的汇编实现方案,保证检查的原子性 。
有些人会以为自旋锁的自旋检测可以用for实现,这种想法“Too young, too simple, sometimes naive”!你可以在理论上用C去解释,但是如果用for,起码会有如下两个问题:
(1)你如何保证在SMP下其他处理器不会同时访问同一个的标志呢?(也就是标志的独占访问)
(2)必须保证每个处理器都不会去读取高速缓存而是真正的内存中的标志(可以实现,编程上可以用volitale) 要根本解决这个问题,需要在芯片底层实现物理上的内存地址独占访问,并且在实现上使用特殊的汇编指令访问 。请看参考资料中对于自旋锁的实现分析 。以arm为例,从存在SMP的ARM构架指令集开始(V6、V7),采用LDREX和STREX指令实现真正的自旋等待 。
四、自旋锁操作组成根据上的介绍,我们很容易知道自旋锁的组成: