http://wang.peng.1123.blog.163.com/blog/static/129821112201381311441180/
在前面的源码分析中我们大致的介绍了一下nginx对负载均衡问题和惊群问题的解决方案,在本次源码分析中我们详细了解一下nginx在解决这两个问题中所用的进程锁的实现原理。
我们在unix环境高级编程中曾看到线程之间共享有互斥变量,进程之间也有好几种进程之间的通信方式。那么进程之间如何实现锁呢?
Nginx中的锁是自己实现的,分为两种,一种是支持原子实现的原子锁,另外一种是文件锁。本文我们重点介绍原子锁的实现。
我们可以看到在线程中实现锁就是通过一个共享的堆上的内存(通过malloc实现),那么在进程中实现锁也是通过这样一个共享的区域来实现进程的同步。说白了就是共享一个变量,然后通过这个变量来控制多个进程同步运行。
大致了解了进程锁的实现原理后我们来看一下nginx中实现进程锁的数据结构,其核心数据结构如下(在Src/core/Ngx_shmtx.h第16行)
typedef struct { #if (NGX_HAVE_ATOMIC_OPS) //原子锁 ngx_atomic_t *lock; //指向一个共享内存区域的地址 #if (NGX_HAVE_POSIX_SEM) //信号量 ngx_uint_t semaphore; sem_t sem; #endif #else //文件锁 ngx_fd_t fd; //进程间共享文件句柄 u_char *name;//文件名 #endif ngx_uint_t spin; } ngx_shmtx_t;
我们可以看到上面的数据结构中包括三种,一种是原子锁,一种是信号量,另外一种是文件锁,其中原子锁实现很简单,就是定义一个指针指向一个内存区域,文件锁中包括两个变量,一个是共享的文件句柄,另外一个就是共享文件的文件名。通过上边的数据结构我们可以看到nginx中实现锁的类型是通过宏来区分的,一共三种,原子和信号量还有共享文件。
原子锁的类型ngx_atomic_t 定义在如下: (参阅文件/src/os/unix/ngx_atomic.h第24行)
typedef AO_t ngx_atomic_uint_t; typedef volatile ngx_atomic_uint_t ngx_atomic_t;
因为我们本次源码分析中只是重点分析原子锁的实现,所以我们来看一下原子锁的实现。首先看一个原子锁的初始化,然后我们在分析获取锁和释放锁。
初始化代码在ngx_event_module_init中,进入到该函数中,我们可以看到如下代码:
size_t size, cl; /* cl should be equal or bigger 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; }
通过上面的代码可以看到初始化了锁的内存大小,长度,数据,同时开辟了一个空间,我们来看一下ngx_shm_alloc的实现。
ngx_shm_alloc(ngx_shm_t *shm) { shm->addr = (u_char *) mmap(NULL, shm->size, PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0); }
函数很简单就是将一个mmap内存映射地址赋给锁的地址区域。接着看代码
shared = shm.addr; ngx_accept_mutex_ptr = (ngx_atomic_t *) shared; ngx_accept_mutex.spin = (ngx_uint_t) -1;
然后将这个空间地址复制给shared变量,在接下来的代码中我们将会看到shared变量的作用。
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; } return NGX_OK; }
很简单,就是将锁指针指向我们刚才映射的内存区域。
现在我们的原子就创建成功了。接下来我们就看一下多个进程之间是如何获得锁并释放锁。在nginx中进程获取锁有两种方式,一种是非阻塞的方式,另外一种是循环不停的获取锁直到获取锁。我们先来看一下非阻塞的实现方式
在非阻塞的实现过程中,只要是未获取锁就会立即返回失败。通过ngx_shmtx_trylock函数来实现的(详细参阅/src/core第58行)
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)); }
首先判断lock是否为0,如果为0表示此时可以获取锁,则调用ngx_atomic_cmp_set函数获取锁。如果获取锁成功则返回1,失败返回 0;ngx_atomic_cmp_set函数是一个原子操作,此时的实现是比较+赋值两个操作。如果在中间比较之后被别的进程抢占之后在进行赋值就有可能出现脏数据。该函数的作用就是如果lock的值为0,则将lock的值更改为当前进程的id,否则返回失败。
看完了trylock的实现我们看一下lock的实现。Lock的实现主要是在ngx_spinlock中实现的。(详阅参见src/core/ngx_spinlock.c)
for ( ;; ) { if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) { return; } if (ngx_ncpu > 1) { for (n = 1; n < spin; n <<= 1) { for (i = 0; i < n; i++) { ngx_cpu_pause(); } if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) { return; } } } ngx_sched_yield(); }
其实现原理也主要是原子指令,如果进程没有锁(即lock为0)则设置lock为当前进程id。然后返回,ngx_ncpu > 1表示如果cpu是多核则进入spin-wait loop阶段,其实现原理是很假单的。就是如果无法获得锁,则进入忙等阶段。同时如果忙等时间太长了就放弃CPU,知道下次获得CPU。
获得大致就是上边这样的两种方式,接下来看一下释放锁。释放锁很简单(详细参阅/src/core/Ngx_shmtx.c第143行)。该函数不具体解释了,大致意思就是比较lock,判断是否为当前进程id,如果是则将lock更改为0,说明放弃这个锁。
Nginx 中锁的实现大致就是这样的,虽然说nginx中锁的实现很简单,但是其作用是非常重要的,在整个进程模型中起到了很大的作用。解决了惊群效应(惊群现象在前边的源码分析有介绍,这里就不在介绍了),在我们前面的章节分析也可以看出来在nginx的设计中处处透露出来高效的设计,所以锁的设计也是为了提高整个服务器的效率。
我们来看一下nginx中使用锁的例子。在nginx中使用锁我们前面也了解了,在处理负载均衡和惊群效应中的应用。我们先来看函数ngx_process_events_and_timers.
if (ngx_use_accept_mutex) { if (ngx_accept_disabled > 0) { //该标志主要是处理负载均衡的。如果大于0则跳过锁处理不争抢accept ngx_accept_disabled--; } else { if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) { //获取锁,失败返回0; return; } if (ngx_accept_mutex_held) { //如果变量ngx_accept_mutex_held大于0则表示已经获取锁 flags |= NGX_POST_EVENTS; } else { //此处即我们前面介绍的如果未获取锁则等待一定的时间再去争抢锁 if (timer == NGX_TIMER_INFINITE || timer > ngx_accept_mutex_delay) { timer = ngx_accept_mutex_delay; } } } }
上面代码的就是进程争抢锁的大致过程。首先判断进程负载情况,如果超载则不争抢,反之则去争抢锁资源。此处使用的是trylock。如果出错直接返回,如果获取锁则设置标志。如果未获取锁则设置等待时间。NGX_POST_EVENTS宏表示,这个标记表示当socket有数据唤醒时,不会立即accept 或者读取,而是将这个事件保存起来,当释放锁之后,才会accept或者读取这个句柄。如果该标记没有设置,则会理解accept或者读取句柄。此处未获取锁的解决很简单我们就不再赘述了。
这里主要看一下ngx_trylock_accept_mutex(cycle),该函数在/src/event第295行
ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle) { //尝试获取锁 if (ngx_shmtx_trylock(&ngx_accept_mutex)) { //如果本来就已经获取锁,则直接返回OK ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "accept mutex locked"); if (ngx_accept_mutex_held && ngx_accept_events == 0 && !(ngx_event_flags & NGX_USE_RTSIG_EVENT)) { return NGX_OK; //获取锁成功,需要打开被关闭的listening句柄 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); if (ngx_accept_mutex_held) { //如果前面获得锁这次获得锁是失败。则当前listening句柄被其他进程监听 if (ngx_disable_accept_events(cycle) == NGX_ERROR) { return NGX_ERROR; } ngx_accept_mutex_held = 0; //设置此次未获取锁 } return NGX_OK; }
以上基本就是进程在获取锁的过程。我们可以看到通过添加进程锁。使其效率提升很大。同时解决了负载均衡和惊群问题。以上基本就是nginx进程锁的过程。
我们来回顾一下nginx中锁的原理,首先是设置一个共享区域来维护锁。然后就是初始化这个锁。通过创建一个共享区域,并设置其值。接下来我们讲解了两种加锁方式,一种是非阻塞的trylock,另外一种是循环等待加锁的lock方式。接下来我们讲解了一个例子,就是nginx中进程获取accept句柄过程中对锁的使用。Nginx中进程锁原理和步骤大概如上。