原文链接http://361324767.blog.163.com/blog/static/114902525201261391625240/
本文,我们来分析下nginx中对共享内存和锁的使用。
在nginx中,很多地方使用到了共享内存,在我们的应用中,往往有一些数据需要在多进程间进行共享,了解了共享内存的实现与使用,对我们写程序可以提供 很多帮助。在我之前的博文中也有介绍到共享内存的使用与slab分配器,以及红黑树的使用。本文,我将从底层实现上简单介绍下nginx共享内存的实现与 锁的利用。
由于nginx不同版本间会有一些差异,我这里是按照nginx-1.0.6版本来分析。
1. 共享内存
nginx在共享内存操作相关的诉在src/os/unix/ngx_shmem.c与src/os/unix/ngx_shmem.h中。
我们先来看看这个结构体
1
2
3
4
5
6
7
|
typedef struct {
u_char *addr; // 共享内存首地址
size_t size; // 共享内存大小
ngx_str_t name; // 共享内存名称
ngx_log_t * log ; // 日志
ngx_uint_t exists; /* unsigned exists:1; */
} ngx_shm_t;
|
在ngx_shmem.c中我们可以看到根据不同的宏,会有不同的共享内存实现方式。我们看看NGX_HAVE_MAP_ANON的这种方式,它很简单就 是使用mmap的方式来创建共享内存。但从代码中,我们可以看出,它创建出来的共享内存是不与文件相关的,也就是不会映射到文件,当然,当nginx重启 后,共享内存里面的内容就消失了。从代码中,我们可以看到,这里没有用到name与exists。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
ngx_int_t
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);
if
(shm->addr == MAP_FAILED) {
ngx_log_error(NGX_LOG_ALERT, shm->
log
, ngx_errno,
"mmap(MAP_ANON|MAP_SHARED, %uz) failed"
, shm->size);
return
NGX_ERROR;
}
return
NGX_OK;
}
void
ngx_shm_free(ngx_shm_t *shm)
{
if
(munmap((
void
*) shm->addr, shm->size) == -1) {
ngx_log_error(NGX_LOG_ALERT, shm->
log
, ngx_errno,
"munmap(%p, %uz) failed"
, shm->addr, shm->size);
}
}
|
共享内存代码的分析很简单,当然使用也很简单了。而在一般情况下,我们在共享内存里面会存放比较复杂的数据结构,需要经常操作共享内存。而如果我们需要经 常分配与释放共享内存,如果每次分配与释放都去调用mmap与munmap的话,这样效率很非常的底,所以在nginx中会使用slab分配器来辅助共享 内存的使用。通常的做法是,先预先分配出一块较大的共享内存池,然后在之后分配共享内存时,就使用slab分配器从共享内存池里面分配出我们需要的内存大 小。这种方法在我之前的文章中有介绍。
那么,共享内存的创建就很简单了,看代码:
1
2
3
4
5
6
7
8
9
10
|
ngx_shm_t shm;
shm.size = 1024;
shm.name.len =
sizeof
(
"nginx_shared_zone"
);
shm.name.data = (u_char *)
"nginx_shared_zone"
;
shm.
log
= ngx_cycle->
log
;
if
(ngx_shm_alloc(&shm) != NGX_OK) {
return
NGX_ERROR;
}
|
nginx中共享内存的实现比较简单,当然也比较局限。首先,共享内存只会存在于内存中,不会保存到文件,所以当程序退出后,共享内存里面的数据都会丢 失,这需要我们手动去缓存到文件。其次,在使用slab分配器的时候,如果一旦出现共享内存越界的时候,会导致意想不到的后果,而且这种错误无法通过内存 检查工具来检查。所以在使用共享内存时,请务必小心。
2. 锁的实现
先看看ngx_shmtx_t这个结构体:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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;
|
我们可以看到,根据NGX_HAVE_ATOMIC_OPS宏,有两种不同的形式,lock或fd。如果是在fd的情况下,nginx通过对文件句柄的加锁来实现的。对于这种方式,这里不做过多介绍。
在fd模式下,ngx_shmtx_lock调用ngx_lock_fd来实现,而ngx_lock_fd的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
ngx_err_t
ngx_lock_fd(ngx_fd_t fd)
{
struct
flock fl;
fl.l_start = 0;
fl.l_len = 0;
fl.l_pid = 0;
fl.l_type = F_WRLCK;
fl.l_whence = SEEK_SET;
if
(fcntl(fd, F_SETLKW, &fl) == -1) {
return
ngx_errno;
}
return
0;
}
|
在NGX_HAVE_ATOMIC_OPS有设置的情况下,而且没有使用信号量的时候,ngx_shmtx_t这个结构体就变得很简单了:
1
2
3
4
|
typedef
struct
{
ngx_atomic_t *lock;
// 指向存放在共享内存里面的lock的地址
ngx_uint_t spin;
// 自旋锁时,可由它来控制自旋时间
} ngx_shmtx_t;
|
首先,调用ngx_shmtx_create来创建:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
ngx_int_t
ngx_shmtx_create(ngx_shmtx_t *mtx,
void
*addr, u_char *name)
{
// 指向共享内存中的地址
mtx->lock = addr;
// 如果有指定为-1,则表示关掉自旋等待,在后面代码中我们可以看到
if
(mtx->spin == (ngx_uint_t) -1) {
return
NGX_OK;
}
// 默认为2048
mtx->spin = 2048;
return
NGX_OK;
}
|
在ngx_shmtx_create中,可以看到在非fd模式下面是没有用到name的。在调用时,mtx为本地分配的一个结构体,而addr这个参数,则是在共享内存中分配的一个lock地址。看nginx自己的调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
static
ngx_int_t
ngx_event_module_init(ngx_cycle_t *cycle)
size_t
size, cl;
// 要大于或等待cache line
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;
}
// shm为创建的共享内存
// 因为之前有预留128字节,所以shared指向它是没有问题的
shared = shm.addr;
ngx_accept_mutex.spin = (ngx_uint_t) -1;
// 注意第二个参数,即共享内存的初始位置就是我们要创建shmtx的locked
if
(ngx_shmtx_create(&ngx_accept_mutex, shared, cycle->lock_file.data)
!= NGX_OK)
{
return
NGX_ERROR;
}
}
|
在上面这段代码中,我只挑了核心代码,为什么要之前留128字节,大于或等待cache line呢?其实这里第一个目的是为了留给lock一个空间,另外一个主要的目的是为了提高性能。这样可以将lock与其它数据分开到不同的 cacheline中去,于是,其它数据的修改(其它几个数据是原子操作,可并发修改)就不会导致有lock的cacheline的失效。cacheline的False sharing问题及其解决方法,可参考余老师http://blog.yufeng.info/archives/tag/cache-line中的介绍。
接下来,就是加锁和解锁了。ngx_shmtx_lock与ngx_shmtx_unlock。看看nginx是如何实现自旋锁的。
ngx_shmtx_lock的实现:
void
ngx_shmtx_lock(ngx_shmtx_t *mtx)
{
ngx_uint_t i, n;
ngx_atomic_uint_t val;
for
( ;; ) {
val = *mtx->lock;
// 如果还没有上锁,就加锁,然后返回,这里容易理解
if
((val & 0x80000000) == 0
&& ngx_atomic_cmp_set(mtx->lock, val, val | 0x80000000))
{
return
;
}
// 在这里,如果在多核情况下,我们就需要再自旋等待一会了,
//因为在单核情况下,自旋等待是没有效果的,你都占用cpu了,其它拥有锁的进程又如何释放锁呢。
if
(ngx_ncpu > 1) {
for
(n = 1; n < mtx->spin; n <<= 1) {
// 每循环一次,就增加一倍的等待时间
// n = 1,2,4,8,16,32,64,128 ...
for
(i = 0; i < n; i++) {
// 如果当前体系结构支持,就让cpu等待一会,理由挺多的,可以降低cpu的占用率,当然省电也是一种理由啦
ngx_cpu_pause();
}
// 重新获取最新数据,然后再尝试加锁
val = *mtx->lock;
if
((val & 0x80000000) == 0
&& ngx_atomic_cmp_set(mtx->lock, val, val | 0x80000000))
{
return
;
}
}
}
// 如果是单核,就直接给别的进程执行了
// 否则,在自旋一段时间之后,如果还没有成功,则就让出cpu吧
ngx_sched_yield();
}
}
ngx_shmtx_unlock的实现就简单了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
void
ngx_shmtx_unlock(ngx_shmtx_t *mtx)
{
ngx_atomic_uint_t val, old, wait;
for
( ;; ) {
old = *mtx->lock;
wait = old & 0x7fffffff;
// 如果有加锁,那wait就是1,那val的值就是0
// 如果未加锁,那wait就是0,val还是0
val = wait ? wait - 1 : 0;
if
(ngx_atomic_cmp_set(mtx->lock, old, val)) {
break
;
}
}
}
|
可以看出,nginx实现的自旋锁还是非常高效的。不过,nginx对锁的实现相对简单,为降低锁的消耗需要编程者小心,尽量减小锁的粒度。而且nginx中没有实现读写锁。