14.3.3 自旋锁

基于原子操作,Nginx实现了一个自旋锁。自旋锁是一种非睡眠锁,也就是说,某进程如果试图获得自旋锁,当发现锁已经被其他进程获得时,那么不会使得当前进程进入睡眠状态,而是始终保持进程在可执行状态,每当内核调度到这个进程执行时就持续检查是否可以获取到锁。在拿不到锁时,这个进程的代码将会一直在自旋锁代码处执行,直到其他进程释放了锁且当前进程获取到了锁后,代码才会继续向下执行。

可见,自旋锁主要是为多处理器操作系统而设置的,它要解决的共享资源保护场景就是进程使用锁的时间非常短(如果锁的使用时间很久,自旋锁会不太合适,那么它会占用大量的CPU资源)。在14.6节和14.7节介绍的两种睡眠锁会导致进程进入睡眠状态。睡眠锁与非睡眠锁应用的场景不同,如果使用锁的进程不太希望自己进入睡眠状态,特别它处理的是非常核心的事件时,这时就应该使用自旋锁,其实大部分情况下Nginx的worker进程最好都不要进入睡眠状态,因为它非常繁忙,在这个进程的epoll上可能会有十万甚至百万的TCP连接等待着处理,进程一旦睡眠后必须等待其他事件的唤醒,这中间极其频繁的进程间切换带来的负载消耗可能无法让用户接受。

注意 自旋锁对于单处理器操作系统来说一样是有效的,不进入睡眠状态并不意味着其他可执行状态的进程得不到执行。Linux内核中对于每个处理器都有一个运行队列,自旋锁可以仅仅调整当前进程在运行队列中的顺序,或者调整进程的时间片,这都会为当前处理器上的其他进程提供被调度的机会,以使得锁被其他进程释放。

用户可以从锁的使用时间长短角度来选择使用哪一种锁。当锁的使用时间很短时,使用自旋锁非常合适,尤其是对于现在普遍存在的多核处理器来说,这样的开销最小。而如果锁的使用时间很长时,那么一旦进程拿不到锁就不应该再执行任何操作了,这时应该使用睡眠锁将系统资源释放给其他进程使用。另外,如果进程拿不到锁,可能只会导致某一类请求(不是进程上的所有请求)不能继续执行,而epoll上的其他请求还是可以执行的,这时应该选用非阻塞的互斥锁,而不能使用自旋锁。

下面介绍基于原子操作的自旋锁方法ngx_spinlock是如何实现的。它有3个参数,其中,lock参数就是原子变量表达的锁,当lock值为0时表示锁是被释放的,而lock值不为0时则表示锁已经被某个进程持有了;value参数表示希望当锁没有被任何进程持有时(也就是lock值为0),把lock值设为value表示当前进程持有了锁;第三个参数spin表示在多处理器系统内,当ngx_spinlock方法没有拿到锁时,当前进程在内核的一次调度中,该方法等待其他处理器释放锁的时间。下面来看一下它的源代码。


void ngx_spinlock(ngx_atomic_t*lock,ngx_atomic_int_t value,ngx_uint_t spin)

{

ngx_uint_t i,n;

//无法获取锁时进程的代码将一直在这个循环中执行

for(;){

//lock为0时表示锁是没有被其他进程持有的,这时将lock值设为value参数表示当前进程持

有了锁

if(*lock==0&&ngx_atomic_cmp_set(lock,0,value)){

//获取到锁后ngx_spinlock方法才会返回

return;

}

//ngx_ncpu是处理器的个数,当它大于1时表示处于多处理器系统中

if(ngx_ncpu>1){

/在多处理器下,更好的做法是当前进程不要立刻“让出”正在使用的CPU处理器,而是等待一段时间,看看其他处理器上的进程是否会释放锁,这会减少进程间切换的次数/

for(n=1;n<spin;n<<=1){

/注意,随着等待的次数越来越多,实际去检查lock是否释放的频繁会越来越小。为什么会这样呢?因为检查lock值更消耗CPU,而执行ngx_cpu_pause对于CPU的能耗来说是很省电的/

for(i=0;i<n;i++){

/ngx_cpu_pause是在许多架构体系中专门为了自旋锁而提供的指令,它会告诉CPU现在处于自旋锁等待状态,通常一些CPU会将自己置于节能状态,降低功耗。注意,在执行ngx_cpu_pause后,当前进程没有“让出”正使用的处理器/

ngx_cpu_pause();

}

/检查锁是否被释放了,如果lock值为0且释放了锁后,就把它的值设为value,当前进程持有锁成功并返回/

if(*lock==0&&ngx_atomic_cmp_set(lock,0,value)){

return;

}

}

}

/当前进程仍然处于可执行状态,但暂时“让出”处理器,使得处理器优先调度其他可执行状态的进程,这样,在进程被内核再次调度时,在for循环代码中可以期望其他进程释放锁。注意,不同的内核版本对于sched_yield系统调用的实现可能是不同的,但它们的目的都是暂时“让出”处理器/

ngx_sched_yield();

}


释放锁时需要Nginx模块通过ngx_atomic_cmp_set方法将原子变量lock值设为0。

可以看到,ngx_spinlock方法是非常高效的自旋锁,它充分考虑了单处理器和多处理器的系统,对于持有锁时间非常短的场景很有效率。