转载一篇nginx锁的细节的文章 http://wang.peng.1123.blog.163.com/blog/static/129821112201381311441180/
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的设计中处处透露出来高效的设计,所以锁的设计也是为了提高整个服务器的效率。