14.8.2 原子变量实现的ngx_shmtx_t锁

当Nginx判断当前操作系统支持原子变量时,将会优先使用原子变量实现表14-1中的5种方法(即原子变量锁的优先级高于文件锁)。不过,同时还需要判断其是否支持信号量,因为支持信号量后进程有可能进入睡眠状态。下面介绍一下如何使用原子变量和信号量来实现ngx_shmtx_t互斥锁,注意,它比文件锁的实现要复杂许多。

ngx_shmtx_t结构中的lock原子变量表示当前锁的状态。为了便于理解,我们还是用接近自然语言的方式来说明这个锁,当lock值为0或者正数时表示没有进程持有锁;当lock值为负数时表示有进程正持有锁(这里的正、负数仅相对于32位系统下有符号的整型变量)。Nginx是怎样快速判断lock值为“正数”或者“负数”的呢?很简单,因为有符号整型的最高位是用于表示符号的,其中0表示正数,1表示负数,所以,在确定整型val是负数或者正数时,可通过判断(val&0x80000000)==0语句的真假进行。

下面看一下初始化ngx_shmtx_t互斥锁的ngx_shmtx_create方法究竟做了些什么事情。


ngx_int_t ngx_shmtx_create(ngx_shmtx_tmtx,voidaddr,u_char*name)

{

mtx->lock=addr;

//注意,当spin值为-1时,表示不能使用信号量,这时直接返回成功

if(mtx->spin==(ngx_uint_t)-1){

return NGX_OK;

}

//spin值默认为2048

mtx->spin=2048;

//同时使用信号量

if(NGX_HAVE_POSIX_SEM)

//以多进程使用的方式初始化sem信号量,sem初始值为0

if(sem_init(&mtx->sem,1,0)==-1){

ngx_log_error(NGX_LOG_ALERT,ngx_cycle->log,ngx_errno,

"sem_init()failed");

}else{

//在信号量初始化成功后,设置semaphore标志位为1

mtx->semaphore=1;

}

endif

return NGX_OK;

}


spin和semaphore成员都将决定ngx_shmtx_lock阻塞锁的行为。

ngx_shmtx_destory方法的唯一目的就是释放信号量,如下所示。


void ngx_shmtx_destory(ngx_shmtx_t*mtx)

{

//支持信号量时才有代码需要执行

if(NGX_HAVE_POSIX_SEM)

/当这把锁的spin值不为(ngx_uint_t)-1时,且初始化信号量成功,semaphore标志位才为1/

if(mtx->semaphore){

//销毁信号量

if(sem_destroy(&mtx->sem)==-1){

ngx_log_error(NGX_LOG_ALERT,ngx_cycle->log,ngx_errno,"sem_destroy()failed");

}

}

endif

}


以非阻塞方式获取锁的ngx_shmtx_trylock方法较为简单,可直接判断lock原子变量的值,当它为非负数时,直接将其置为负数即表示持有锁成功。怎样把0或者正数置为负数呢?很简单,使用语句val|0x80000000即可把非负数的val变为负数,这种方法效率最高,即直接修改val的最高符号标志位为1。


ngx_uint_t ngx_shmtx_trylock(ngx_shmtx_t*mtx)

{

ngx_atomic_uint_t val;

//取出lock锁的值,通过判断它是否为非负数来确定锁状态

val=*mtx->lock;

/如果val为0或者正数,则说明没有进程持有锁,这时调用ngx_atomic_cmp_set方法将lock锁改为负数,表示当前进程持有了互斥锁/

return((val&0x80000000)==0&&

ngx_atomic_cmp_set(mtx->lock,val,val|0x80000000));

}


注意(val&0x80000000)==0是一行语句,而ngx_atomic_cmp_set(mtx->lock,val,val|0x80000000)又是一行语句,多进程的Nginx服务将有可能出现虽然第1行语句执行成功(表示锁未被任何进程持有),但在执行第2行语句前,又有一个进程拿到了锁,这时第2行语句将会执行失败。这正是ngx_atomic_cmp_set方法自身先判断lock值是否为非负数val的原因,只有lock值为非负数val,它才会确定将lock值赋为负数val|0x80000000并返回1,否则返回0(详见14.3.2节)。

阻塞式获取互斥锁的ngx_shmtx_lock方法较为复杂,在不支持信号量时它与14.3.3节介绍的自旋锁几乎完全相同,但在支持了信号量后,它将有可能使进程进入睡眠状态。下面我们分析一下它的操作步骤。


void ngx_shmtx_lock(ngx_shmtx_t*mtx)

