ngx_array_t 介绍 

ngx_array_t是nginx内部使用的数组结构。显而易见ngx_array_t是一个顺序容器,它以数组的形式存储元素,并能够在数组容量达到上限时动态扩容

数组,很像c++ STL中的vector容器。ngx_array_t 使用了nginx内存池,因此其分配的内存也是在内存池中申请得到的,总的来说ngx_array_t具有访问

速度快、数组可动态扩容、负责容器元素内存分配等优点。

ngx_array_t基本结构

nginx数组实现在文件:./src/core/ngx_array.{h,c}。

nginx的数组结构为ngx_array_t,定义如下:

typedef struct ngx_array_s  ngx_array_t;  
struct ngx_array_s {
    void        *elts;  //具体的数据区域的起始地址
    ngx_uint_t   nelts; //已经存储了的元素数量
    size_t       size;  //单个元素的大小(字节)
    ngx_uint_t   nalloc; //数组容量,即数组预先分配的内存大小
    ngx_pool_t  *pool;  //内存池,用其保存分配此数组的内存池地址。
};
在32位系统上,sizeof(ngx_array_t)=20B,由定义可见,nginx的数组也要从内存池中分配。将分配nalloc个大小为size的小空间(分配的大小

为nalloc * size B)。

ngx_array_t基本操作

下面介绍ngx_array_t 包含的一些基本操作。

ngx_array_t*ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size);//创建数组

ngx_array_create作用是创建一个动态数组,并预分配n个大小为size的内存空间,参数p是内存池,n是初始分配元素的最大个数,size是每个元素

所占用内存的大小。首先分配数组头(20B),然后分配数组数据区(nalloc * size B),两次分配均在传入的内存池(pool指向的内存池)中进行。

然后简单初始化数组头并返回数组头的起始位置。代码如下:

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;
    }

    a->elts = ngx_palloc(p, n * size);//接着分配n*size大小的区域作为数组数据区
    if (a->elts == NULL) {
        return NULL;
    }

    a->nelts = 0;//初始化
    a->size = size;
    a->nalloc = n;
    a->pool = p;

    return a;//返回数组头的起始位置 
}

ngx_array_t 对于的逻辑结构如下图:

           

static ngx_inlinengx_int_t

ngx_array_init(ngx_array_t*array, ngx_pool_t *p, ngx_uint_t n, size_t size)//初始化数组

初始化1个已经存在的动态数组, 并预分配n个大小为size的内存空间,参数array为指向数组结构体指针,p,n,size和ngx_array_create参数意义相同。

代码如下:

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 beeninitialized

    */

   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;
}

通过上面的代码可以很容易知道它与ngx_array_create的区别,不同之处在,init不需要为ngx_array_t本身分配空间。

void ngx_array_destroy(ngx_array_t*a) // 销毁数组

销毁已经分配的数组数据区以及数组头,参数a是指向动态数组的指针。

这里的销毁动作实际上就是修改内存池的last指针,并没有调用free等释放内存的操作,这种操作效率比较高。代码如下:

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;//设置内存池的last指针
    }
}

void * ngx_array_push(ngx_array_t*a) // 添加一个元素

从数组a中取一个存储一个元素的空间,并返回这个空间地址, 参数a是动态数组结构体指针。可知, nginx数组的用法就是先申请内存,然后我们需要

对返回的指针指向的地址进行赋值等操作来实现实际数组值的添加。代码如下:

/*
返回可以在该数组数据区中添加这个元素的位置
*/
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//若内存池的last指针指向数组数据区的末尾
            && p->d.last + a->size <= p->d.end)//且内存池未使用的区域可以再分配一个size大小的小空间 
        {
            /*
             * the array allocation is the last in the pool
             * and there is space for new allocation
             */

            p->d.last += a->size;//分配一个size大小的小空间(a->size为数组一个元素的大小)
            a->nalloc++;//实际分配小空间的个数加1

        } else {//否则,扩展数组数据区为原来的2倍 
            /* 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; //返回该末尾指针,即下一个元素应该存放的位置
}

通过上面的代码分析,可以不程序流程分为以下几种情况:

①  如果array当前已分配的元素个数小于最大分配个数(即a->nelts < a->nalloc),那么用数组元素首地址a->elts计算出分配元素的首地址,

        并返回结果。

②   如果array中当前数组数据区满(即a->nelts == a->nalloc),并且array所在内存池pool的last指针指向数组数据区的末尾

      (即 (u_char *)a->elts + size == p->d.last)且还有空间可分配给新元素(即p->d.last+ a->size <= p->d.end),那么对array在本对array进行

      扩充一个单元,扩充后即变成情形①进行处理。

③   如果array中当前数组数据区满(即a->nelts == a->nalloc),且不满足(u_char *)a->elts + size == p->d.last或p->d.last + a->size <= p->d.end,

      那么对array大小增大一倍后进行重新分配,并将原来array内容拷贝到新地址空间中,完成后最大容量变成原来的两倍,同情形①进行处理。

void * ngx_array_push_n(ngx_array_t*a, ngx_uint_t n) //添加n个元素

向当前动态数组a添加n个元素,返回的是新添加这批元素中第一个元素的地址, 参数a是动态数组结构体指针。ngx_array_push_n和ngx_array_push

是类似的处理,不再次敷述了。

测试例子

为了更好的理解上面的知识点,写一些测试代码,并进行调试来进一步理解开源代码的原理和设计思路。

测试代码如下:

// ngx_array_test.c
#include <stdio.h>
#include <string.h>
#include "ngx_config.h"
#include "nginx.h"
#include "ngx_conf_file.h"
#include "ngx_core.h"
#include "ngx_string.h"
#include "ngx_palloc.h"
#include "ngx_array.h"

#define N 5

volatile ngx_cycle_t *ngx_cycle;

void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log,
			ngx_err_t err, const char *fmt, ...)
{
}
// 自定义结构体类型
typedef struct node
{
	int id;
	char buf[32];
}Node;

void print_array(ngx_array_t *a)// 遍历输出array
{
	printf("-------------------------------\n");
	Node *p = a->elts;
	size_t i;
	for(i=0; i < a->nelts; ++i)
	{
		printf("%s.\n", (p+i)->buf);
	}
	printf("-------------------------------\n");
}


int main()
{
	ngx_pool_t *pool;
	int i;
	Node *ptmp;
	char str[] = "hello NGX!";
	ngx_array_t *a;

	pool = ngx_create_pool(1024, NULL);// 创建内存池
	printf("Create pool. pool max is %d\n", pool->max);
	a = ngx_array_create(pool, N, sizeof(Node));// 创建动态数组
	printf("Create array. size=%d nalloc=%d\n", a->size, a->nalloc);
	printf("unused memory size is %d\n", (ngx_uint_t)(pool->d.end - 
					pool->d.last) );
	for(i=0; i<8; ++i)
	{
		ptmp = ngx_array_push(a);// 添加一个元素
		ptmp->id = i+1;
		sprintf(ptmp->buf, "My Id is %d,%s", ptmp->id, str);
	}
	ptmp = ngx_array_push_n(a, 2);// 添加两个元素
	ptmp->id = i+1;
	sprintf(ptmp->buf, "My Id is %d,%s", ptmp->id, str);
	++ptmp;
	ptmp->id = i+2;
	sprintf(ptmp->buf, "My Id is %d,%s", ptmp->id, str);
	print_array(a);
	printf("unused memory size is %d\n", (ngx_uint_t)(pool->d.end - 
					pool->d.last) );
	ngx_array_destroy(a);
	printf("After destroy array unused memory size is %d\n", 
				(ngx_uint_t)(pool->d.end - pool->d.last) );
	ngx_destroy_pool(pool);
	return 0;
}
对于上面的代码, 编写 相应的Makefile(不熟悉make的可以 参考 这里)文件如下:

CC=gcc
C_FLAGS = -g -Wall -Wextra  
DIR=/home/dane/nginx-1.2.0
TARGETS=ngx_array_test
TARGETS_FILE=$(TARGETS).c

all:$(TARGETS)

clean:
	rm -f $(TARGETS) *.o

CORE_INCS=-I $(DIR)/src/core/ \
		  -I $(DIR)/objs/ \
		  -I $(DIR)/src/event \
		  -I $(DIR)/src/event/modules \
		  -I $(DIR)/src/os/unix \
		  -I $(DIR)/Nginx_Pre/pcre-8.32/

NGX_OBJ = $(DIR)/objs/src/core/ngx_palloc.o \
		  $(DIR)/objs/src/core/ngx_string.o \
		  $(DIR)/objs/src/os/unix/ngx_alloc.o \
		  $(DIR)/objs/src/core/ngx_array.o

$(TARGETS):$(TARGETS_FILE)
	$(CC) $(C_FLAGS) $(TARGETS_FILE) $(CORE_INCS) $(NGX_OBJ) -o $@

上面的Makefile 编写好后, 直接 make 就可产生 出 可执行文件 ngx_array_test

./ngx_array_test 即可运行 可执行文件。

结果如下:

                

通过程序分析可知刚开始我们创建一个内存池为1024字节大小的内存池,除去内存池头部节点所占内存大小(40B),此时内存池max

即最大可用内存为 1024 - 40 = 984 字节,然后ngx_array_create 创建动态数组,此时所内存池大小为 头部20字节以及 申请的内存

块(即数据区n*size大小 ) 5*36=180 字节,创建这样的动态数组总共消耗内存池大小为 20+180 = 200 字节,所以此时内存池所剩内

存大小 为 984 - 200 = 784 字节,随后在数组中添加第6个元素时,此时数组数据区已满,需要分配2陪的数据区,因此最后内存池所

剩内存大小为784-180=604字节。最后调用ngx_array_destroy()通过代码分析我们需要注意:数组在扩容时,旧的内存不会被释放,

会造成内存的浪费。因此,最好能提前规划好数组的容量,在创建或者初始化的时候一次搞定,避免多次扩容,造成内存浪费。

最后内存真正释放需要通过调用函数ngx_destroy_pool().

参考:

http://code.google.com/p/nginxsrp/wiki/NginxCodeReview

http://blog.csdn.net/daniel_ustc/article/details/11645293

http://blog.csdn.net/livelylittlefish/article/details/6599056

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