14.8 互斥锁

基于原子操作、信号量以及文件锁,Nginx在更高层次封装了一个互斥锁,使用起来很方便,许多Nginx模块也是更多直接使用它。下面看一下表14-1中介绍的操作这个互斥锁的5种方法。

14.8.1 文件锁实现的ngx_shmtx_t锁 - 图1

表14-1中的5种方法非常全面,获取互斥锁时既可以使用不会阻塞进程的ngx_shmtx_trylock方法,也可以使用ngx_shmtx_lock方法告诉Nginx必须持有互斥锁后才能继续向下执行代码。它们都通过操作ngx_shmtx_t类型的结构体来实现互斥操作,下面再来看一下ngx_shmtx_t中有哪些成员,如下所示。


typedef struct{

if(NGX_HAVE_ATOMIC_OPS)

//原子变量锁

ngx_atomic_t*lock;

if(NGX_HAVE_POSIX_SEM)

//semaphore为1时表示获取锁将可能使用到的信号量

ngx_uint_t semaphore;

//sem就是信号量锁

sem_t sem;

endif

else

//使用文件锁时fd表示使用的文件句柄

ngx_fd_t fd;

//name表示文件名

u_char*name;

endif

/自旋次数,表示在自旋状态下等待其他处理器执行结果中释放锁的时间。由文件锁实现时,spin没有任何意义/

ngx_uint_t spin;

}ngx_shmtx_t;


注意 读者可能会觉得奇怪,既然ngx_shmtx_t结构体中的spin成员对于文件锁没有任何意义,为什么不放在#if(NGX_HAVE_ATOMIC_OPS)宏内呢?这是因为,对于使用ngx_shmtx_t互斥锁的代码来说,它们并不想知道互斥锁是由文件锁、原子变量或者信号量实现的。同时,spin的值又具备非常多的含义(C语言的编程风格导致可读性比面向对象语言差些),当仅用原子变量实现互斥锁时,spin只表示自旋等待其他处理器的时间,达到spin值后就会“让出”当前处理器。如果spin为0或者负值,则不会存在调用PAUSE的机会,而是直接调用sched_yield“让出”处理器。假设同时使用信号量,spin会多一种含义,即当spin值为(ngx_uint_t)-1时,相当于告诉这个互斥锁绝不要使用信号量使得进程进入睡眠状态。这点很重要,实际上,在实现第9章提到的负载均衡锁时,spin的值就是(ngx_uint_t)-1。

可以看到,ngx_shmtx_t结构体涉及两个宏:NGX_HAVE_ATOMIC_OPS、NGX_HAVE_POSIX_SEM,这两个宏对应着互斥锁的3种不同实现。

第1种实现,当不支持原子操作时,会使用文件锁来实现ngx_shmtx_t互斥锁,这时它仅有fd和name成员(实际上还有spin成员,但这时没有任何意义)。这两个成员使用14.7节介绍的文件锁来提供阻塞、非阻塞的互斥锁。

第2种实现,支持原子操作却又不支持信号量。

第3种实现,在支持原子操作的同时,操作系统也支持信号量。

后两种实现的唯一区别是ngx_shmtx_lock方法执行时的效果,也就是说,支持信号量只会影响阻塞进程的ngx_shmtx_lock方法持有锁的方式。当不支持信号量时,ngx_shmtx_lock取锁与14.3.3节中介绍的自旋锁是一致的,而支持信号量后,ngx_shmtx_lock将在spin指定的一段时间内自旋等待其他处理器释放锁,如果达到spin上限还没有获取到锁,那么将会使用sem_wait使得当前进程进入睡眠状态,等其他进程释放了锁内核后才会唤醒这个进程。当然,在实际实现过程中,Nginx做了非常巧妙的设计,它使得ngx_shmtx_lock方法在运行一段时间后,如果其他进程始终不放弃锁,那么当前进程将有可能强制性地获得到这把锁,这也是出于Nginx不宜使用阻塞进程的睡眠锁方面的考虑。

14.8.1 文件锁实现的ngx_shmtx_t锁

本节介绍如何通过文件锁实现表14-1中的5种方法(也就是Nginx对fcntl系统调用封装过的ngx_trylock_fd、ngx_lock_fd和ngx_unlock_fd方法实现的锁)。

ngx_shmtx_create方法用来初始化ngx_shmtx_t互斥锁,ngx_shmtx_t结构体要在调用ngx_shmtx_create方法前先行创建。下面看一下该方法的源代码。


