ngx_list_t 介绍

ngx_list_t是nginx封装的单链表容器, 它在nginx中的应用比较频繁, 比如在nginx 源码中src/http/ngx_http_request.{h,c}的HTTP的头部就是使用

ngx_list_t来存储的。这个链表结构和我们常见的链表有所不同, 其不同点在于链表的每一个节点,它的节点不像我们常见的单链表的节点(即每

个节点只能存放一个元素),ngx_list_t 的节点实际上是一个固定大小的数组(或称为块)。此外,ngx_list_t单向链表与上一节ngx_queue_t 双向

链表有所不同,ngx_queue_t与nginx的内存池无关,ngx_queue_t不负责内存分配来存储链表元素,ngx_queue_t只负责把已经分配好内存的

元素用双向链表连接起来。ngx_list_t 则负责链表元素内存的分配(即使用nginx内存池),ngx_list_t使用单链表将多段内存块连接起来,每段内存块

存储了连续的多个元素(大于等于1个元素),形式如 “数组+单链表”。
关于nginx内存池的相关介绍参考这篇文章,ngx_queue_t参考这篇文章

ngx_list_t 基本结构

相关定义在文件src/core/ngx_list.{h,c}下:

typedef struct ngx_list_part_s ngx_list_part_t;  
   
struct ngx_list_part_s {  //链表每个节点的结构
    void          *elts; //指向该节点的数据区(该数据区中可存放nalloc个大小为size的元素)  
    ngx_uint_t        nelts;  //已存放的元素个数  
    ngx_list_part_t  *next;   //指向下一个链表节点  
};  
   
typedef struct{              //链表头结构  
    ngx_list_part_t  *last;   //指向链表最后一个节点(part)  
    ngx_list_part_t   part;   //链表头中包含的第一个节点(part)  
    size_t            size;   //每个元素大小  
    ngx_uint_t        nalloc; //链表所含空间个数,即实际分配的小空间的个数  
    ngx_pool_t       *pool;   //内存池  
}ngx_list_t;

链表头结构内包含第一个块,以及最后一个块的指针。在32位系统上sizeof(ngx_list_t)=28,sizeof(ngx_list_part_t)=12。从上面的链表结构可知,

对于每一个链表节点(list part)将分配nalloc个大小为size的小空间, 即大小为(nalloc * size)字节的链表节点数据存储区。

ngx_list_t 基本操作

ngx_list_t链表的基本操作共3个,分别介绍如下(假设在32位linux系统之上):

ngx_list_t* ngx_list_create(ngx_pool_t *pool, ngx_uint_t n, size_t size);   //创建链表  

创建链表主要是申请两块内存,初始化一些变量。细说起来,首先分配链表头(28B)所占内存,然后分配头节点(即链表头中包含的part)数据区(n*size大小),

两次分配均在传入的内存池(pool指向的内存池)中进行, 最后简单初始化链表头并返回链表头的起始位置。

ngx_list_t *  
ngx_list_create(ngx_pool_t*pool, ngx_uint_t n, size_t size)  
{  
    ngx_list_t *list;  
    list = ngx_palloc(pool,sizeof(ngx_list_t));  //从内存池中分配链表头  
    if (list == NULL) {  
        return NULL;  
    }  
   
    list->part.elts =ngx_palloc(pool, n * size); //接着分配n*size大小的区域作为链表数据区  
    if (list->part.elts == NULL) {  
        return NULL;  
    }  
   
    list->part.nelts = 0;     //已存0个元素  
    list->part.next = NULL;  //只有一个块
    list->last = &list->part;  //指向最后一块
    list->size = size;  //可存的元素个数
    list->nalloc = n;  
    list->pool = pool;  
   
    return list;    //返回链表头的起始位置  
}
下面给出具有两个节点的链表逻辑结构图:

   

上图的ngx_list包含2个块,共n+m 个元素(即第二个链表节点所含元素为m<n)。注意,只有最后一个块(即last所指节点)才可能处于不满状态,

ngx_list 没有定义元素删除操作和链表销毁的操作。

static ngx_inlinengx_int_t

ngx_list_init(ngx_list_t*list, ngx_pool_t *pool, ngx_uint_t n, size_t size); //初始化链表

该函数是用于ngx_list_t类型的对象已经存在,但是其第一个节点存放元素的内存空间还未分配的情况下,可以调用此函数来给这个list的首节点来

分配存放元素的内存空间。代码如下:

static ngx_inline ngx_int_t
ngx_list_init(ngx_list_t *list, ngx_pool_t *pool, ngx_uint_t n, size_t size)
{
    list->part.elts = ngx_palloc(pool, n * size); // 从内存池申请空间后,让elts指向可用空间
    if (list->part.elts == NULL) {
        return NGX_ERROR;
    }
    list->part.nelts = 0; 
    list->part.next = NULL;
    list->last = &list->part; // last开始的时候指向首节点
    list->size = size;
    list->nalloc = n;
    list->pool = pool;
    return NGX_OK;
}

