总体设计
nginx使用锁ngx_accept_mutex来避免惊群现象。锁是自己来实现的,这里锁的实现分为两种情况,一种是支持原子操作的情况,也就是由NGX_HAVE_ATOMIC_OPS这个宏来进行控制的,一种是不支持原子操作,这是是使用文件锁来实现。
用户空间进程间锁实现的原理,就是能弄一个让所有进程共享的东西,比如mmap的内存,比如文件,然后通过这个东西来控制进程的互斥。参考之前的一篇文档《锁的本质》,其实锁就是一种共享的资源而已。——共享一个变量,然后通过设置这个变量来控制进程的行为。
typedef struct {
#if (NGX_HAVE_ATOMIC_OPS)
ngx_atomic_t *lock; //原子操作
#else
ngx_fd_t fd; //fd表示进程间共享的文件句柄
u_char *name; //name表示文件名
#endif
} ngx_shmtx_t;
文件锁
上锁:ngx_trylock_accept_mutex----------------->ngx_shmtx_trylock--------------->ngx_trylock_fd
删除锁:ngx_shmtx_unlock------------------------->ngx_unlock_fd
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
/*ngx_shmtx_trylock通过fcntl对ngx_accept_mutex中fd成员变量上锁,锁定整个文件*/
if (ngx_shmtx_trylock(&ngx_accept_mutex)) {/*锁定成功*/
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"accept mutex locked");
/*ngx_accept_mutex_held 获得锁的标志,如果本来已经获得锁,则直接返回OK*/
if (ngx_accept_mutex_held
&& ngx_accept_events == 0
&& !(ngx_event_flags & NGX_USE_RTSIG_EVENT))
{
return NGX_OK;
}
/*ngx_enable_accept_events将cycle->listening中的端口号都加到epoll事件中。把进程对应的监听socket 放入到epoll中进行监听,这样只有该进程能监听到accept操作。*/
if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
ngx_shmtx_unlock(&ngx_accept_mutex);
return NGX_ERROR;
}
ngx_accept_events = 0;
ngx_accept_mutex_held = 1;/*获得锁的标识*/
return NGX_OK;
}
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"accept mutex lock failed: %ui", ngx_accept_mutex_held);
/*//如果我们前面已经获得了锁,然后这次获得锁失败,则说明当前的listen句柄已经被其他的进程锁监听,因此此时需要从epoll中移出调已经注册的listen句柄。这样就很好的控制了子进程的负载均衡 */
if (ngx_accept_mutex_held) {
/*ngx_disable_accept_events将cycle->listening中的端口号从epoll事件中删除*/
if (ngx_disable_accept_events(cycle) == NGX_ERROR) {
return NGX_ERROR;
}
ngx_accept_mutex_held = 0;
}
return NGX_OK;
}
ngx_shmtx_trylock(ngx_shmtx_t *mtx){
ngx_err_t err;
err = ngx_trylock_fd(mtx->fd);
if (err == 0) {
return 1;
}
}
ngx_err_t
ngx_trylock_fd(ngx_fd_t fd)
{
struct flock fl;
ngx_memzero(&fl, sizeof(struct flock));
fl.l_type = F_WRLCK;
fl.l_whence = SEEK_SET;
if (fcntl(fd, F_SETLK, &fl) == -1) {
return ngx_errno;
}
return 0;
}
void
ngx_shmtx_unlock(ngx_shmtx_t *mtx)
{
ngx_err_t err;
err = ngx_unlock_fd(mtx->fd);
if (err == 0) {
return;
}
ngx_log_abort(err, ngx_unlock_fd_n " %s failed", mtx->name);
}
ngx_err_t
ngx_unlock_fd(ngx_fd_t fd)
{
struct flock fl;
ngx_memzero(&fl, sizeof(struct flock));
fl.l_type = F_UNLCK;
fl.l_whence = SEEK_SET;
if (fcntl(fd, F_SETLK, &fl) == -1) {
return ngx_errno;
}
return 0;
}
支持原子操作的锁
/* cl should be equal or bigger than cache line size */
cl = 128;
// 创建共享内存,用于accept mutex,connection counter和ngx_temp_number
size = cl /* ngx_accept_mutex */
+ cl /* ngx_connection_counter */
+ cl; /* ngx_temp_number */
通过代码可以看到这里将会有3个区域被所有进程共享,其中我们的锁将会用到的是第一个。下面这段代码是初始化对应的共享内存区域。然后保存对应的互斥体指针。 shm.size = size;
shm.name.len = sizeof("nginx_shared_zone");
shm.name.data = (u_char *) "nginx_shared_zone";
shm.log = cycle->log;
// 创建共享内存,共享内存的起始地址保存在shm.addr
if (ngx_shm_alloc(&shm) != NGX_OK) {
return NGX_ERROR;
}
// 获得共享内存的起始地址
shared = shm.addr;
// accept互斥体取得共享内存的第一段cl大小内存
ngx_accept_mutex_ptr = (ngx_atomic_t *) shared;
ngx_accept_mutex.spin = (ngx_uint_t) -1;
// 系统支持原子数据则使用原子数据实现accept mutex,否则使用文件上锁实现
if (ngx_shmtx_create(&ngx_accept_mutex, shared, cycle->lock_file.data)
!= NGX_OK)
{
return NGX_ERROR;
}
ngx_int_t
ngx_shmtx_create(ngx_shmtx_t *mtx, void *addr, u_char *name)
{
mtx->lock = addr;
if (mtx->spin == (ngx_uint_t) -1) {
return NGX_OK;
}
mtx->spin = 2048;
#if (NGX_HAVE_POSIX_SEM)
if (sem_init(&mtx->sem, 1, 0) == -1) {
ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,
"sem_init() failed");
} else {
mtx->semaphore = 1;
}
我们先来看获得锁。 这里nginx分为两个函数,一个是trylock,它是非阻塞的,也就是说它会尝试的获得锁,如果没有获得的话,它会直接返回错误。 而第二个是lock,它也会尝试获得锁,而当没有获得他不会立即返回,而是开始进入循环然后不停的去获得锁,知道获得。不过nginx这里还有用到一个技巧,就是每次都会让当前的进程放到cpu的运行队列的最后一位,也就是自动放弃cpu。 先来看trylock 这个很简单,首先判断lock是否为0,为0的话表示可以获得锁,因此我们就调用ngx_atomic_cmp_set去获得锁,如果获得成功就会返回1,负责为0.
ngx_uint_t
ngx_shmtx_trylock(ngx_shmtx_t *mtx)
{
ngx_atomic_uint_t val;
val = *mtx->lock;
return ((val & 0x80000000) == 0
&& ngx_atomic_cmp_set(mtx->lock, val, val | 0x80000000));
}
ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)
的意思就是如果lock的值是0的话,就把lock的值修改为当前的进程id,否则返回失败。
接下来来看lock的实现,lock最终会调用ngx_spinlock,因此下面我要主要来分析这个函数。#define ngx_shmtx_lock(mtx) ngx_spinlock((mtx)->lock, ngx_pid, 1024)
我们来看spinklock,必须支持原子指令,才会有这个函数,这里nginx采用宏来控制的. 这里和trylock的处理差不多,都是利用原子指令来实现的,只不过这里如果无法获得锁,则会继续等待。 我们来看代码的实现:void
ngx_spinlock(ngx_atomic_t *lock, ngx_atomic_int_t value, ngx_uint_t spin)
{
#if (NGX_HAVE_ATOMIC_OPS)
ngx_uint_t i, n;
for ( ;; ) {
//如果lock为0,则说明没有进程持有锁,因此设置lock为value(为当前进程id),然后返回。
if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {
return;
}
//如果cpu个数大于1(也就是多核),则进入spin-wait loop阶段。
if (ngx_ncpu > 1) {
//开始进入循环。
for (n = 1; n < spin; n <<= 1) {
//下面这段就是纯粹的spin-loop wait。
for (i = 0; i < n; i++) {
//这个函数其实就是执行"PAUSE"指令,接下来会解释这个指令。
ngx_cpu_pause();
}
//然后重新获取锁,如果获得则直接返回。
if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {
return;
}
}
}
//这个函数调用的是sched_yield,它会强迫当前运行的进程放弃占有处理器。
ngx_sched_yield();
}
#else
#if (NGX_THREADS)
#error ngx_spinlock() or ngx_atomic_cmp_set() are not defined !
#endif
#endif
}
通过上面的代码可以看到spin lock实现的很简单,就是一个如果无法获得锁,就进入忙等的过程,不过这里nginx还多加了一个处理,就是如果忙等太长,就放弃cpu,直到下次任务再次占有cpu。 接下来来看下PAUSE指令,这条指令主要的功能就是告诉cpu,我现在是一个spin-wait loop,然后cpu就不会因为害怕循环退出时,内存的乱序而需要处理,所引起的效率损失问题。
##参考http://lonelyc.blog.51cto.com/8072409/1363799
http://www.xuebuyuan.com/557899.html
文件锁
上锁:ngx_trylock_accept_mutex----------------->ngx_shmtx_trylock--------------->ngx_trylock_fd
删除锁:ngx_shmtx_unlock------------------------->ngx_unlock_fd
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
/*ngx_shmtx_trylock通过fcntl对ngx_accept_mutex中fd成员变量上锁,锁定整个文件*/
if (ngx_shmtx_trylock(&ngx_accept_mutex)) {/*锁定成功*/
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"accept mutex locked");
/*ngx_accept_mutex_held 获得锁的标志,如果本来已经获得锁,则直接返回OK*/
if (ngx_accept_mutex_held
&& ngx_accept_events == 0
&& !(ngx_event_flags & NGX_USE_RTSIG_EVENT))
{
return NGX_OK;
}
/*ngx_enable_accept_events将cycle->listening中的端口号都加到epoll事件中。把进程对应的监听socket 放入到epoll中进行监听,这样只有该进程能监听到accept操作。*/
if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
ngx_shmtx_unlock(&ngx_accept_mutex);
return NGX_ERROR;
}
ngx_accept_events = 0;
ngx_accept_mutex_held = 1;/*获得锁的标识*/
return NGX_OK;
}
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"accept mutex lock failed: %ui", ngx_accept_mutex_held);
/*//如果我们前面已经获得了锁,然后这次获得锁失败,则说明当前的listen句柄已经被其他的进程锁监听,因此此时需要从epoll中移出调已经注册的listen句柄。这样就很好的控制了子进程的负载均衡 */
if (ngx_accept_mutex_held) {
/*ngx_disable_accept_events将cycle->listening中的端口号从epoll事件中删除*/
if (ngx_disable_accept_events(cycle) == NGX_ERROR) {
return NGX_ERROR;
}
ngx_accept_mutex_held = 0;
}
return NGX_OK;
}
ngx_shmtx_trylock(ngx_shmtx_t *mtx){
ngx_err_t err;
err = ngx_trylock_fd(mtx->fd);
if (err == 0) {
return 1;
}
}
ngx_err_t
ngx_trylock_fd(ngx_fd_t fd)
{
struct flock fl;
ngx_memzero(&fl, sizeof(struct flock));
fl.l_type = F_WRLCK;
fl.l_whence = SEEK_SET;
if (fcntl(fd, F_SETLK, &fl) == -1) {
return ngx_errno;
}
return 0;
}
void
ngx_shmtx_unlock(ngx_shmtx_t *mtx)
{
ngx_err_t err;
err = ngx_unlock_fd(mtx->fd);
if (err == 0) {
return;
}
ngx_log_abort(err, ngx_unlock_fd_n " %s failed", mtx->name);
}
ngx_err_t
ngx_unlock_fd(ngx_fd_t fd)
{
struct flock fl;
ngx_memzero(&fl, sizeof(struct flock));
fl.l_type = F_UNLCK;
fl.l_whence = SEEK_SET;
if (fcntl(fd, F_SETLK, &fl) == -1) {
return ngx_errno;
}
return 0;
}
支持原子操作的锁
/* cl should be equal or bigger than cache line size */
cl = 128;
// 创建共享内存,用于accept mutex,connection counter和ngx_temp_number
size = cl /* ngx_accept_mutex */
+ cl /* ngx_connection_counter */
+ cl; /* ngx_temp_number */
通过代码可以看到这里将会有3个区域被所有进程共享,其中我们的锁将会用到的是第一个。下面这段代码是初始化对应的共享内存区域。然后保存对应的互斥体指针。
shm.size = size;
shm.name.len = sizeof("nginx_shared_zone");
shm.name.data = (u_char *) "nginx_shared_zone";
shm.log = cycle->log;
// 创建共享内存,共享内存的起始地址保存在shm.addr
if (ngx_shm_alloc(&shm) != NGX_OK) {
return NGX_ERROR;
}
// 获得共享内存的起始地址
shared = shm.addr;
// accept互斥体取得共享内存的第一段cl大小内存
ngx_accept_mutex_ptr = (ngx_atomic_t *) shared;
ngx_accept_mutex.spin = (ngx_uint_t) -1;
// 系统支持原子数据则使用原子数据实现accept mutex,否则使用文件上锁实现
if (ngx_shmtx_create(&ngx_accept_mutex, shared, cycle->lock_file.data)
!= NGX_OK)
{
return NGX_ERROR;
}
ngx_int_t
ngx_shmtx_create(ngx_shmtx_t *mtx, void *addr, u_char *name)
{
mtx->lock = addr;
if (mtx->spin == (ngx_uint_t) -1) {
return NGX_OK;
}
mtx->spin = 2048;
#if (NGX_HAVE_POSIX_SEM)
if (sem_init(&mtx->sem, 1, 0) == -1) {
ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,
"sem_init() failed");
} else {
mtx->semaphore = 1;
}
我们先来看获得锁。 这里nginx分为两个函数,一个是trylock,它是非阻塞的,也就是说它会尝试的获得锁,如果没有获得的话,它会直接返回错误。 而第二个是lock,它也会尝试获得锁,而当没有获得他不会立即返回,而是开始进入循环然后不停的去获得锁,知道获得。不过nginx这里还有用到一个技巧,就是每次都会让当前的进程放到cpu的运行队列的最后一位,也就是自动放弃cpu。 先来看trylock 这个很简单,首先判断lock是否为0,为0的话表示可以获得锁,因此我们就调用ngx_atomic_cmp_set去获得锁,如果获得成功就会返回1,负责为0.
ngx_uint_t
ngx_shmtx_trylock(ngx_shmtx_t *mtx)
{
ngx_atomic_uint_t val;
val = *mtx->lock;
return ((val & 0x80000000) == 0
&& ngx_atomic_cmp_set(mtx->lock, val, val | 0x80000000));
}
ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)
的意思就是如果lock的值是0的话,就把lock的值修改为当前的进程id,否则返回失败。
#define ngx_shmtx_lock(mtx) ngx_spinlock((mtx)->lock, ngx_pid, 1024)
我们来看spinklock,必须支持原子指令,才会有这个函数,这里nginx采用宏来控制的. 这里和trylock的处理差不多,都是利用原子指令来实现的,只不过这里如果无法获得锁,则会继续等待。 我们来看代码的实现:
void
ngx_spinlock(ngx_atomic_t *lock, ngx_atomic_int_t value, ngx_uint_t spin)
{
#if (NGX_HAVE_ATOMIC_OPS)
ngx_uint_t i, n;
for ( ;; ) {
//如果lock为0,则说明没有进程持有锁,因此设置lock为value(为当前进程id),然后返回。
if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {
return;
}
//如果cpu个数大于1(也就是多核),则进入spin-wait loop阶段。
if (ngx_ncpu > 1) {
//开始进入循环。
for (n = 1; n < spin; n <<= 1) {
//下面这段就是纯粹的spin-loop wait。
for (i = 0; i < n; i++) {
//这个函数其实就是执行"PAUSE"指令,接下来会解释这个指令。
ngx_cpu_pause();
}
//然后重新获取锁,如果获得则直接返回。
if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {
return;
}
}
}
//这个函数调用的是sched_yield,它会强迫当前运行的进程放弃占有处理器。
ngx_sched_yield();
}
#else
#if (NGX_THREADS)
#error ngx_spinlock() or ngx_atomic_cmp_set() are not defined !
#endif
#endif
}
通过上面的代码可以看到spin lock实现的很简单,就是一个如果无法获得锁,就进入忙等的过程,不过这里nginx还多加了一个处理,就是如果忙等太长,就放弃cpu,直到下次任务再次占有cpu。 接下来来看下PAUSE指令,这条指令主要的功能就是告诉cpu,我现在是一个spin-wait loop,然后cpu就不会因为害怕循环退出时,内存的乱序而需要处理,所引起的效率损失问题。