nginx是多进程的运行的一个程序,对需要跨进程访问的资源,nginx提供了共享内存上的slab来管理。这里我们实现一个在slab上的红黑树,这里把代码贴出来,对流程的理解都在代码的注释里!!
首先,我们在配置阶段来创建一块共享内存,然后再该共享内存上使用nginx的slab机制,把这块共享内存管理起来,方便我们分配红黑树相关的内容。
static void
ngx_jtxy_create_shared_memory(ngx_conf_t *cf, ngx_jtxy_conf_t* jtxy_cf)
{
ngx_shm_zone_t *shm_zone;
void* tag = (void*)&ngx_jtxy_module;
//在配置阶段调用ngx_shared_memory_add,
//说明要创建一块名为sean_share_data_name的共享内存,
//此时,并没有真正创建出来,要等配置阶段完成,
//nginx统计出所有模块要创建的所有共享内存后,
//会在ngx_init_cycle里真正的分配每一块有名的共享内存
//这里再提一下,再nginx里,几乎所有的add,都是返回出一个指针的做法,
//add函数内会分配出一块内存并返回这块内存的
//指针给调用者使用。
//比如他的list,array都是这样,拿到返回的指针后再初始化这个元素,
//而不像是我们常规的做法,先初始化好一个元素,
//再把这个元素添加到list,array里,这是因为nginx里list,
//array都是在pool里分配的,它的add实际是向pool申请一块内存出来使用。
shm_zone = ngx_shared_memory_add(cf,
&jtxy_share_uri_data_name, ngx_pagesize * 16, tag);
if (shm_zone->data) {
//多个location都配置了的,之后的就会到这里
ngx_log_error(NGX_LOG_EMERG, cf->cycle->log, 0,
"ngx_jtxy_create_shared_memory %V is already add",
&jtxy_share_uri_data_name);
ngx_jtxy_conf_t* last_jtxy_cf = (ngx_jtxy_conf_t*)shm_zone->data;
jtxy_cf->uri_shctx = last_jtxy_cf->uri_shctx;
return;
}
ngx_log_error(NGX_LOG_EMERG, shm_zone->shm.log, 0,
"ngx_jtxy_create_shared_memory %V, shm_zone %p, sean_cf %p, "
"shm_zone->data %p, noreuse %d, ngx_pagesize * 2 %d",
&jtxy_share_uri_data_name, shm_zone, jtxy_cf,
shm_zone->data, shm_zone->noreuse, ngx_pagesize * 2);
ngx_http_jtxy_shctx_t* shctx = (ngx_http_jtxy_shctx_t*)ngx_pcalloc(
cf->pool, sizeof(ngx_http_jtxy_shctx_t));
if (shctx == NULL) {
return;
}
jtxy_cf->uri_shctx = shctx;
//这里为什么要设置一下init函数和data值呢,因为上面讲到
//ngx_shared_memory_add时并没有真正的去分配出一块共享内存出来,
//他得等到统计完所有的模块后才会去真正分配一块共享内存出来
//到那时,共享内存分配出来了,就会给用户一个时机,
//调用这里设置的回调ngx_sean_shared_memory_init来做一些初始化的事情。
shm_zone->init = ngx_jtxy_shared_memory_init;
shm_zone->data = (void*)jtxy_cf;
shm_zone->noreuse = 1; //每次重启都新建共享内存
ngx_log_error(NGX_LOG_EMERG, shm_zone->shm.log, 0,
"ngx_sean_create_shared_memory 2 %V, shm_zone %p, "
"sean_cf %p, shm_zone->data %p, noreuse %d",
&jtxy_share_uri_data_name, shm_zone, jtxy_cf
, shm_zone->data, shm_zone->noreuse);
}
上面的函数就是我们在配置阶段开始分配共享内存 ,比如如下,我们就是在ngx_jtxy_block函数里调用ngx_jtxy_create_shared_memory。
typedef struct {
ngx_rbtree_t uri_rbtree; //统计共享内存中的红黑树
ngx_rbtree_node_t uri_rbtree_sentinel; //统计共享内存中的红黑树
} ngx_http_jtxy_shctx_rbtree_t; //共享内存下的红黑树
typedef struct {
ngx_slab_pool_t* uri_shpool; //统计的slab共享内存
ngx_http_jtxy_shctx_rbtree_t* rbtree; //统计共享内存中的红黑树
} ngx_http_jtxy_shctx_t; //共享内存下的红黑树
typedef struct
{
ngx_http_jtxy_shctx_t* uri_shctx; //保存共享内存的相关信息
}ngx_jtxy_conf_t;
static ngx_command_t ngx_jtxy_commands[] = {
{ ngx_string("jtxy"), // name
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LMT_CONF|NGX_CONF_NOARGS, //type
ngx_jtxy_block, // char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
NGX_HTTP_LOC_CONF_OFFSET, // conf
0, // offset
NULL }, // post
ngx_null_command
};
...
...
...
static char *ngx_jtxy_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
...
...
...
ngx_jtxy_conf_t* jtxy_cf = (ngx_jtxy_conf_t*)conf;
//uri的slab上的红黑树
ngx_jtxy_create_shared_memory(cf, jtxy_cf);
...
...
...
return NGX_CONF_OK;
}
在配置阶段调用ngx_shared_memory_add,说明要创建一块名为sean_share_data_name的共享内存,此时,并没有真正创建出来,要等配置阶段完成,nginx统计出所有模块要创建的所有共享内存后,会在ngx_init_cycle里真正的分配每一块有名的共享内存。因为可能在不同的location里会使用相同名称的共享内存,先做统一的名称统计,然后再分配。
这里再提一下,在nginx里,几乎所有的add函数,都是返回出一个指针的做法。add函数内会分配出一块内存并返回这块内存的指针给调用者使用。比如他的list,array都是这样,拿到返回的指针后再初始化这个元素,而不像是我们常规的做法,先初始化好一个元素,再把这个元素添加到list,array里,这是因为nginx里list,array都是在pool里分配的,它的add实际是向pool申请一块内存出来使用。
我们看看对shm_zone->data的判断和赋值
if (shm_zone->data) {
//多个location都配置了的,之后的就会到这里
ngx_log_error(NGX_LOG_EMERG, cf->cycle->log, 0,
"ngx_jtxy_create_shared_memory %V is already add",
&jtxy_share_uri_data_name);
ngx_jtxy_conf_t* last_jtxy_cf = (ngx_jtxy_conf_t*)shm_zone->data;
jtxy_cf->uri_shctx = last_jtxy_cf->uri_shctx;
return;
}
ngx_http_jtxy_shctx_t* shctx = (ngx_http_jtxy_shctx_t*)ngx_pcalloc(
cf->pool, sizeof(ngx_http_jtxy_shctx_t));
if (shctx == NULL) {
return;
}
jtxy_cf->uri_shctx = shctx;
shm_zone->init = ngx_jtxy_shared_memory_init;
shm_zone->data = (void*)jtxy_cf;
这里的逻辑是怎么回事呢?这个共享内存确实是在一个module里创建的,但是,如果我们在多个location里都配置了该模块,那么该共享内存的配置就应该保存到每个location的配置里,ngx_jtxy_create_shared_memory传进来的参数cf和jtxy_cf就是ngx_jtxy_block传入的每个location的上下文信息。注意不同的location的cf和jtxy_cf是不同的,指针指向不同的内存块。
这里呢,我们的共享内存块只有一块,他的shm_zone->data只能保存一个值,一个location的上下文保存在了shm_zone->data里,其他的location就保存不到shm_zone->data里了,那么其他location的上下文如何拿到共享内存的值呢,当然就是上面的if (shm_zone->data)判断里了,让其他location能拿到保存着共享内存信息的那片内存的指针。
然后看着两行代码
shm_zone->init = ngx_jtxy_shared_memory_init;
shm_zone->data = (void*)jtxy_cf;
这里为什么要设置一下init函数和data值呢,因为上面讲到ngx_shared_memory_add时并没有真正的去分配出一块共享内存出来,他得等到统计完所有的模块后才会去真正分配一块共享内存出来,到那时,共享内存分配出来了,就会给用户一个时机,调用这里设置的回调ngx_sean_shared_memory_init来做一些初始化的事情。接下来再看看ngx_sean_shared_memory_init函数。
static ngx_int_t
ngx_jtxy_shared_memory_init(ngx_shm_zone_t *shm_zone, void *data)
{
//void *data 共享内存重用时有用,就是上次(reload前)分配的共享内存
ngx_jtxy_conf_t* jtxy_cf = (ngx_jtxy_conf_t*)shm_zone->data; //ngx_sean_conf_t
//把这片共享内存设置为内存池,
//可以看到,这里并没有初始化shpool的ngx_slab_pool_t结构体,
//只是简单的把shm_zone->shm.addr赋值给了shpool,
//难道不用初始化shpool吗,其实初始化ngx_slab_pool_t的过程nginx已经做了。
//在ngx_init_zone_pool函数里
//ngx_init_cycle会调用ngx_init_zone_pool来完成。
//这里我们就只是保存一下,拿到他的指针而已
jtxy_cf->uri_shctx->uri_shpool = (ngx_slab_pool_t *)shm_zone->shm.addr;
if (shm_zone->shm.exists) {
//如果这片共享内存池之前已经存在了,拿一下之前的共享内存上的shctx
//为什么会有这段代码呢,因为我们创建的是有名的共享内存,
//即使nginx停掉了,这块内存还是存在的,
//等nginx重新启动后,拿到这片共享内存后就会在这里得到之前的上下文。
//为什么会重用这个共享内存呢,因为shm_zone->noreuse被设置为了0,
//这也nginx是默认设置(ngx_shared_memory_add里),它表明是否复用之前的共享内存
// jtxy_cf->uri_shctx = (ngx_http_jtxy_shctx_t*)jtxy_cf->uri_shctx->uri_shpool->data; /to do //
return NGX_OK;
}
//到这里呢我们就可以开始使用者片共享内存了,
//要注意的是所有的内存分配都要是在这片共享内存上
//这里只看到在shpool上分配内存了,实际shpool还要调用ngx_slab_init初始化,
//这个过程应该是系统里帮我们做了
//比如下面的代码,就是分配一个红黑树的东西并做初始化,
jtxy_cf->uri_shctx->rbtree = (ngx_http_jtxy_shctx_rbtree_t*)ngx_slab_alloc(
jtxy_cf->uri_shctx->uri_shpool, sizeof(ngx_http_jtxy_shctx_rbtree_t));
if (jtxy_cf->uri_shctx->rbtree == NULL) {
ngx_log_error(NGX_LOG_EMERG, shm_zone->shm.log, 0,
"ngx_slab_alloc %V, ngx_http_sean_shctx_t failed");
return NGX_ERROR;
}
//把分配出来的值存到uri_shpool得data上下文中,因为如果shm_zone->shm.exists存在,还要用的
jtxy_cf->uri_shctx->uri_shpool->data = jtxy_cf->uri_shctx->rbtree;
ngx_rbtree_init(&jtxy_cf->uri_shctx->rbtree->uri_rbtree
, &jtxy_cf->uri_shctx->rbtree->uri_rbtree_sentinel, ngx_str_rbtree_insert_value);
return NGX_OK;
}
我们看看这句代码
jtxy_cf->uri_shctx->uri_shpool = (ngx_slab_pool_t *)shm_zone->shm.addr;
可以看到,这里并没有初始化shpool的ngx_slab_pool_t结构体,只是简单的把shm_zone->shm.addr赋值给了shpool,难道不用初始化shpool吗,其实初始化ngx_slab_pool_t的过程nginx已经做了。在ngx_init_zone_pool函数里ngx_init_cycle会调用ngx_init_zone_pool来完成。这里我们就只是保存一下,拿到他的指针而已。
if (shm_zone->shm.exists)这句判断是什么意思呢?
如果这片共享内存池之前已经存在了,拿一下之前的共享内存上的shctx,为什么会有这段代码呢,因为我们创建的是有名的共享内存,即使nginx停掉了,这块内存还是存在的,等nginx重新启动后,拿到这片共享内存后就会在这里得到之前的上下文。
为什么会重用这个共享内存呢?
因为shm_zone->noreuse被设置为了0,这也nginx是默认设置(ngx_shared_memory_add里),它表明是否复用之前的共享内存。我们这里设置的是
shm_zone->noreuse = 1; //每次重启都新建共享内存
可以不管if内的处理。
接下来就是调用ngx_slab_alloc和ngx_rbtree_init来创建一个在共享内存上的红黑树了。这个看看上面的代码就明白了。
因为是多进程共享这一棵树,那么使用时就需要加锁。这里没有slab上的锁操作,是因为目前还在初始化阶段,不涉及资源竞争的问题。
当需要使用这棵树时是这样的,这时就需要加锁了
ngx_shmtx_lock(&jtxy_cf->uri_shctx->uri_shpool->mutex);
uriinfo_node = ngx_str_rbtree_lookup(&jtxy_cf->uri_shctx->rbtree->uri_rbtree, token, hash);
...
...
...
ngx_shmtx_unlock(&jtxy_cf->uri_shctx->uri_shpool->mutex);
ngx_shmtx_lock(&jtxy_cf->uri_shctx->uri_shpool->mutex);
ngx_rbtree_insert(&jtxy_cf->uri_shctx->rbtree->uri_rbtree, &jtxyreqinfo->key_node.node);
...
...
...
ngx_shmtx_unlock(&jtxy_cf->uri_shctx->uri_shpool->mutex);
最后,我们可以通过ngx_slab_stat模块开查看slab内存的使用情况,如下,ngx_slab_stat是Tengine提供的一个模块,我们可以把他编译进nginx里使用,方便了我们查看slab的内容。
Hello World!root@DESKTOP-EMC868D:~# curl -v --noproxy "*" http://127.0.0.1:8881/slab_stat
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8881 (#0)
> GET /slab_stat HTTP/1.1
> Host: 127.0.0.1:8881
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.20.0
< Date: Thu, 13 Jan 2022 08:02:09 GMT
< Content-Length: 2120
< Connection: keep-alive
<
* shared memory: sean_share_data
total: 16(KB) free: 8(KB) size: 4(KB)
pages: 8(KB) start:00007FFFF7FF4000 end:00007FFFF7FF7000
slot: 8(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 16(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 32(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 64(Bytes) total: 64 used: 1 reqs: 1 fails: 0
slot: 128(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 256(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 512(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 1024(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 2048(Bytes) total: 0 used: 0 reqs: 0 fails: 0
* shared memory: jtxy_share_uri_data
total: 64(KB) free: 56(KB) size: 4(KB)
pages: 56(KB) start:00007FFFF7FE4000 end:00007FFFF7FF3000
slot: 8(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 16(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 32(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 64(Bytes) total: 64 used: 1 reqs: 1 fails: 0
slot: 128(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 256(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 512(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 1024(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 2048(Bytes) total: 0 used: 0 reqs: 0 fails: 0
我们看
slot: 64(Bytes) total: 64 used: 1 reqs: 1 fails: 0
表示slot为64bytes的块有64个,使用了1个,请求了1次,失败0次
如果64个都使用完了呢,就会在分配一页出来给64bytes的slot使用。
total: 64(KB) free: 56(KB) size: 4(KB)
pages: 56(KB) start:00007FFFF7FE9000 end:00007FFFF7FF8000
slot: 8(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 16(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 32(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 64(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 128(Bytes) total: 32 used: 32 reqs: 32 fails: 0
slot: 256(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 512(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 1024(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 2048(Bytes) total: 0 used: 0 reqs: 0 fails: 0
比如上面,128bytes的32个slot都使用完了,我们再请求分配会如何呢,如下
slot: 8(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 16(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 32(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 64(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 128(Bytes) total: 64 used: 33 reqs: 33 fails: 0
slot: 256(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 512(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 1024(Bytes) total: 0 used: 0 reqs: 0 fails: 0
slot: 2048(Bytes) total: 0 used: 0 reqs: 0 fails: 0
128bytes的slot变为了64个,使用了33个。可见系统又拿出一页来给128bytes的slot使用