代码改变世界

Nginx的锁的实现以及惊群的避免

2016-11-13 15:49  Loull  阅读(4901)  评论(0编辑  收藏  举报

在前面的文章中,其实很多代码就涉及到加锁释放锁的动作了,但是自己一直避免去深究他们,好了这篇文章就讲Nginx是如何实现锁的吧,然后还要讲Nginx是如何使用锁来避免惊群的发生。

在Nginx的锁的实现中,要分为两种情况,分别为支持原子操作以与不支持原子操作。其定义在Ngx_shmtx.h当中:

//锁的定义
typedef struct {
#if (NGX_HAVE_ATOMIC_OPS)
    ngx_atomic_t  *lock;  //如果支持原子锁的话,那么使用它
#if (NGX_HAVE_POSIX_SEM) 
    ngx_atomic_t  *wait;
    ngx_uint_t     semaphore;
    sem_t          sem;
#endif
#else
    ngx_fd_t       fd;   //不支持原子操作的话就使用文件锁来实现
    u_char        *name;
#endif
    ngx_uint_t     spin;     //这是自旋锁么?
} ngx_shmtx_t;

嗯,其实定义还是很简单的,一看就明白了。好接下来看支持原子操作的方式是如何实现的吧,在ngx_event_core_module模块的ngx_event_module_init函数中会有如下代码:

    /*后面将会创建size大小的共享内存,这块共享内存将被均分成三段, 
    分别供ngx_accept_mutex、ngx_connection_counter、ngx_temp_number 
    使用。 
    */  
    /* cl should be equal to or greater than cache line size */
    cl = 128;
    size = cl            /* ngx_accept_mutex */
           + cl          /* ngx_connection_counter */
           + cl;         /* ngx_temp_number */

//共享内存的初始化
    shm.size = size;
    shm.name.len = sizeof("nginx_shared_zone");
    shm.name.data = (u_char *) "nginx_shared_zone";
    shm.log = cycle->log;

    if (ngx_shm_alloc(&shm) != NGX_OK) {   //为共享内存分配内存空间
        return NGX_ERROR;
    }

    shared = shm.addr;   //获取共享内存的地址

    ngx_accept_mutex_ptr = (ngx_atomic_t *) shared;   //存放互斥量内存地址的指针
    ngx_accept_mutex.spin = (ngx_uint_t) -1;   //初始化自旋锁的初值为-1

    if (ngx_shmtx_create(&ngx_accept_mutex, (ngx_shmtx_sh_t *) shared,  //如果支持原子操作的话,这个就很简单了,就直接将内存地址分配过去就行了
                         cycle->lock_file.data)
        != NGX_OK)
    {
        return NGX_ERROR;
    }

    ngx_connection_counter = (ngx_atomic_t *) (shared + 1 * cl);   //ngx_connection_counter为其分配共享内存的内存空间

    (void) ngx_atomic_cmp_set(ngx_connection_counter, 0, 1);

    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "counter: %p, %d",
                   ngx_connection_counter, *ngx_connection_counter);

    ngx_temp_number = (ngx_atomic_t *) (shared + 2 * cl);  //ngx_temp_number的内存空间

这段代码的意思是首先调用ngx_shm_alloc函数创建共享内存,然后再为ngx_accept_mutex变量在其中分配其lock域内存,嗯,这个变量的用处大概大家也知道吧。(其实lock说白了也就是一个64位的int而已),当然共享内存中还有其他一些变量的定义,ngx_connection_counter变量用于保存当前服务器总共持有的connection。

嗯,注意ngx_shmtx_create函数,它用于创建锁,这里有两种方式的实现,分别为支持原子操作,和不支持原子操作的两种,这里我们只看支持原子操作的方式吧:

//为锁mtx的lock域分配内存
ngx_int_t
ngx_shmtx_create(ngx_shmtx_t *mtx, ngx_shmtx_sh_t *addr, u_char *name)
{
    mtx->lock = &addr->lock;  //其实就是直接将内存地址赋个mtx的lock域就完事了

    if (mtx->spin == (ngx_uint_t) -1) {
        return NGX_OK;
    }

    mtx->spin = 2048;

    return NGX_OK;
}

上面的代码够简单吧。

嗯,接下来看Nginx如何获取以及释放锁。嗯,实现有两种,分别为lock与trylock如果是lock的话,那么会组设,也就是自旋,直到获取了锁位置,如果使用trylock的话,那么就是非阻塞的方式,如果没有获取到,那么直接返回错误就好了。我们先看trylock吧,定义在Ngx_shmtx.c 当中(还是只看支持原子操作的实现方式吧):

//尝试获取锁,原子的方式
ngx_uint_t
ngx_shmtx_trylock(ngx_shmtx_t *mtx)
{
    return (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid));
}

