任何一个程序都少不了和内存打交道,nginx 作为一个稳定的后台常驻进程更需要对内存进 行有效的管理。

Web server 需要完成很多字符串处理,并且这些字符串是不定长的,直接使用系统调用会 降低性能和带来内存碎片;而使用如 slab allocator 那样的定长内存管理机制的话,也会 带来较多的内存浪费。Nginx 和 Apache 这些 Web server 根据 Web 业务的类型 (请求和 连接对象生命周期都是很短的,并且是一次性的) 设计出了和对象生命周期相关联的内存池 管理数据结构。

基本数据结构

    ---------core/ngx_palloc.h:40--------
    struct ngx_pool_large_s {
        ngx_pool_large_t    *next;
        void                *alloc; /* 大块内存的起始地址 */
    };
    typedef ngx_pool_large_s ngx_pool_large_t;

    typedef struct {
        u_char              *last; /* 可用内存起始地址 */
        u_char              *end;  /* 可用内存终止地址 */
        ngx_pool_t          *next; /* 一个内存池可以由多个 `ngx_pool_t` 组成 */
        ngx_uint_t          failed;
    } ngx_pool_data_t;

    struct ngx_pool_s {
        ngx_pool_data_t     d;
        size_t              max;   /* 可从此内存池中申请的最大内存块大小 */
        ngx_pool_t          *current; /* 当前正在使用的 `ngx_pool_t` */
        ngx_chain_t         *chain;
        ngx_pool_large_t    *large; /* 大块内存链表 */
        ngx_pool_cheanup_t  *cleanup; /* 从内存池分配出去的内存块可能被用来
                                         存储需要在内存释放时进行清理的资源,
                                         这个链表就是用来存储清理函数的 */
        ...
    };
    typedef struct ngx_pool_s ngx_pool_t;

内存池操作

先来看看内存池的示意图:

内存池初始化和销毁

内存池初始化使用 ngx_create_pool 函数,调用时传入的size参数指定创建的内存池初 始化大小和每次增加的 block 块大小。内存池总容量并没有上限。

    --------core/ngx_palloc.c:15--------------
    ngx_pool_t *
    ngx_create_pool(size_t size, ngx_log_t *log)
    {
        ngx_pool_t *p;
        /*
           使用 posix_memalign (转而又会调用 malloc), 申请size大小的内存,并
           且保证申请的内存起始地址按照 16 bytes 对齐。
         */
        p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
        ...
        p->d.last = (u_char *) p + sizeof(ngx_pool_t);
        p->d.end = (u_char *) p + size;
        p->d.next = NULL;
        p->d.failed = 0;

        size = size - sizeof(ngx_pool_t);
        p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

        p->current = p;
        p->chain = NULL;
        p->large = NULL;
        p->cleanup = NULL;
        p->log = log;

        return p;
    }

几个点: * ngx_pool_t 和可用内存空间连续存放。

内存池销毁使用 ngx_destroy_pool 函数:

    --------core/ngx_palloc.c:43-------------
    void
    ngx_destroy_pool(ngx_pool_t *pool)
    {
        ...
        for (c = pool->cleanup; c; c = c->next) {
            if (c->handler) {
                ...
                c->handler(c->data);
            }
        }

        for (l = pool->large; l; l = l->next) {
            ...
            if (l->alloc) {
                ngx_free(l->alloc);
            }
        }
        ...
        for (p = pool, n = pool->d.next; ; p = n, n = n->d.next) {
            ngx_free(p);

            if (n == NULL) {
                break;
            }
        }
    }

内存申请

如果需要从内存池申请的内存块大小大于 pool->max 的话,直接使用 malloc 从操作 系统中申请,并将内存块挂接到 pool->large 链表中,方便在内存池销毁时进行释放。

如果需要从内存池申请的内存块大小小于 pool->max 的话,尝试从内存池已经申请到的 内存 block 中分配。如果内存池的当前 block 里没有足够的空闲空间的话,就需要从系统 中申请一个新的内存 block,并从新 block 中分配申请的内存块。新 block 会链接到 pool 的 block 链表的末尾 (d.next)。

    p = pool->current;

    do {
        m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);

        if ((size_t) (p->d.end - m) >= size) {
            p->d.last = m + size;
            return m;
        }

        p = p->d.next;
    } while (p);

    return ngx_palloc_block(pool, size);

