Nginx之共享内存与slab机制

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_t {
    // 通常指向创建该共享内存模块的上下文结构体
    // 如对于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)
    //对于原子操作,该gile是没有意义的
    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机制对这部分内存进行两级管理,下面以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->size, 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 = '\0';
}

经过调用该ngx_slab_init函数后,此时共享内存的初始布局大致如下:

由该图知,slab机制对page页的静态管理主要体现在slab和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_slot_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_slot_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+i; 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;

} 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值