1. 共享内存
在 Nginx 里,一块完整的共享内存以结构体 ngx_shm_zone_t 来封装,如下:
typedef struct ngx_shm_zone_s ngx_shm_zone_t;
typedef ngx_int_t (*ngx_shm_zone_init_pt) (ngx_shm_zone_t *zone, void *data);
typedef struct {
/* 执行共享内存的起始地址 */
u_char *addr;
/* 共享内存的长度 */
size_t size;
/* 这块共享内存的名称 */
ngx_str_t name;
/* 记录日志的 ngx_log_t 对象 */
ngx_log_t *log;
/* 表示共享内存是否已经分配过的标志位,为 1 时表示已经存在 */
ngx_uint_t exists; /* unsigned exists:1 */
}ngx_shm_t;
struct ngx_shm_zone_s {
// 通常指向创建该共享内存模块的上下文结构体,
// 如对于 ngx_http_limit_req_module 模块,则指向
// ngx_http_limit_req_ctx_t 结构体
void *data;
// 描述了一块共享内存
ngx_shm_t shm;
// 初始回调函数
ngx_shm_zone_init_pt init;
void *tag;
ngx_uint_t noreuse; /* unsigned noreuse:1; */
};
- tag 与 shm.name:name 字段主要用作共享内存的唯一标识,它能让 Nginx 知道调用者想使用哪个共享内存,但它没法让 Nginx 区分user到底想创建一个共享内存,还是使用那个已经存在的旧的共享内存。如,模块 A 创建了共享内存 sa,模块 A 或另外一个模块 B 再以同样的名称 sa 去获取共享内存,那么此时 Nginx 是返回模块 A 已创建的那个共享内存 sa 给模块 A /模块 B,还是直接以共享内存名重复提示模块 A /模块 B 出错呢?因此新增一个 tag 字段做冲突标识,该字段一般指向当前模块的 ngx_module_t 变量即可。通过 tag 字段,如果模块A/模块B再以同样的名称 sa 去获取模块A已创建的共享内存sa,模块A将获得它之前创建的共享内存的引用(因为模块A前后两次请求的tag相同),而模块B则将获得共享内存已做他用的错误提示(因为模块B请求的tag与之前模块A请求的tag不同)。
使用共享内存,需要在配置文件里加上该共享内存的相关配置信息,而 Nginx 在进行配置解析的过程中,根据这些配置信息就会创建对应的共享内存,不过此时的创建仅仅只是代表共享内存的结构体 ngx_shm_zone_t 变量的创建。具体实现在函数 ngx_shared_memory_add 内。
下面以 ngx_http_limit_req_module 模块为例,讲述共享内存的创建,配置如下:
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
当检测到该配置项 limit_req_zone 时,ngx_http_limit_req_module 模块就会调用 ngx_http_limit_req_zone 函数进行解析,如下:
typedef struct {
ngx_http_limit_req_shctx_t *sh;
ngx_slab_pool_t *shpool;
/* integer value, 1 corresponds to 0.001 r/s */
ngx_uint_t rate;
ngx_http_complex_value_t key;
ngx_http_limit_req_node_t *node;
} ngx_http_limit_req_ctx_t;
static char *
ngx_http_limit_req_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
u_char *p;
size_t len;
ssize_t size;
ngx_str_t *value, name, s;
ngx_int_t rate, scale;
ngx_uint_t i;
ngx_shm_zone_t *shm_zone;
ngx_http_limit_req_ctx_t *ctx;
ngx_http_compile_complex_value_t ccv;
// 获取第一个参数,这里即为 "limit_req_zone"
value = cf->args->elts;
// 为 ngx_http_limit_req_ctx_t 结构体分配内存
ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_limit_req_ctx_t));
if (ctx == NULL) {
return NGX_CONF_ERROR;
}
ngx_memzero(&ccf, sizeof(ngx_http_compile_complex_value_t));
ccv.cf = cf;
// 获取第二个参数,即为 "$binary_remote_addr"
ccv.value = &value[1];
ccv.complex_value = &ctx->key;
if (ngx_http_compile_complex_value(&ccv) != NGX_OK) {
return NGX_CONF_ERROR;
}
size = 0;
rate = 1;
scale = 1;
name.len = 0;
for (i = 2; i < cf->args->nelts; i++) {
// value[2].data = zone=one:10m
if (ngx_strncmp(value[i].data, "zone=", 5) == 0) {
// name.data = one:10m
name.data = value[i].data + 5;
// p -> ":10m"
p = (u_char *) ngx_strchr(name.data, ':');
if (p == NULL) {
return NGX_CONF_ERROR;
}
// name.len = 3
name.len = p - name.data;
// s.data = "10m"
s.data = p + 1;
// s.len = 3
s.len = value[i].data + value[i].len - s.data;
// size = 10 * 1024 * 1024 = 10485760
size = ngx_parse_size(&s);
if (size == NGX_ERROR) {
return NGX_CONF_ERROR;
}
if (size < (ssize_t) (8 * ngx_pagesize)) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"zone "%V" is too small", &value[i]);
return NGX_CONF_ERROR;
}
continue;
}
// value[3].data = "rate=1r/s"
if (ngx_strncmp(value[i].data, "rate=", 5) == 0) {
// len = 9
len = value[i].len;
// p = "r/s"
p = value[i].data + len - 3;
if (ngx_strncmp(p, "r/s", 3) == 0) {
scale = 1;
// len = 6
len -= 3;
} else if (ngx_strncmp(p, "r/m", 3) == 0) {
scale = 60;
len -= 3;
}
// rate = 1
rate = ngx_atoi(value[i].data + 5, len - 5);
if (rate <= 0) {
return NGX_CONF_ERROR;
}
continue;
}
return NGX_CONF_ERROR;
}
if (name.len == 0) {
return NGX_CONF_ERROR;
}
// ctx->rate = 1000
ctx->rate = rate * 1000 / scale;
// 创建一个共享内存 ngx_shm_zone_t,并将该共享内存以list链表的形式
// 添加到 cf->cycle->shared_memory 下
shm_zone = ngx_shared_memory_add(cf, &name, size,
&ngx_http_limit_req_module);
if (shm_zone == NULL) {
return NGX_CONF_ERROR;
}
if (shm_zone->data) {
ctx = shm_zone->data;
return NGX_CONF_ERROR;
}
// 设置该共享内存的初始化函数
shm_zone->init = ngx_http_limit_req_init_zone;
shm_zone->data = ctx;
return NGX_CONF_OK;
}
该函数中会调用 ngx_shared_memory_add 为该 ngx_http_limit_req_module 模块创建一个共享内存,并将其以list链表的形式组织到全局变量 cf->cycle->shared_memory 下,具体实现如下:
ngx_shm_zone_t *
ngx_shared_memory_add(ngx_conf_t *cf, ngx_str_t *name, size_t size, void *tag)
{
ngx_uint_t i;
// 代表一块共享内存
ngx_shm_zone_t *shm_zone;
ngx_list_part_t *part;
part = &cf->cycle->shared_memory.part;
shm_zone = part->elts;
// 先遍历 shared_memory 链表,检测是否有与 name 相冲突的
// 共享内存,若name和size一样的,则直接返回该共享内存
for (i = 0; /* void */; i++) {
if (i >= part->nelts) {
if (part->next == NULL) {
break;
}
part = part->next;
shm_zone = part->elts;
i = 0;
}
if (name->len != shm_zone[i].shm.name.len) {
continue;
}
if (ngx_strncmp(name->data, shm_zone[i].shm.name.data, name->len)
!= 0)
{
continue;
}
if (tag != shm_zone[i].tag) {
return NULL;
}
if (shm_zone[i].shm.size == 0) {
shm_zone[i].shm.size = size;
}
if (size && size != shm_zone[i].shm.size) {
return NULL;
}
return &shm_zone[i];
}
// 从 shared_memory 链表中取出一个空闲项
shm_zone = ngx_list_push(&cf->cycle->shared_memory);
if (shm_zone == NULL) {
return NULL;
}
// 初始化该共享内存
shm_zone->data = NULL;
shm_zone->shm.log = cf->cycle->log;
// 由上面知 size 为 10m
shm_zone->shm.size = size;
// name = "one:10m"
shm_zone->shm.name = *name;
shm_zone->shm.exists = 0;
shm_zone->init = NULL;
// ngx_shm_zone_t 的 tag 字段指向 ngx_http_limit_req_module 变量
shm_zone->tag = tag;
shm_zone->noreuse = 0;
// 返回该表示共享内存的结构体
return shm_zone;
}
上面的执行仅是为 ngx_http_limit_req_module 模块创建 ngx_shm_zone_t 结构体变量并将其加入到全局链表 cf->cycle->shared_memory 中。共享内存的真正创建是在配置文件全部解析完后,所有代表共享内存的结构体 ngx_shm_zone_t 变量以链表的形式挂接在全局变量 cf->cycle->shared_memory 下,Nginx 此时遍历该链表并逐个进行实际创建,即分配内存、管理机制(如锁、slab)初始化等。具体代码如下所示:
ngx_cycle_t *
ngx_init_cycle(ngx_cycle_t *old_cycle)
{
...
/* create shared memory */
part = &cycle->shared_memory.part;
shm_zone = part->elts;
for (i = 0; /* void */ ; i++) {
if (i >= part->nelts) {
if (part->next == NULL) {
break;
}
part = part->next;
shm_zone = part->elts;
i = 0;
}
if (shm_zone[i].shm.size == 0) {
ngx_log_error(NGX_LOG_EMERG, log, 0,
"zero size shared memory zone "%V"",
&shm_zone[i].shm.name);
goto failed;
}
shm_zone[i].shm.log = cycle->log;
opart = &old_cycle->shared_memory.part;
oshm_zone = opart->elts;
// 检测是否冲突
for (n = 0; /* void */ ; n++) {
if (n >= opart->nelts) {
if (opart->next == NULL) {
break;
}
opart = opart->next;
oshm_zone = opart->elts;
n = 0;
}
if (shm_zone[i].shm.name.len != oshm_zone[n].shm.name.len) {
continue;
}
if (ngx_strncmp(shm_zone[i].shm.name.data,
oshm_zone[n].shm.name.data,
shm_zone[i].shm.name.len)
!= 0)
{
continue;
}
if (shm_zone[i].tag == oshm_zone[n].tag
&& shm_zone[i].shm.size == oshm_zone[n].shm.size
&& !shm_zone[i].noreuse)
{
shm_zone[i].shm.addr = oshm_zone[n].shm.addr;
#if (NGX_WIN32)
shm_zone[i].shm.handle = oshm_zone[n].shm.handle;
#endif
if (shm_zone[i].init(&shm_zone[i], oshm_zone[n].data)
!= NGX_OK)
{
goto failed;
}
goto shm_zone_found;
}
ngx_shm_free(&oshm_zone[n].shm);
break;
}
// 分配新的共享内存,由前面分析知大小为 10m
if (ngx_shm_alloc(&shm_zone[i].shm) != NGX_OK) {
goto failed;
}
// 共享内存分配成功后,调用该函数进行共享内存管理机制的初始化
// 具体分析看下面的 slab 机制 一节
if (ngx_init_zone_pool(cycle, &shm_zone[i]) != NGX_OK) {
goto failed;
}
// 该 shm_zone[i].init 是各个共享内存所特定的,根据使用方的自身需求不同而不同
if (shm_zone[i].init(&shm_zone[i], NULL) != NGX_OK) {
goto failed;
}
shm_zone_found:
continue;
}
...
}
2. slab 机制
Nginx 的 slab 机制主要是和共享内存一起使用,Nginx 在解析完配置文件,把即将使用的共享内存全部以 list 链表的形式组织在全局变量 cf->cycle->shared_memory 下之后,就会统一进行实际的内存分配,而 Nginx 的 slab 机制要做的就是对这些共享内存进行进一步的内部划分与管理。
先看 ngx_init_zone_pool 函数:
static ngx_int_t ngx_init_zone_pool(ngx_cycle_t *cycle, ngx_shm_zone_t *zn)
{
u_char *file;
ngx_slab_pool_t *sp;
// 指向该共享内存的起始地址处
sp = (ngx_slab_pool_t *) zn->shm.addr;
// 判断该共享内存是否已经存在了
if (zn->shm.exists) {
if (sp == sp->addr) {
return NGX_OK;
}
#if (NGX_WIN32)
...
#endif
return NGX_ERROR;
}
// 指向共享内存的末尾
sp->end = zn->shm.addr + zn->shm.size;
sp->min_shift = 3;
// 指向共享内存的起始
sp->addr = zn->shm.addr;
// 对于互斥锁,优先使用支持原子操作的互斥锁
#if (NGX_HAVE_ATOMIC_OPS)
// 对于原子操作,该file是没有意义的
file = NULL;
#else
// 这里代表使用的是文件锁
file = ngx_pnalloc(cycle->pool, cycle->lock_file.len + zn->shm.name.len);
if (file == NULL) {
return NGX_ERROR;
}
(void) ngx_sprintf(file, "%V%V%Z", &cycle->lock_file, &zn->shm.name);
#endif
// 初始化一个信号量:对于Nginx,若支持原子操作,则优先使用
// 原子变量实现的信号量
if (ngx_shmtx_create(&sp->mutex, &sp->lock, file) != NGX_OK) {
return NGX_ERROR;
}
ngx_slab_init(sp);
return NGX_OK;
}
ngx_init_zone_pool() 函数是在共享内存分配好后进行的初始化调用,而该函数先调用 ngx_shmtx_create 函数为该共享内存初始化好一个信号量,接着调用 slab 的初始化函数 ngx_slab_init()。此时,该新分配的共享内存初始布局图如下:
有该图可知,共享内存的开始部分内存已经被用作结构体 ngx_slab_pool_t 的存储空间,这相当于是 slab 的额外开销(overhead),后面还会有其他额外开销,任何一种管理机制都有自己的一些控制信息需要存储,所以这些内存使用是无法避免的。共享内存剩下的部分才是被管理的主体,slab 机制对这部分内存进行两级管理,首先是 page 页,然后是 page 页内的 slab 块(通过 slot 对相等大小的 slab 块进行管理,下面以 slot 块来指代这些 slab 块),也就是说 slot 块是在 page 页内存的再一次管理。
如下变量是由系统环境(假设当前系统为 64 位)确定的,是常量值:
- ngx_pagesize:4096
- 描述:系统内存页大小,Linux 下一般情况是 4KB。
- ngx_pagesize_shift:12
- 描述:对应 ngx_pagesize(4096),即是 4096 = 1 << 12。
- ngx_slab_max_size:2048
- 描述:slots 分配和 pages 分配的分割点,大于等于该值则需从 pages 里分配。
- ngx_slab_exact_size:64
- 描述:正好能用一个 uintptr_t 类型的位图变量表示的页划分;比如在 4KB 内存页、64 位系统环境下,一个 uintptr_t 类型的位图变量最多可以对应表示 64 个划分块的状态,所以要恰好完整地表示一个 4KB 内存页的每一个划分块状态,必须把这个 4KB 内存页划分为 64 块,即每一块大小为:ngx_slab_exact_size = 4096 / 64 = 64.
- ngx_slab_exact_shift:4
- 描述:对应 ngx_slab_exact_size(128),即是 128 = 1 << 4.
- pool->min_shift:3
- 描述:固定值为 3
- pool->min_size:8
- 描述:固定值为 8,最小划分块大小,即是 1 << pool->min_shift.
2.1 page 页的静态管理
page 页的静态管理是通过 ngx_slab_init 函数完成的。
下面先分析 ngx_slab_init,该函数主要对整个共享内存的使用进行划分:
void
ngx_slab_init(ngx_slab_pool_t *pool)
{
u_char *p;
size_t size;
ngx_int_t m;
ngx_uint_t i, n, pages;
ngx_slab_page_t *slots, *page;
/* STUB */
if (ngx_slab_max_size == 0) {
// ngx_slab_max_size = 2048: 该变量为 slots 分配和 pages
// 分配的分割点,大于等于该值则需要从 pages 里分配.
ngx_slab_max_size = ngx_pagesize / 2;
// ngx_slab_exact_size = 64: 正好能用一个 uintptr_t 类型的位图变量表示的
// 的页划分;比如在 4KB 内存页、64位系统环境下,一个 uintptr_t 类型的位图
// 变量最多可以对应表示 64 个划分块的状态,所以要恰好完整地表示一个 4KB
// 内存页的每一个划分块状态,必须把这个 4KB 内存页划分为 64 块,即每一块大小
// 为 ngx_slab_exact_size = 4096 / (8 * 8) = 64
ngx_slab_exact_size = ngx_pagesize / (8 * sizeof(uintptr_t));
// ngx_slab_exact_shift = 6: 对应 ngx_slab_exact_size(64),即是 64 = 1 << 6
for (n = ngx_slab_exact_size; n >>= 1; ngx_slab_exact_shift++) {
/* void */
}
}
// pool->min_shift = 3, pool->min_size = 8
pool->min_size = (size_t) 1 << pool->min_shift;
/* ngx_slab_slots 宏实际是将 (pool) + sizeof(ngx_slab_pool_t) 后的起始地址
* 赋给 slots */
slots = ngx_slab_slots(pool);
p = (u_char *) slots;
// 计算此块共享内存中除去 pool (即ngx_slab_pool_t结构体)所占的内存后
// 余下的内存大小
size = pool->end - p;
ngx_slab_junk(p, size);
// n = 9
n = ngx_pagesize_shift - pool->min_shift;
// 初始化 slots 数组,大小为 9 * sizeof(ngx_slab_page_t)
for (i = 0; i < n; i++) {
/* only "next" is used in list head */
slots[i].slab = 0;
slots[i].next = &slots[i];
slots[i].prev = 0;
}
// p 指针向前移动,指向slots数组的末尾
p += n * sizeof(ngx_slab_page_t);
// pool->start 指向 slots 数组的末尾
pool->stats = (ngx_slab_stat_t *) p;
// 将 pool->stats[9] 数组置零
ngx_memzero(pool->stats, n * sizeof(ngx_slab_stat_t));
// p 再次向前移动,指向 pool->stats[9] 数组的尾部
p += n * sizeof(ngx_slab_stat_t);
// 计算余下共享内存的大小,此时,共享内存已经用于三块:
// ngx_slab_pool_t + n * (sizeof(ngx_slab_page_t) + sizeof(ngx_slab_stat_t)
size -= n * (sizeof(ngx_slab_page_t) + sizeof(ngx_slab_stat_t));
// 计算余下的共享内存中可以分成多少个 page(每个 page 为 4096 +
// 额外的 sizeof(ngx_slab_page_t) 大小)
// 由前面知,此时 pages = 2544
pages = (ngx_uint_t) (size / (ngx_pagesize + sizeof(ngx_slab_page_t)));
// 将 pool->pages 指向 pool->stats[9] 数组的尾部
pool->pages = (ngx_slab_page_t *) p;
ngx_memzero(pool->pages, pages * sizeof(ngx_slab_page_t));
page = pool->pages;
/* only "next" is used in list head */
pool->free.slab = 0;
pool->free.next = page;
pool->free.prev = 0;
// pages 的大小为余下共享内存中可以划分的页数
// 每页(大小为 4096 + sizeof(ngx_slab_page_t))
page->slab = pages;
// pool->free 代表当前空闲的页
page->next = &pool->free;
page->prev = (uintptr_t) &pool->free;
// 指向经过对齐后的新地址起始处
pool->start = ngx_align_ptr(p + pages * sizeof(ngx_slab_page_t),
ngx_pagesize);
m = pages - (pool->end - pool->start) / ngx_pagesize;
if (m > 0) {
pages -= m;
page->slab = pages;
}
pool->last = pool->pages + pages;
pool->pfree = pages;
pool->log_nomem = 1;
pool->log_ctx = &pool->zero;
pool->zero = ' ';
}
经过调用该 ngx_slab_init 函数后,此时共享内存的初始布局大致如下:
由该图知,slab 机制对 page 页的静态管理主要体现在 slots 和 page 这两个数组上,以下有几点需要注意:
- 虽然是一个页管理结构(即 ngx_slab_page_t 元素)与一个 page 内存页(大小 4096)相对应,但因为有对齐消耗以及 slot 块管理结构体的占用(图中的 slots 数组),所以实际上页管理结构体数目比 page 页内存数目要多。
- 如何根据页管理结构 page 获得对应内存页的起始地址 p? 计算方法如下:
p = (page - pool->pages) << ngx_pagesize_shift;
p += (uintptr_t) pool->start;
- 对齐是指实际 page 内存页按 ngx_pagesize 大小对齐,对齐能提高对内存页的访问速度,但有一些内存浪费,并且末尾可能因为不够一个 page 内存页而被浪费掉,所以在 ngx_slab_init 函数的最末尾有一次最终可用内存页的准确调整。
m = pages - (pool->end - pool->start) / ngx_pagesize;
// 若 m 大于 0,说明对齐等操作导致实际可用内存页数减少
// 所以 if 语句里进行调整
if (m > 0) {
pages -= m;
page->slab = pages;
}
2.2 page 页的动态管理
page 页的动态管理即为 page 页的申请与释放。
先看空闲页的管理,Nginx 对空闲 page 页进行链式管理,链表的头节点 pool->free,初始状态下的空闲链表情况如下:
由该图可知,该空闲链表的节点是一个数组,如上图中的 ngx_slab_page_t[N] 数组就是一个链表节点,这个数组通过第 0 号数组元素,即 ngx_slab_page_t[0],接入到这个空闲 free 页链表内,并且整个数组的元素个数也记录在这个第 0 号数组元素的 slab 字段内。
下面开始从该共享内存中申请 size 大小的内存,具体函数为 ngx_slab_alloc:
void *
ngx_slab_alloc(ngx_slab_pool_t *pool, size_t size)
{
void *p;
// 获取互斥锁
ngx_shmtx_lock(&pool->mutex);
p = ngx_slab_alloc_locked(pool, size);
// 释放互斥锁
ngx_shmtx_unlock(&pool->mutex);
return p;
}
该函数主要调用 ngx_slab_alloc_locked 做进一步的处理。
void *
ngx_slab_alloc_locked(ngx_slab_pool_t *pool, size_t size)
{
size_t s;
uintptr_t p, n, m, mask, *bitmap;
ngx_uint_t i, slot, shift, map;
ngx_slab_page_t *page, *prev, *slots;
// 由前面知 ngx_slab_max_size 为 2048,该变量是为 slots 分配和 pages
// 分配的分割点,大于该值则需要从 pages 里分配.
if (size > ngx_slab_max_size) {
page = ngx_slab_alloc_pages(pool, (size >> ngx_pagesize_shift)
+ ((size % ngx_pagesize) ? 1 : 0));
if (page) {
p = ngx_slab_page_addr(pool, page);
} else {
p = 0;
}
goto done;
}
// pool->min_size 为固定值 8
if (size > pool->min_size) {
shift = 1;
for (s = size - 1; s >>= 1; shift++) { /* void */ }
// 计算该页划分的 slot 块的大小(该 slot 大小对应的移位值)
slot = shift - pool->min_shift;
} else {
shift = pool->min_shift;
slot = 0;
}
// 设置该 slot 的引用计数加1
pool->stats[slot].reqs++;
// slots 指向共享内存中 slots 数组的首地址
slots = ngx_slab_slots(pool);
// page 指向 slots[slot] 元素的首地址
page = slots[slot].next;
// 最开始的时候,即还没有开始分配页时,page->next 等于 page
if (page->next != page) {
if (shift < ngx_slab_exact_shift) {
bitmap = (uintptr_t *) ngx_slab_page_addr(pool, page);
map = (ngx_pagesize >> shift) / (sizeof(uintptr_t) * 8);
for (n = 0; n < map; n++) {
if (bitmap[n] != NGX_SLAB_BUSY) {
for (m = 1, i = 0; m; m <<= 1, i++) {
if (bitmap[n] & m) {
continue;
}
bitmap[n] |= m;
i = (n * sizeof(uintptr_t) * 8 + i) << shift;
p = (uintptr_t) bitmap + i;
pool->stats[slot].used++;
if (bitmap[n] == NGX_SLAB_BUSY) {
for (n = n + 1; n < map; n++) {
if (bitmap[n] != NGX_SLAB_BUSY) {
goto done;
}
}
prev = ngx_slab_page_prev(page);
prev->next = page->next;
page->next->prev = page->prev;
page->next = NULL;
page->prev = NGX_SLAB_SMALL;
}
goto done;
}
}
}
} else if (shift == ngx_slab_exact_shift) {
for (m = 1, i = 0; m; m <<= 1, i++) {
if (page->slab & m) {
continue;
}
page->slab |= m;
if (page->slab == NGX_SLAB_BUSY) {
prev = ngx_slab_page_prev(page);
prev->next = page->next;
page->next->prev = page->prev;
page->next = NULL;
page->prev = NGX_SLAB_EXACT;
}
p = ngx_slab_page_addr(pool, page) + (i << shift);
pool->stats[slot].used++;
goto done;
}
} else { * shift > ngx_slab_exact_shift */
mask = ((uintptr_t) 1 << (ngx_pagesize >> shift)) - 1;
mask <<= NGX_SLAB_MAP_SHIFT;
for (m = (uintptr_t) 1 << NGX_SLAB_MAP_SHIFT, i = 0;
m & mask;
m <<= 1, i++)
{
if (page->slab & m) {
continue;
}
page->slab |= m;
if ((page->slab & NGX_SLAB_MAP_MASK) == mask) {
prev = ngx_slab_page_prev(page);
prev->next = page->next;
page->next->prev = page->prev;
page->next = NULL;
page->prev = NGX_SLAB_BIG;
}
p = ngx_slab_page_addr(pool, page) + (i << shift);
pool->stats[slot].used++;
goto done;
}
}
ngx_slab_error(pool, NGX_LOG_ALERT, "ngx_slab_alloc(): page is busy");
ngx_debug_point();
}
// 从空闲页链表中申请 1 页
page = ngx_slab_alloc_pages(pool, 1);
// 申请页成功
if (page) {
// 申请的内存小于 ngx_slab_exact_size 的情况
// 假设按 8 字节划分,则 1 个 4KB 的 page 页将被划分为 512 块,
// 表示各个 slot 块状态的位图也就需要 512 个 bit 位,一个 slab
// 字段明显不足,所以需要为位图另找存储空间,而 slab 字段仅用于
// 存储 slot 块大小(仅存其对应的移位数).
if (shift < ngx_slab_exact_shift) {
bitmap = (uintptr_t *) ngx_slab_page_addr(pool, page);
// n = (4096 >> 3) / ((1 << 3) * 8) = 8
n = (ngx_pagesize >> shift) / ((1 << shift) * 8);
if (n == 0) {
n = 1;
}
/* "n" elements for bitmap, plus one requested */
bitmap[0] = ((uintptr_t) 2 << n) - 1;
// map = (4096 >> 3) / (8 * 8) = 8
map = (ngx_pagesize >> shift) / (sizeof(uintptr_t) * 8);
for (i = 1; i < map; i++) {
bitmap[i] = 0;
}
// slab 仅用于存储 slot 块大小(仅存其对应的移位数)
page->slab = shift;
page->next = &slots[slot];
page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_SMALL;
slots[slot].next = page;
pool->stats[slot].total += (ngx_pagesize >> shift) - n;
p = ngx_slab_page_addr(pool, page) + (n << shift);
pool->stats[slot].used++;
goto done;
} else if (shift == ngx_slab_exact_shift) {
page->slab = 1;
page->next = &slots[slot];
page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_EXACT;
slots[slot].next = page;
pool->stats[slot].total += sizeof(uintptr_t) * 8;
p = ngx_slab_page_addr(pool, page);
pool->stats[slot].used++;
goto done
} else { /* shift > ngx_slab_exact_shift */
// 当划分的每个 slot 块比 ngx_slab_exact_size 还大,意味着一个
// 一个 page 页划分的 slot 块数更少,此时同样使用 ngx_slab_page_t
// 结构体的 slab 字段作为位图。由于比 ngx_slab_exact_size 大的
// 划分可以有很多种,因此需要把其具体的大小记录下来,这个值同样
// 记录在 slab 字段里。如下,slab 字段的高端 bit 用作位图,低端
// bit 用于存储 slot 块的大小(仅存其对应的移位数):
page->slab = ((uintptr_t) 1 << NGX_SLAB_MAP_SHIFT) | shift;
page->next = &slots[slot];
page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_BIG;
slots[slot].next = page;
pool->stats[slot].total += ngx_pagesize >> shift;
// 计算出该页实际提供给用户使用的内存的起始地址,
// 将该起始地址返回给调用者
p = ngx_slab_page_addr(pool, page);
pool->stats[slot].used++;
goto done;
}
}
p = 0;
pool->stats[slot].fails++;
done:
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, ngx_cycle->log, 0,
"slab alloc: %p", (void *) p);
return (void *) p;
}
当首次开始调用该函数进行分配的时候,即之前从没有分配过页,则会先调用 ngx_slab_alloc_pages 函数分配一页.
/**
* @pages: 请求分配的页数
*/
static ngx_slab_page_t *
ngx_slab_alloc_pages(ngx_slab_pool_t *pool, ngx_uint_t pages)
{
ngx_slab_page_t *page, *p;
// 遍历空闲页 free 链表
for (page = pool->free.next; page != &pool->free; page = page->next) {
// 检测当前共享内存总的页数是否大于请求分配的页数
if (page->slab >= pages) {
if (page->slab > pages) {
page[page->slab - 1].prev = (uintptr_t) &page[pages];
// 计算当前共享内存中余下空闲的页数
page[pages].slab = page->slab - pages;
// 使 page[pages].next 指向 pool->free 的首地址
page[pages].next = page->next;
// 使 page[pages].prev 指向 pool->free 的首地址
page[pages].prev = page->prev;
// p 指向 pool->free 首地址
p = (ngx_slab_page_t *) page->prev;
p->next = &page[pages];
// pool->free->prev 指向当前空闲链表中的第一个空闲页
page->next->prev = (uintptr_t) &page[pages];
// page->slab = pages
} else {
p = (ngx_slab_page_t *) page->prev;
p->next = page->next;
page->next->prev = page->prev;
}
// 将该页从空闲页 free 链表中移除
page->slab = pages | NGX_SLAB_PAGE_START;
page->next = NULL;
page->prev = NGX_SLAB_PAGE;
// 共享内存中空闲页数减 pages
pool->pfree -= pages;
// 上面从 for 循环开始到这里即完成一个页的申请,
// 即将原先空闲页链表中的第一个空闲页从空闲页链表中移除,
// 并将pool->free 指向原先空闲页链表中的第一个空闲页的下一个空闲页
// pages 为 0,表示分配页已经完成了
if (--pages == 0) {
return page;
}
// 若当前申请的页数pages大于1,则设置当前已经申请出去的页的接下来
// 几个页(直到满足所要申请的页数为止)都将其从空闲链表中移除
for (p = page + 1; pages; pages--) {
p->slab = NGX_SLAB_PAGE_BUSY;
p->next = NULL;
p->prev = NGX_SLAB_PAGE;
p++;
}
// 申请多页的情况下,这里返回所申请的多页的第一页,
// 因为页都是连续的,因此可根据该页遍历所有申请的页.
return page;
}
}
if (pool->log_nomem) {
ngx_slab_error(pool, NGX_LOG_CRIT,
"ngx_slab_alloc() failed: no memory");
}
return NULL;
}
假设进程A首次从共享内存中申请 1 页,则经过 ngx_slab_alloc_pages 函数后,此刻共享内存中页的布局情况如下图所示.
假设进程B接着调用 ngx_slab_alloc_pages 函数申请了 2 页,此时共享内存中页的布局情况如下图:
假设接着进程A调用 ngx_slab_free_pages 函数将刚才申请的 1 页释放了,具体代码如下:
static void
ngx_slab_free_pages(ngx_slab_pool_t *pool, ngx_slab_page_t *page,
ngx_uint_t pages)
{
ngx_slab_page_t *prev, *join;
// 更新共享内存中空闲页数,即将其加上当前释放的页数
pool->pfree += pages;
page->slab = pages--;
if (pages) {
ngx_memzero(&page[1], pages * sizeof(ngx_slab_page_t));
}
if (page->next) {
prev = ngx_slab_page_prev(page);
prev->next = page->next;
page->next->prev = page->prev;
}
join = page + page->slab;
if (join < pool->last) {
if (ngx_slab_page_type(join) == NGX_SLAB_PAGE) {
if (join->next != NULL) {
pages += join->slab;
page->slab += join->slab;
prev = ngx_slab_page_prev(join);
prev->next = join->next;
join->next->prev = join->prev;
join->slab = NGX_SLAB_PAGE_FREE;
join->next = NULL;
join->prev = NGX_SLAB_PAGE;
}
}
}
if (page > pool->pages) {
join = page - 1;
if (ngx_slab_page_type(join) == NGX_SLAB_PAGE) {
if (join->slab == NGX_SLAB_PAGE_FREE) {
join = ngx_slab_page_prev(join);
}
if (join->next != NULL) {
pages += join->slab;
join->slab += page->slab;
prev = ngx_slab_page_prev(join);
prev->next = join->next;
join->next->prev = join->prev;
page->slab = NGX_SLAB_PAGE_FREE;
page->next = NULL;
page->prev = NGX_SLAB_PAGE;
page = join;
}
}
}
if (pages) {
page[pages].prev = (uintptr_t) page;
}
page->prev = (uintptr_t) &pool->free;
page->next = pool->free.next;
page->next->prev = (uintptr_t) page;
pool->free.next = page;
}
此时共享内存中页的布局如下图所示:
由图知,释放的页被插入到 free 空闲页链表的头部.
假设进程 B 将其申请的 2 页也释放了,则共享内存中空闲页的布局如下图:
由图知,Nginx 对空闲 page 页的链式管理不会进行节点合并。
下面分析 slab 机制的第二级管理机制,即 slot 块。
slot 块是对每一页 page 内存的内存管理,它将 page 页划分为很多小块,各个 page 页的 slot 块大小可以不相等,但同一个 page 页的 slot 块大小一定是相等。page 页的状态通过其所在的链表即可辨明,而 page 页内各个 slot 块的状态却需要一个额外的标记,在 Nginx 的具体实现里采用的是位图方式,即一个 bit 位标记一个对应的 slot 块的状态, 1 为使用,0 为空闲。
根据 slot 块的大小不同,一个 page 页可划分的 slot 块数也不相同,从而需要的位图大小也不一样。每一个 page 页对应一个名为 ngx_slab_page_t 的管理结构,该结构体有一个 uintptr_t 类型的 slab 字段。在 64 位平台上,uintptr_t 类型占 8 个字节,即 slab 字段有 64 个bit位。如果 page 页划分的 slot 数小于等于 64,那么 Nginx 直接利用该字段充当位图,这在 Nginx 内叫 exact 划分,每个 slot 块的大小保存在全局变量 ngx_slab_exact_size 以及 ngx_slab_exact_shift 内。比如,1 个 4KB 的 page 页,如果每个 slot 块大小为 64 字节,那么恰好可以划分成 64 块。
申请的内存大于 ngx_slab_exact_size 的情况
如果划分的每个 slot 块比 ngx_slab_exact_size 还大,那意味着一个 page 划分的 slot 块数更少,此时也是使用 ngx_slab_page_t 结构体的 slab 字段作为位图。由于比 ngx_slab_exact_size 大的划分可以有很多种,所以需要把其具体的大小也记录下来,这个值同样记录在 slab 字段里。由于划分总是按 2 次幂增长,所以比 ngx_slab_exact_size 还大的划分至少要减少一半的 slot 块数,因此利用 slab 字段的一半 bit 位即可完整表示所有 slot 块的状态。具体点说就是:slab 字段的高端 bit 用作位图,低端 bit 用于存储 slot 块的大小(仅存其对应的移位数)。具体代码如下:
page->slab = ((uintptr_t) 1 << NGX_SLAB_MAP_SHIFT) | shift;
如果要申请的内存大于等于 ngx_slab_max_size,Nginx 直接返回一个 page 整页,此时不在 slot 块管理里。
申请的内存小于 ngx_slab_exact_size 的情况
假若申请的内存小于 ngx_slab_exact_size 的情况,此时 slot 块数目已经超过了 slab 字段可表示的容量。比如假设按 8 字节划分,那个 1 个 4KB 的 page 页将被划分为 512 块,表示各个 slot 块的状态的位图也就需要 512 个 bit 位,一个 slab 字段明显是不够的,所以需要为位图另找存储空间,而 slab 字段仅用于存储 slot 块大小(仅存其对应的移位数)。
另找的位图存储空间就在 page 页内,具体点说时其划分的前面几个 slot 块内。512 个 bit 位的位图,即 64 个字节,而一个 slot 块有 8 个字节,所以就需要占用 page 页的前 8 个 slot 块用作位图。一个按 8 字节划分 slot 块的 page 页初始情况如下图所示:
由于前几个 slot 块一开始就被用作位图空间,所以必须把它们对应的 bit 位设置为 1,表示其状态为使用。具体代码片段如下:
if (shift < ngx_slab_exact_shift) {
bitmap = (uintptr_t *) ngx_slab_page_addr(pool, page);
n = (ngx_pagesize >> shift) / ((1 << shift) * 8);
if (n == 0) {
n = 1;
}
/* "n" elements for bitmap, plus one requested */
bitmap[0] = ((uintptr_t) 2 << n) - 1;
map = (ngx_pagesize >> shift) / (sizeof(uintptr_t) * 8);
for (i = 1; i < map; i++) {
bitmap[i] = 0;
}
page->slab = shift;
page->next = &slots[slot];
page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_SMALL;
slots[slot].next = page;
pool->stats[slot].total += (ngx_pagesize >> shift) - n;
p = ngx_slab_page_addr(pool, page) + (n << shift);
pool->stats[slot].used++;
goto done;
}