几个点:

  • 界定小内存块和大内存块的值是 min (size, NGX_MAX_ALLOC_FROM_POOL (4095))

  • 随着内存池中的block增多,如果block链表的前面的block元素都没内存可分配,还每次 从链表头开头查找可用block的话,性能也是一种损耗。nginx 使用 pool->failed 字段 来记录没有从此 block 成功分配到内存的次数,如果超过 6 次,随后的内 存申请直接略过检查此 block (修改 current 指针)。

  • 从pool中申请的内存起始地址都按 NGX_ALIGNMENT (16) 对齐。

  • 除了第一个 ngx_pool_t 所在的内存块需要存储完整的 ngx_pool_t 结构体外,后续 的内存块只需要存储 ngx_pool_data_t 结构体。一方面是,一个内存池不需要多个 ngx_pool_t 结构体。另一方面是,可以腾出更多的空间用于存储用户数据 (一个 block 可以腾出 sizeof(ngx_pool_t) - sizeof(ngx_pool_data_t) 个字节空间)。

内存释放

前面已经提到了,nginx 内存池其实是 Region memory allocator,整个内存池会被统一 释放。但是对 large 的内存块,nginx 也会尝试先行释放:

    -----------core/ngx_palloc.c:279-------------
    ngx_int_t
    ngx_pfree(ngx_pool_t *pool, void *p)
    {
        for (l = pool->large; l; l = l->next) {
            if (p == l->alloc) {
                ...
                ngx_free(l->alloc);
                l->alloc = NULL;

                return NGX_OK;
            }
        }

        return NGX_DECLINED;
    }

内存池在Nginx中的使用

下面分析几个 Nginx 中内存池使用的例子。

数组结构

Nginx 实现的基础的数据结构中,除queuerbtree 通常是和共享内存配合使用的之 外,其它数据结构,例如 array, hash, list, radix tree等,使用的内存都是从 内存池中申请来的。所以,它们的定义里都有一个 pool 字段指向它们使用的内存池。

拿数组结构来说,

    ----------core/ngx_array.h:15---------------
    struct ngx_array_s {
        void        *elts;  /* 指向数组使用的连续内存空间 */
        ngx_uint_t  nelts;  /* 数组目前有多少个元素 */
        size_t      size;   /* 每个元素的大小 */
        ngx_uint_t  nalloc; /* 数组申请的内存可以存储多少个元素 */
        ngx_pool_t  *pool;  /* 数组使用的内存池 */
    };
    typdef struct ngx_array_s ngx_array_t;

对数组的使用有基本可以归纳为创建数组,申请元素内存,在申请的内存上存入元素。在 配置解析阶段,对 Nginx 可识别的 HTTP Request Header 进行组织成 hash 表的函数 ngx_http_init_headers_in_hash 中可以看到整个过程:

    ----------http/ngx_http.c:431-----------------
    static ngx_int_t
    ngx_http_init_headers_in_hash(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf)
    {
        ngx_array_t         headers_in;
        ngx_hash_key_t      *hk;
        ...
        ngx_http_header_t   *header;

        if (ngx_array_init(&headers_in, cf->temp_pool, 32, sizeof(ngx_hash_key_t))
            != NGX_OK)
        {
            return NGX_ERROR;
        }

        for (header = ngx_ngx_http_headers_in; header->name.len, header++) {
            hk = ngx_array_push(&headers_in);
            ...
            hk->key = header->name;
            hk->key_hash = ngx_hash_key_lc(header->name.data, header->name.len);
            hk->value = header;
        }
        ...
        ngx_hash_init(&hash, headers_in.elts, headers_in.nelts);
        ...
    }

可以看出,array 创建和插入元素,使用 ngx_array_init/create, ngx_array_push 完成。array 元素的引用,并无专用的函数,将数组的 elts 字段转换成元素的指针类 型,然后使用 array[index] 的形式直接引用即可。至于 array 的销毁,Nginx 提供 了 ngx_array_destroy 函数,但是纵观整个代码树,它并没有被调用过。毕竟 Nginx 的 内存使用基本都是基于内存池和共享内存,内存池按生命周期被销毁后,使用它的 array 占用的空间也自然被释放掉了。

那在数组创建和元素插入过程中,是如何和内存池打交道的呢?

    -------------core/ngx_array.c:11--------------
    ngx_array_t
    ngx_array_create(ngx_poo_t *p, ngx_uint_t n, size_t size)
    {
        ngx_array_t *a;

        a = ngx_pcalloc(p, sizeof(ngx_array_t));
        ...
        a->elts = ngx_palloc(p, n * size);
        ...
        a->nelts = 0;
        a->size = size;
        a->nalloc = n;
        a->pool = p;

        return a;
    }

    ------------core/ngx_array.c:35---------------
    void
    ngx_array_push(ngx_array_t *a)
    {
        void        *elt, *new;
        size_t      size;
        ngx_pool_t  *p;

        if (a->nelts == a->nalloc) {
            size = a->size * a->nalloc;
            p = a->pool;

            if ((u_char *) a->elts + size == p->d.last
                && p->d.last + a->size <= p->d.end)
            {
                p->d.last += a->size;
                a->nalloc++;
            } else {
                new = ngx_palloc(p, 2 * size);
                ...

                ngx_memcpy(new, a->elts, size);
                a->elts = new;
                a->nalloc *= 2;
            }
        }

        elt = (u_char *) a->elts + a->size * a->nelts;
        a->nelts++;

        return elt;
    }

