任何一个程序都少不了和内存打交道,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 实现的基础的数据结构中,除queue
和 rbtree
通常是和共享内存配合使用的之
外,其它数据结构,例如 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
不要轻轻地离开我,请留下点什么...