ngx_int_t ngx_shmtx_create(ngx_shmtx_tmtx,voidaddr,u_char*name)

{

//不用在调用ngx_shmtx_create方法前先行赋值给ngx_shmtx_t结构体中的成员

if(mtx->name){

/如果ngx_shmtx_t中的name成员有值,那么如果与name参数相同,意味着mtx互斥锁已经初始化过了;否则,需要先销毁mtx中的互斥锁再重新分配mtx/

if(ngx_strcmp(name,mtx->name)==0){

//如果name参数与ngx_shmtx_t中的name成员相同,则表示已经初始化了

mtx->name=name;

//既然曾经初始化过,证明fd句柄已经打开过,直接返回成功即可

return NGX_OK;

}

/如果ngx_shmtx_t中的name与参数name不一致,说明这一次使用了一个新的文件作为文件锁,那么先调用ngx_shmtx_destory方法销毁原文件锁/

ngx_shmtx_destory(mtx);

}

//按照name指定的路径创建并打开这个文件

mtx->fd=ngx_open_file(name,NGX_FILE_RDWR,NGX_FILE_CREATE_OR_OPEN,NGX_FILE_DEFAULT_ACCESS);

if(mtx->fd==NGX_INVALID_FILE){

//一旦文件因为各种原因(如权限不够)无法打开,通常会出现无法运行错误

return NGX_ERROR;

}

/由于只需要这个文件在内核中的INODE信息,所以可以把文件删除,只要fd可用就行/

if(ngx_delete_file(name)==NGX_FILE_ERROR){

}

mtx->name=name;

return NGX_OK;

}


ngx_shmtx_create方法需要确保ngx_shmtx_t结构体中的fd是可用的,它的成功执行是使用互斥锁的先决条件。

ngx_shmtx_destory方法用于关闭在ngx_shmtx_create方法中已经打开的fd句柄,如下所示。


void ngx_shmtx_destory(ngx_shmtx_t*mtx)

{

//关闭ngx_shmtx_t结构体中的fd句柄

if(ngx_close_file(mtx->fd)==NGX_FILE_ERROR){

ngx_log_error(NGX_LOG_ALERT,ngx_cycle->log,ngx_errno,

ngx_close_file_n"\"%s\"failed",mtx->name);

}

}


ngx_shmtx_trylock方法试图使用非阻塞的方式获得锁,返回1时表示获取锁成功,返回0表示获取锁失败。


ngx_uint_t ngx_shmtx_trylock(ngx_shmtx_t*mtx)

{

ngx_err_t err;

//由14.7节介绍过的ngx_trylock_fd方法实现非阻塞互斥锁的获取

err=ngx_trylock_fd(mtx->fd);

if(err==0){

return 1;

}

//如果err错误码是NGX_EAGAIN,则表示现在锁已经被其他进程持有了

if(err==NGX_EAGAIN){

return 0;

}

ngx_log_abort(err,ngx_trylock_fd_n"%s failed",mtx->name);

return 0;

}


ngx_shmtx_lock方法将会在获取锁失败时阻塞代码的继续执行,它会使当前进程处于睡眠状态,等待其他进程释放锁后内核唤醒它。可见,它是通过14.7节介绍的ngx_lock_fd方法实现的,如下所示。


void ngx_shmtx_lock(ngx_shmtx_t*mtx)

{

ngx_err_t err;

//ngx_lock_fd方法返回0时表示成功地持有锁,返回-1时表示出现错误

err=ngx_lock_fd(mtx->fd);

if(err==0){

return;

}

ngx_log_abort(err,ngx_lock_fd_n"%s failed",mtx->name);

}


ngx_shmtx_lock方法没有返回值,因为它一旦返回就相当于获取到互斥锁了,这会使得代码继续向下执行。

ngx_shmtx_unlock方法通过调用ngx_unlock_fd方法来释放文件锁,如下所示。


void ngx_shmtx_unlock(ngx_shmtx_t*mtx)

{

ngx_err_t err;

//返回0即表示释放锁成功

err=ngx_unlock_fd(mtx->fd);

if(err==0){

return;

}

ngx_log_abort(err,ngx_unlock_fd_n"%s failed",mtx->name);

}


可以看到,ngx_shmtx_t互斥锁在使用文件锁实现时是非常简单的,它只是简单地封装了14.7节介绍的文件锁。