一、简介
C语言中的数组大小是固定的,每次扩容基本上都会引起数据的拷贝。nginx实现了一套数组基础组件,使用内存池方式分配空间,减少扩容引发的内存分配以及数据拷贝。
主要涉及ngx_array.h, ngx_array.c
二、 数据结构
typedef struct {
void *elts;
ngx_uint_t nelts;
size_t size;
ngx_uint_t nalloc;
ngx_pool_t *pool;
} ngx_array_t;
- elts指向数组的首地址
- nelts表示当前数组中已有元素的个数
- size表示数组中每个元素的大小
- nalloc表示数组分配的空间总个数
- pool表示分配空间的内存池
2.1 逻辑结构图
2.2 实际结构图
三、相关API
3.1 创建数组
ngx_array_t *
ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size)
{
ngx_array_t *a;
a = ngx_palloc(p, sizeof(ngx_array_t));
if (a == NULL) {
return NULL;
}
if (ngx_array_init(a, p, n, size) != NGX_OK) {
return NULL;
}
return a;
}
static ngx_inline ngx_int_t
ngx_array_init(ngx_array_t *array, ngx_pool_t *pool, ngx_uint_t n, size_t size)
{
/*
* set "array->nelts" before "array->elts", otherwise MSVC thinks
* that "array->nelts" may be used without having been initialized
*/
array->nelts = 0;
array->size = size;
array->nalloc = n;
array->pool = pool;
array->elts = ngx_palloc(pool, n * size);
if (array->elts == NULL) {
return NGX_ERROR;
}
return NGX_OK;
}
根据代码可以看出,所有的结构体以及数组本身空间都是从内存池中分配的。
3.2 销毁数组
void
ngx_array_destroy(ngx_array_t *a)
{
ngx_pool_t *p;
p = a->pool;
if ((u_char *) a->elts + a->size * a->nalloc == p->d.last) {
p->d.last -= a->size * a->nalloc;
}
if ((u_char *) a + sizeof(ngx_array_t) == p->d.last) {
p->d.last = (u_char *) a;
}
}
对于数组的销毁,其实就是对内存池的操作,而只有两种情况才能立刻将内存返还给内存池,其他情况都只能等待内存池的销毁而回收内存。
数组分了两部分,数组头结构体和数组本身,只有数组结尾地址/结构体结尾地址是最后下次开始分配的地址,才能立刻将内存返还给内存池。
比如最开始画的那个图就是可以立刻回收的。
回收后如下,原有的那些数据就变成了垃圾数据,并且这些内存可以立刻被重新分配给其他模块使用。
而如下的图就不能被回收,数组后面的空间已经被其他模块使用了,这样就不连续了,无法回收;而且这种内存结构情况下,如果是插入,并且写入数组元素已经达到上限,则只能重新分配空间,拷贝数据:
3.3 插入数据
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;
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 */
new = ngx_palloc(p, 2 * size);
if (new == NULL) {
return NULL;
}
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;
}
和一般的插入函数不同,nginx的数组插入函数并没有传递需要插入的数据,这样提供的API更简单,不要考虑数据的类型,也就不需要考虑类似C++函数重载(或者模板)来实现不同类型参数的插入操作。nginx的数组插入函数只是返回需要插入数据的位置的首地址,需要调用方来写入数据。
当预分配的数组大小写满了,则需要扩容,这里分了两种情况:
- 数组结尾地址和下次分配地址重合,并且还有空间可以分配一个元素
直接增加一个元素
- 数组结尾地址和下次分配地址不重合
从内存池中重新分配2倍大小的新数组,将已有数据拷贝到新数组中,涉及数组拷贝
3.4 连续插入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 */
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;
}
和插入一个元素逻辑一致,只是在扩容2倍时,如果n大于等于数组原始大小,则扩大到2n个元素。
3.5 总结
nginx的数组api相对简单,就这4个api。但还是满满的料,很多细节可以慢慢品。