几点分析:

  • ngx_array_init 用来初始化静态的或者已经分配了内存的 ngx_array_t 变量。 ngx_array_create 会完全创建一个新的 ngx_array_t 变量,会对其进行初始化。

  • 如果 array 预分配内存 (array->elts) 已经用完 (或者刚刚初始化 array 为空), 再添加新的元素前,会检查 array 占用内存是否刚好和内存池可用内存相邻并且内存池 中可用内存可以容纳下新添元素。如果相邻并且内存足够,array 直接在内存池可用空间 中为新添元素申请内存,否则,从内存池中再申请 2 倍于当前内存的新内存块,并将 array 中已有元素拷贝至新内存块 (array 的属性要求所用内存必须连续)。

配置解析

Nginx 配置解析过程中,各个模块的数据结构、从配置文件读取的 token等,都需要动态申 请内存进行存储。同时,在配置解析过程中申请的内存块,有些是临时性的,在运行时并不 需求;有些需要在 Nginx 整个生命周期中一直存在。

配置解析的主要结构体 ngx_conf_t 提供了一个临时内存池 temp_pool,同时,也和 ngx_cycle_t 共用一个持久内存池 pool。Nginx 初始化正常完成后,temp_pool 会 被立即释放掉。

请求处理

每个 ngx_connection_t 对象都会拥有一个内存池,每个 ngx_http_request_t 对象也 会有一个内存池。connection 的内存池在 accept 成功,并使用 ngx_get_connection 申请到 connection 后创建。

    -----------event/ngx_event_accept.c:17-------------
    void
    ngx_event_accept(ngx_event_t *ev)
    {
        ...
        do {
            ...
            s = accept(lc->fd, (struct sockaddr *) sa, &socklen);
            ...
            c = ngx_get_connection(s, ev->log);
            ...
            c->pool = ngx_create_pool(ls->pool_size, ev->log);
            ...
            c->sockaddr = ngx_palloc(c->pool, socklen);
            ..
        } while (ev->available);
    }

随后,HTTP 请求使用的 connection 会由 ngx_http_init_connection 再次处理:

    ------------http/ngx_http_request.c:177--------------
    void
    ngx_http_init_connection(ngx_connection_t *c)
    {
        ...
        rev = c->read;
        rev->handler = ngx_http_init_request;
        c->write->handler = ngx_http_empty_handler;
        ...
    }

然后,从 connection 中收到请求数据后, request 才会创建出来并真正开始它的生 命之旅。

    -----------http/ngx_http_request.c:232--------------
    static void
    ngx_http_init_request(ngx_event_t *rev)
    {
        ...
        c->requests++;
        ...
        r = ngx_pcalloc(c->pool, sizeof(ngx_http_request_t));
        ...
        r->connection = c;
        ...
        c->buffer = ngx_create_temp_buf(c->pool,
                                        cscf->client_header_buffer_size);
        ...
        r->header_in = c->buffer;
        r->pool = ngx_create_pool(cscf->request_pool_size, c->log);
        ...
        ngx_list_init(&r->headers_out.headers, r->pool, 20,
                      sizeof(ngx_table_elt_t));
        ...
        r->ctx = ngx_pcalloc(r->pool, sizeof(void *) * ngx_http_max_module);
        ...
        r->variables = ngx_pcalloc(r->pool, cmcf->variables.nelts
                                            * sizeof(ngx_http_variable_value_t));
        ...
    }
  • 从一个 connection 来的多个 request 共用一个临时缓冲区 c->buffer,用于存 储 request 的请求数据。request 是顺序到来的,这样共用并不会有问题。

  • 跟一个 request 相关的其它数据结构就直接从 r->pool 里申请内存了。

request 处理完成或者处理过程中发生错误时,Nginx 调用 ngx_http_free_request 释放 request 的内存池。

connection 正常关闭或者因为处理过程中发生错误而关闭时,Nginx 调用 ngx_http_close_connection 释放 connection 的内存池。

Comments

不要轻轻地离开我,请留下点什么...


Published

Category

Nginx

Tags

Contact