{

ngx_uint_t i,n;

ngx_atomic_uint_t val;

//没有拿到锁之前是不会跳出循环的

for(;){

/lock值是当前的锁状态。注意,lock一般是在共享内存中的,它可能会时刻变化,而val是当前进程的栈中变量,下面代码的执行中它可能与lock值不一致/

val=*mtx->lock;

/如果val为非负数,则说明锁未被持有。下面试图通过修改lock值为负数来持有锁/

if((val&0x80000000)==0

&&ngx_atomic_cmp_set(mtx->lock,val,val|0x80000000))

{

/*在成功地将lock值由原先的val改为非负数后,表示成功地持有了锁,ngx_shmtx_lock

方法结束*/

return;

}

//仅在多处理器状态下spin值才有意义,否则PAUSE指令是不会执行的

if(ngx_ncpu>1){

//循环执行PAUSE,检查锁是否已经释放

for(n=1;n<mtx->spin;n<<=1){

//随着长时间没有获得到锁,将会执行更多次PAUSE才会检查锁

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

//对于多处理器系统,执行ngx_cpu_pause可以降低功耗

ngx_cpu_pause();

}

//再次由共享内存中获得lock原子变量的值

val=*mtx->lock;

/检查lock是否已经为非负数,即锁是否已经被释放,如果锁已经释放,那么会通过将lock原子变量值设置为负数来表示当前进程持有了锁/

if((val&0x80000000)==0

&&ngx_atomic_cmp_set(mtx->lock,val,val|0x80000000))

{

//持有锁成功后立刻返回

return;

}

}

}

//支持信号量时才继续执行

if(NGX_HAVE_POSIX_SEM)

//semaphore标志位为1才使用信号量

if(mtx->semaphore){

//重新获取一次可能在共享内存中的lock原子变量

val=*mtx->lock;

//如果lock值为负数,则lock值加上1

if((val&0x80000000)

&&ngx_atomic_cmp_set(mtx->lock,val,val+1))

{

/检查信号量sem的值,如果sem值为正数,则sem值减1,表示拿到了信号量互斥锁,同时sem_wait方法返回0。如果sem值为0或者负数,则当前进程进入睡眠状态,等待其他进程使用ngx_shmtx_unlock方法释放锁(等待sem信号量变为正数),到时Linux内核会重新调度当前进程,继续检查sem值是否为正,重复以上流程/

while(sem_wait(&mtx->sem)==-1){

ngx_err_t err;

err=ngx_errno;

//当EINTR信号出现时,表示sem_wait只是被打断,并不是出错

if(err!=NGX_EINTR){

break;

}

}

}

//循环检查lock锁的值,注意,当使用信号量后不会调用sched_yield

continue;

}

endif

//在不使用信号量时,调用sched_yield将会使当前进程暂时“让出”处理器

ngx_sched_yield();

}

}


可以看到,在不使用信号量时(例如,NGX_HAVE_POSIX_SEM宏没打开,或者spin的值为(ngx_uint_t)-1),ngx_shmtx_lock方法与ngx_spinlock方法非常相似,而在使用信号量后将会使用可能让进程进入睡眠的sem_wait方法代替“让出”处理器的ngx_sched_yield方法。这里不建议在Nginx worker进程中使用带信号量的ngx_shmtx_lock取锁方法。

ngx_shmtx_unlock方法会释放锁,虽然这个释放过程不会阻塞进程,但设置原子变量lock值时是可能失败的,因为多进程在同时修改lock值,而ngx_atomic_cmp_set方法要求参数old的值与lock值相同时才能修改成功,因此,ngx_atomic_cmp_set方法会在循环中反复执行,直到返回成功为止。该方法的实现如下所示:


void ngx_shmtx_unlock(ngx_shmtx_t*mtx)

{

ngx_atomic_uint_t val,old,wait;

//试图循环重置lock值为正数,此时务必将互斥锁释放

for(;){

//由共享内存中的lock原子变量取出锁状态

old=*mtx->lock;

//通过把最高位置为0,将lock变为正数

wait=old&0x7fffffff;

//如果变为正数的lock不是0,则减去1

val=wait?wait-1:0;

//将lock锁的值设为非负数val

if(ngx_atomic_cmp_set(mtx->lock,old,val)){

//设置锁成功后才能跳出循环,否则将持续地试图修改lock值为非负数

break;

}

}

if(NGX_HAVE_POSIX_SEM)

/如果lock锁原先的值为0,也就是说,并没有让某个进程持有锁,这时直接返回;或者,semaphore标志位为0,表示不需要使用信号量,也立即返回/

if(wait==0||!mtx->semaphore){

return;

}

/通过sem_post将信号量sem加1,表示当前进程释放了信号量互斥锁,通知其他进程的sem_wait继续执行/

if(sem_post(&mtx->sem)==-1){

ngx_log_error(NGX_LOG_ALERT,ngx_cycle->log,ngx_errno,

"sem_post()failed while wake shmtx");

}

endif

}


由于原子变量实现的这5种互斥锁方法是Nginx中使用最广泛的同步方式,当需要Nginx支持数以万计的并发TCP请求时,通常都会把spin值设为(ngx_uint_t)-1。这时的互斥锁在取锁时都会采用自旋锁,对于Nginx这种单进程处理大量请求的场景来说是非常适合的,能够大量降低不必要的进程间切换带来的消耗。