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