nginx 源码学习(五) 基本数据结构 ngx_list_t
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是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/
更多推荐
所有评论(0)