嗯,其实很简单,首先是判断mtx的lock域是否等于0,如果不等于,那么就直接返回false好了,如果等于的话,那么就要调用原子操作ngx_atomic_cmp_set了,它用于比较mtx的lock域,如果等于零,那么设置为当前进程的进程id号,否则返回false。嗯,这个ngx_atomic_cmp_set函数是跟体系结构相关的,这里就不细讲了。

然后就可以将lock的实现了,

//尝试获取锁,原子的方式
ngx_uint_t
ngx_shmtx_trylock(ngx_shmtx_t *mtx)
{
    return (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid));
}

//阻塞的方式获取锁
void
ngx_shmtx_lock(ngx_shmtx_t *mtx)
{
    ngx_uint_t         i, n;

    ngx_log_debug0(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0, "shmtx lock");
//一个死循环,不断的去看是否获取了锁,直到获取了之后才退出
    for ( ;; ) {
//如果获取了锁,那么就可以直接返回了
        if (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)) {
            return;
        }
//如果cpu的数量大于一
        if (ngx_ncpu > 1) {
            for (n = 1; n < mtx->spin; n <<= 1) {
                for (i = 0; i < n; i++) {
                    ngx_cpu_pause();
                }

                if (*mtx->lock == 0
                    && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid))
                {
                    return;
                }
            }
        }

        ngx_sched_yield();
    }
}

一个for循环就暴露了其自旋的本质。里面还涉及到 一些优化的,嗯,我也不太懂,以后再说吧。接下来就可以将unlock了:

//释放锁
void
ngx_shmtx_unlock(ngx_shmtx_t *mtx)
{
    if (mtx->spin != (ngx_uint_t) -1) {
        ngx_log_debug0(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0, "shmtx unlock");
    }

    if (ngx_atomic_cmp_set(mtx->lock, ngx_pid, 0)) {
        ngx_shmtx_wakeup(mtx);
    }
}

嗯,还是很简单,判断锁的lock域与当前进程的进程id是否相等,如果相等的话,那么就将lock设置为0,然后就相当于释放了锁。

好接下来可以看如何用锁来避免惊群了。在ngx_event_core_module模块的ngx_event_module_init函数中我们已经看到了ngx_accept_mutex的lock域的内存是在共享内存中,因而,所有worker进程都共享它,在ngx_process_events_and_timers函数中我们可以看到如下的代码:

            /*尝试锁accept mutex,只有成功获取锁的进程,才会将listen 
              套接字放入epoll中。因此,这就保证了只有一个进程拥有 
              监听套接口,故所有进程阻塞在epoll_wait时,不会出现惊群现象。 
            */  
            //这里的ngx_trylock_accept_mutex函数中,如果顺利的获取了锁,那么它会将监听端口注册到当前worker进程的epoll当中
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                return;
            }

函数ngx_trylock_accept_mutex用于尝试获取ngx_accept_mutex锁,如果获取了的话,那么就将listening加入到epoll当中,我们可以来看这个函数:

//尝试获取锁,如果获取了锁,那么还要将当前监听端口全部注册到当前worker进程的epoll当中去
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {  //尝试获取互斥锁
        
        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "accept mutex locked");
        //如果本来已经获得锁,则直接返回Ok
        if (ngx_accept_mutex_held
            && ngx_accept_events == 0
            && !(ngx_event_flags & NGX_USE_RTSIG_EVENT))
        {
            return NGX_OK;
        }
        //到达这里,说明重新获得锁成功,因此需要打开被关闭的listening句柄,调用ngx_enable_accept_events函数,将监听端口注册到当前worker进程的epoll当中去
        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);
//这里表示的是以前曾经获取过,但是这次却获取失败了,那么需要将监听端口从当前的worker进程的epoll当中移除,调用的是ngx_disable_accept_events函数
    if (ngx_accept_mutex_held) {
        if (ngx_disable_accept_events(cycle) == NGX_ERROR) {
            return NGX_ERROR;
        }

        ngx_accept_mutex_held = 0;   //表示当前并没有获取锁
    }

    return NGX_OK;
}

调用ngx_shmtx_trylock来尝试获取ngx_accept_mutex锁,如果获取了的话,在判断在上次循环中是否已经获取了锁,如果获取了,那么listening就已经在当前worker进程的epoll当中了,否则的话就调用ngx_enable_accept_events函数来讲listening加入到epoll当中,并要对变量ngx_accept_mutex_held赋值,表示已经获取了锁。如果没有获取到锁的话,还要判断上次是否已经获取了锁,如果上次获取了的话,那么还要调用ngx_disable_accept_events函数,将listening从epoll当中移除。

嗯,就这样就可以保证所有的worker进程中就只有一个worker将listening放入到了epoll当中,也就避免了惊群的发生。好了,就讲完了(当然我只讲了有原子操作的情况下的实现方案,并没有讲文件锁的实现方案,但是其实也都大同小异)。

 

转自:http://www.xuebuyuan.com/2041519.html