05Nginx源码分析之数组结构ngx_array.c
前言:
前面一章我们介绍了Nginx的内存池的数据结构。Nginx的内存管理都是围绕内存池来实现的,包括array数组类型也是基于Nginx的pool来实现数据结构。
Nginx的Array结构设计得非常小巧,主要用于存储小块内存。该数组存储在内存池数据域中,因为该数据域本来就主要存储小块内存的,并且内存池的大小被上篇讲到的宏NGX_MAX_ALLOC_FROM_POOL给限制。存储大内存的话会被nginx的开发人员人为放在nginx的large中处理。
Nginx的数组每个元素的大小是固定的。
1 数据结构定义
1)ngx_array_t 数组的基础数据结构。
/* 数组Array数据结构 */
typedef struct {
void *elts; /* 指向数组第一个元素指针*/
ngx_uint_t nelts; /* 已使用元素的索引(原博主写成未使用)*/
size_t size; /* 每个元素的大小,元素大小固定*/
ngx_uint_t nalloc; /* 一共分配了多少个元素。如果元素不够用,Nginx会数组会进行自动扩容 */
ngx_pool_t *pool; /* 内存池,即属于那个内存池.数组的数据结构ngx_array_t和元素所需要的内存都会分配在pool内存池上*/
} ngx_array_t;
2 数据结构图
下面可以看到数组在内存池上是非常简单的,包括数组结构体大小和相应的元素个数的大小。
说明:
1)Nginx的数组只存储比较小的数据
2)数组的元素长度在创建数组的时候就固定死了。但是数组个数,会自动扩容。
3)数组的数据结构和元素内存都会分配在Nginx的pool内存池上。
4)数组回收会去检查pool内存池,看是否可以将数组内存交还给内存池。所以下面当数组的末尾和d->last不等时,就不管了,交给内存池统一管理。
3 具体函数实现
1)创建数组 ngx_array_create
下面可以看到,我们可以定义创建多少个数组元素,可以定义每个元素的size,并且内部实现开辟空间时是将结构体大小和实际大小分开申请,非常简单。
/**
* 初始化一个数组
* p:内存池容器
* n:支持多少个数组元素
* size:每个元素的大小
*/
ngx_array_t *
ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size)
{
ngx_array_t *a;
/* 在内存池 pool上面 分配一段内存给 ngx_array数据结构*/
a = ngx_palloc(p, sizeof(ngx_array_t));
if (a == NULL) {
return NULL;
}
/**
* 数组初始化,并且分配内存空间给数组元素
* PS:这边数组的数据结构和数组元素的存储分成了两次在pool上分配,笔者认为可以一次进行分配
* 但是Nginx是多进程的,程序执行流程是线性的,所以分两次分配也无伤大雅。
*/
if (ngx_array_init(a, p, n, size) != NGX_OK) {
return NULL;
}
return a;
}
2)数组销毁 ngx_array_destroy
数组销毁设计的也挺讲究的,会去检查数组尾部是否在内存池内存块上的d->last有效数据的结尾部分,如果在,则将内存回收给内存池。然后在减去结构体的大小。
注意:若开辟一个数组后,接着又开辟了多个数组,那么d->last可能与第一个数组结尾不同,此时在销毁第一个数组是不满足条件的,你无法再处理它了,它会交给内存池处理,不需要我们担心,只不过内存池会慢慢变大,但也不会特别大,因为本来就是存储小数据块嘛。
/**
* 数组销毁
* 数组销毁设计的也挺讲究的,会去帮助清除内存池上的内存
*/
void
ngx_array_destroy(ngx_array_t *a)
{
ngx_pool_t *p;
p = a->pool;
/**
* PS:你估计比较奇怪,为何数组的内存空间一定会分配在内存池(pool->d存储小内存)上面
* 如果比较大的内存块不是会存储在内存池的pool->large上面吗?
* 当我们全局搜索Nginx代码中ngx_array_create方法的时候发现,Nginx的数组都是比较小的,存储的数据量也
* 并不是很大。所以ngx_array_t适合存储小块的内存。
*/
/**
* 如果数组元素的末尾地址和内存池pool的可用开始的地址相同
* 则将内存池pool->d.last移动到数组元素的开始地址,相当于清除当前数组的内容
*/
if ((u_char *) a->elts + a->size * a->nalloc == p->d.last) {
p->d.last -= a->size * a->nalloc;
}
/**
* 如果数组的数据结构ngx_array_t的末尾地址和内存池pool的可用开始地址相同
* 则将内存池pool->d.last移动到数组元素的开始地址,相当于清除当前数组的内容
*/
if ((u_char *) a + sizeof(ngx_array_t) == p->d.last) {
p->d.last = (u_char *) a;
}
}
3)往数组中增加一个元素的空间 ngx_array_push
为何叫增加一个元素的空间呢,因为它并非像函数名是push一个元素内容,而是返回增大那个元素的首地址,实际数据仍需要你自己添加,看返回值就知道了。
函数逻辑:
首先判断已有元素是否等于开辟的元素个数,如果满了就扩容,否则返回已使用的元素后的地址(实际上没有任何处理)。
扩容有两种方法:1)若数组元素的末尾和内存池pool的可用开始的地址相同并且所属内存池后空间足够,则在本内存池扩容;2)否则在其它内存池d->next中寻找足够的内存,并且将原来的数据拷贝过去新的内存池中,该寻找在ngx_palloc里面做了,它会返回已有数据的地址即m并且p->d->last已经帮我们计算好为已有数据的大小加上扩容的大小。然后最后更新elt即扩容后的元素首地址。
注:
a->nalloc++;或者a->nalloc *= 2;非常重要,它是我们数组能否被内存池管理的关键。假设我们数组容量nalloc=10,已用5,在扩容一个理应直接在后面获取即可,但是nalloc不增加的话,那么nelts就增加已使用一个,那么可用的就变成了4个,但是你现在是在扩容,反而可用变少了,会造成少回收一个。因为他可以根据nalloc的数组大小和elts求出数组大小交给内存池回收。所以说nalloc的增加是非常重要的。下面的扩容n个也同理。
/**
* 添加一个元素
*/
void *
ngx_array_push(ngx_array_t *a)
{
void *elt, *new;
size_t size;
ngx_pool_t *p;
/* 首先判断数组后的空间大小是否够用,若不够 ,则需要对数组进行扩容 */
if (a->nelts == a->nalloc) {
/* the array is full */
size = a->size * a->nalloc;
p = a->pool;
/**
* 扩容有两种方式
* 1.如果数组元素的末尾和内存池pool的可用开始的地址相同,
* 并且内存池剩余的空间支持数组扩容,则在当前内存池上扩容
* 2. 如果扩容的大小超出了当前内存池剩余的容量或者数组元素的末尾和内存池pool的可用开始的地址不相同,
* 则需要在其它内存池重新分配一个新的内存块存储数组,并且将原数组拷贝到新的地址上
*/
if ((u_char *) a->elts + size == p->d.last
&& p->d.last + a->size <= p->d.end)
{
/*
* the array allocation is the last in the pool
* and there is space for new allocation
*/
p->d.last += a->size;
a->nalloc++;//非常重要
} else {
/* allocate a new array */
/* 重新分配一个 2*size的内存块,ngx_palloc会自动寻找其它有足够空间的内存池开辟 */
new = ngx_palloc(p, 2 * size);
if (new == NULL) {
return NULL;
}
/* 内存块拷贝,将老的内存块拷贝到新的new内存块上面 */
ngx_memcpy(new, a->elts, size);
a->elts = new; /* 内存块指针地址改变 */
a->nalloc *= 2; /* 分配的个数*2 ,重要*/
// size 不变(最大容量,即单个元素大小 x 最大元素数)
// pool 不变(因为分配新的内存块的时候,会去循环读取pool->d.next链表上的缓存池,
// 并且比较剩余空间大小,是否可以容乃新的内存块存储)
// nelts 已用元素数量不变
}
}
/* 最新的元素指针 地址 */
elt = (u_char *) a->elts + a->size * a->nelts;
a->nelts++; //只分配一个元素,所以元素数量+1,注意:该句表示,实际上该内存没有存放数据,但是仍然认为已用
return elt;
}
4)往数组中增加n个元素的空间 ngx_array_push_n
下面的与上面单个同理。
函数逻辑:
首先判断已有元素加上要扩容的n个元素大小是否大于已经开辟的个数,如果大于就扩容,否则返回已使用的元素后的地址(实际上没有任何处理)。
/**
* 这个方法同上,只不过支持多个元素
*/
void *
ngx_array_push_n(ngx_array_t *a, ngx_uint_t n)
{
void *elt, *new;
size_t size;
ngx_uint_t nalloc;
ngx_pool_t *p;
size = n * a->size;
if (a->nelts + n > a->nalloc) {
/* the array is full */
p = a->pool;
if ((u_char *) a->elts + a->size * a->nalloc == p->d.last
&& p->d.last + size <= p->d.end)
{
/*
* the array allocation is the last in the pool
* and there is space for new allocation
*/
p->d.last += size;
a->nalloc += n;
} else {
/* allocate a new array */
//该句表示,若n比已开辟的内存大,则按其两倍扩容,否则按已开辟的大小扩容
nalloc = 2 * ((n >= a->nalloc) ? n : a->nalloc);
new = ngx_palloc(p, nalloc * a->size);
if (new == NULL) {
return NULL;
}
ngx_memcpy(new, a->elts, a->nelts * a->size);
a->elts = new;
a->nalloc = nalloc;
}
}
//返回扩容后新元素的地址,即实际数组已用地址
elt = (u_char *) a->elts + a->size * a->nelts;
a->nelts += n;
return elt;
}
4 总结
我们这篇着重说明一点就是,当创建多个数组后,如果你想调用数组销毁函数销毁最后一个数组之前的数组,是不会满足销毁函数的条件的,此时在最后一个数组前面的那些数组将会统一交给内存池管理。在销毁函数也强调了一遍。