nginx 源碼學習(六) 基本數據結構 ngx_array_t

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章