这里并不是讲spinlock的一般用法,只是提出自己曾经的一些疑问点,也给出自己的解答。
在《LINUX内核设计与实现》《LINUX设备驱动程序》里都提过,在非抢占式的单处理器系统上的自旋锁被优化为不做任何事情。我最初的疑问是即使是单处理器,没有抢占能力,但时间片用完,持有锁的一方终会交出锁,等待锁的一方不会永远自旋下去。但在《linux设备驱动程序》里面明确说到,“如果非抢占式的单处理器系统进入某个锁上的自旋状态,则会永远自旋下去;也就是说,没有任何其他线程能够获得CPU来释放这个锁”。
为了说明问题,还是有必要先说明一下在SMP(多处理器)或抢占式下就需要真正的自旋锁。
一、SMP下
CPU A对公共资源执行一些操作,在这个过程中随时CPU B也可以对公共资源执行操作。
如下面的代码片断:
if (NULL == buf)
buf = kmalloc(size, GFP_KERNEL);
当CPU A,CPU B都执行了if (NULL == buf),发现buf的确为NULL,那么都会进行内存分配,但是只有一个可以被buf记录(最后执行赋值那个)。这样会造成另一部内存无法回到系统。
所以SMP上,需要锁实现。
二、抢占式
在2.4及以前的linux内核均不是抢占式内核,是在2.6正式引入的。按以前的进程调度,如果一个进程在不耗光自己时间片前,不主动退出是不会交出控制权的,很明显这样一些实时性要求很高的进程无法得到满足,所以需要抢占。
如下图所示,进程A即使被中断打断了,后面还是会回到进程A,但进程B可能更需要马上执行,如用户敲了一个字符。
如果是抢占式,在中断后进程调度器可以调度更高优先级的进程执行,如下所示:
从上面可以看出,进程A可能随时会被B所抢占,即是在单处理器上也会造成SMP同样的并发问题,所以需要锁实现。
有了上面的介绍后,我们回到原来的问题:在非抢占式的单处理器系统上的自旋锁被优化为不做任何事情。
上面是linux2.6.10的非SMP下spinlock的内核代码,虽然非抢占也调用了preempt_disable();(禁用内核抢占),但进去后会发现非抢占定义为#define preempt_disable() do { } while (0)
spinlock最初就是为多处理器系统设计的。这里有一条很重要的原则,上面两本书都没有提到过,那就是不能在可能交出CPU控制权的代码上运行,被调度,休眠都不可以。
内核从2.6开始就支持内核抢占,对于非内核抢占系统,内核代码可以一直执行,直到完成,也就是说当进程处于内核态时,是不能被抢占的(当然,运行于内核态的进程可以主动放弃CPU,比如,在系统调用服务例程中,由于内核代码由于等待资源而放弃CPU,这种情况叫做计划性进程切换(planned process switch))。但是,对于由异步事件(比如中断)引起的进程切换,抢占式内核与非抢占式是有区别的,对于前者叫做强制性进程切换(forced process switch)。 那么单处理器非抢占内核下,内核进程是可以不主动交出CPU的。
在中断使用spinlock,如果其他中断可能使用你的保护区域,那么可以禁止本地中断,即同一CPU的中断。那为什么不用禁止其他CPU的中断呢,假使另一CPU尝试持有锁而自旋,但由于是不同CPU,所以持有锁的CPU总会执行完,所以不会永远自旋下去。但是本地中断打断后自旋,那么将无法恢复,因为持有锁的代码永远没有机会释放锁。