void*ngx_list_push(ngx_list_t *l)  //添加元素 

实际的添加操作并不在该函数中完成。函数ngx_list_push返回可以在该链表数据区中放置元素(元素可以是1个或多个)的内存地址,而添加数据操作在获得

添加位置之后进行加入元素时,首先根据ngx_list_t的last指针定位最后一块,然后检测最后一块的元素是否已经用完了,即判断

ngx_list_part_t->nelts == ngx_list_t->nalloc(相等即满,否则最后一块内存未用完)。如果没满,则将最后一块的第nelts元素地址返回即可。如果满了,则申请

一个新的链表块,并采用尾插法加入链表,然后将新申请的内存块第0个元素地址返回。

程序流程图如下:

                          

测试例子

为了更好的理解上面的知识点,写一些测试代码,并进行调试来进一步理解开源代码的原理和设计思路。下面结合nginx内存池和ngx_list_t 及

nginx基本数据类型ngx_str_t进行举例测试。

使用nginx对c语言的字符串类型进行封装的字符串类型ngx_str_t类型(文件src/core/ngx_string.{h,c}中有此类型的定义 ),其定义如下所示:

typedef struct {
    size_t      len;
    u_char     *data;
} ngx_str_t;

len表示字符串的有效长度;

data指针指向字符串首地址,当然该段字符串未必就一定以\0字符结束,因此在使用时,必须结合长度len来使用data指针。


下面给出一个创建内存池并从中分配一个链表的简单例子。

在该例中,链表的每个节点(part)可存放5个元素,每个元素sizeof(ngx_str_t)字节大小(32位系统为8个字节),创建链表后,要向链表添加

10个ngx_str_t类型的元素 其中,ngx_str_t 类型中存放字符内存为从内存池申请,每个ngx_str_t 为32字节。代码如下:

//ngx_list_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_list.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, ...)
{
}

void print_list(ngx_list_t *l)
{
	
	ngx_list_part_t *p = &(l->part);
	size_t i, n_part=0;
	while(p)
	{
		i=0;
		printf("------------part %d---------------\n", ++n_part);
		for(; i<p->nelts; ++i)
		{
			printf("%s\n", (char*)(((ngx_str_t*)p->elts + i)->data) );
		}
		p = p->next;
	}
	printf("-------------------------------\n");
}


int main()
{
	ngx_pool_t *pool;
	int i;
	char str[] = "hello NGX!";
	ngx_list_t *l;

	pool = ngx_create_pool(1024, NULL);
	printf("Create pool. pool max is %d\n", pool->max);
	l = ngx_list_create(pool, N, sizeof(ngx_str_t));
	printf("Create list. size=%d nalloc=%d\n", l->size, l->nalloc);
	printf("unused memory size is %d\n", (ngx_uint_t)(pool->d.end - 
					pool->d.last) );
	for(i=0; i<10; ++i)
	{
		ngx_str_t *pstr = ngx_list_push(l);
		char *buf = ngx_palloc(pool, 6*N);
		sprintf(buf, "My Id is %d,%s", i+1, str);

		pstr->len = strlen(buf);
		pstr->data = buf;
	}
	print_list(l);
	printf("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_list_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_list.o

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

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

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

结果如下:

             

通过程序运行结果、程序、以及链表的逻辑结构图,我们 可以知道,刚开始我们创建一个内存池为1024字节大小的内存池,除去内存池头部节点

所占内存大小(40B),此时内存池max即最大可用内存为 1024 - 40 = 984 字节,然后ngx_list_create 创建链表,此时所剩内存池大小为 链表头部28字节

以及 分配头节点(即链表头中包含的part)数据区(n*size大小 ) 5*8=40 字节,创建这样的链表总共消耗内存池大小为 28+40 = 68 字节,所以此时内存

池所示内存大小 为 984 - 68 = 916 字节。然后程序在链表中添加10个ngx_str_t 类型的元素,由于链表创建时 只创建了一个链表节点, 此时的唯一的

链表节点只能容纳 5个 ngx_str_t 字符串结构, 因此在添加 第6个元素时 ngx_list_push 会自动创建一个新的链表节点,此时会申请一个ngx_list_part_t

类型的结构体 其大小为 12字节,并申请节点(part)数据区所占内存(n*size)大小为40字节,此外 10个ngx_str_t 类型的元素 存储字符串(每个ngx_str_t

要从内存池申请32字节 存放其容纳的字符串),所以最后内存池所剩内存大小为 916 - 5*32 - 12 - 40 - 5*32 = 544 字节。计算结果与程序运行结果一致。

通过分析和测试,我们可以看出 ngx_list 采用分块的方式进行内存管理,确实在很大程度上降低了从内存池申请内存的频率,对性能提升有较好的帮助。

参考:

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

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

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

http://hankjin.blog.163.com/blog/static/33731937201091111303630/



Logo

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

更多推荐