nginx的数据结构
nginx自己封装了很多数据结构。因为nginx具有两个特点:跨平台,c语言,所以很多库中的容器和数据结构就不能使用了,针对不同平台,nginx也封装了很多系统调用,这样在上层看来能做到统一的调用接口。
一 nginx里简单的数据结构
1.1 ngx_queue_t
ngx_queue_t 是nginx里重要的数据结构,采用双向链表的形式,具体定义在Ngx_queue.h和Ngx_queue.c中。俗称:双向容器。定义如下:
typedef struct ngx_queue_s ngx_queue_t;
struct ngx_queue_s {
ngx_queue_t *prev;
ngx_queue_t *next;
};
可见在ngx_queue_t中有两个指针成员,都指向ngx_queue_t自身。那么nginx怎么使用ngx_queue_t呢?其实nginx只是把ngx_queue_t作为链表元素的一部分,比如下所示:
typedef struct
{
int num;
ngx_queue_t;
}TestNode;
nginx是通过TestNode中的ngx_queue_t来控制整个链表的。控制的函数包括:
#define ngx_queue_init(q) \
(q)->prev = q; \
(q)->next = q
#define ngx_queue_empty(h) \
(h == (h)->prev)
#define ngx_queue_insert_head(h, x) \
(x)->next = (h)->next; \
(x)->next->prev = x; \
(x)->prev = h; \
(h)->next = x
#define ngx_queue_insert_after ngx_queue_insert_head
#define ngx_queue_insert_tail(h, x) \
(x)->prev = (h)->prev; \
(x)->prev->next = x; \
(x)->next = h; \
(h)->prev = x
#define ngx_queue_head(h) \
(h)->next
#define ngx_queue_last(h) \
(h)->prev
#define ngx_queue_sentinel(h) \
(h)
#define ngx_queue_next(q) \
(q)->next
#define ngx_queue_prev(q) \
(q)->prev
#if (NGX_DEBUG)
#define ngx_queue_remove(x) \
(x)->next->prev = (x)->prev; \
(x)->prev->next = (x)->next; \
(x)->prev = NULL; \
(x)->next = NULL
#else
#define ngx_queue_remove(x) \
(x)->next->prev = (x)->prev; \
(x)->prev->next = (x)->next
#endif
#define ngx_queue_split(h, q, n) \
(n)->prev = (h)->prev; \
(n)->prev->next = n; \
(n)->next = q; \
(h)->prev = (q)->prev; \
(h)->prev->next = h; \
(q)->prev = n;
#define ngx_queue_add(h, n) \
(h)->prev->next = (n)->next; \
(n)->next->prev = (h)->prev; \
(h)->prev = (n)->prev; \
(h)->prev->next = h;
#define ngx_queue_data(q, type, link) \
(type *) ((u_char *) q - offsetof(type, link))
ngx_queue_t *ngx_queue_middle(ngx_queue_t *queue);
void ngx_queue_sort(ngx_queue_t *queue,
ngx_int_t (*cmp)(const ngx_queue_t *, const ngx_queue_t *));
注意,大部分是用宏实现的,这样没有建立函数的出栈和入栈开销,可以大大提高程序的效率。下面接下几个函数的意义:
#define ngx_queue_init(q) \
(q)->prev = q; \
(q)->next = q
此宏定义,主要初始化一个queue,切记,初始化的第一个queue不会包含于任何数据结构中,仅仅是表头,pre和next都指向自己。
#define ngx_queue_insert_head(h, x) \
(x)->next = (h)->next; \
(x)->next->prev = x; \
(x)->prev = h; \
(h)->next = x
在容器的头部插入x,跟双向链表的插入方法一模一样,只要调整各自的pre和next就可以。
其他的类似。那么怎么通过ngx_queue_t获取存取的元素呢?即获取TestNode,我们已经知道某个ngx_queue_t的地址了,要知道TestNode的地址,只要知道TestNode中ngx_queue_t的相对偏移就可以了,这就是宏定义
#define ngx_queue_data(q, type, link) \
(type *) ((u_char *) q - offsetof(type, link))
的作用了,q是类型ngx_queue_t成员的地址,type是TestNode,link是类型ngx_queue_t。此宏会返回TestNode的地址。所有宏定义(函数)的说明如下图:
控制链表的函数:
控制容器中的元素的函数:
1.2 ngx_array_t
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:指向内存池,动态数组最终分配的地方。
记住:ngx_array_t存放的数组一定是在pool里的,不是在栈上的。
涉及的控制函数如下:
ngx_array_t *ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size);
void ngx_array_destroy(ngx_array_t *a);
void *ngx_array_push(ngx_array_t *a);
void *ngx_array_push_n(ngx_array_t *a, ngx_uint_t n);
static ngx_inline ngx_int_t
ngx_array_init(ngx_array_t *array, ngx_pool_t *pool, ngx_uint_t n, size_t size)
ngx_array_t *ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size);创建一个动态数组,p指内存池,n指数组元素的个数,size指数组元素的大小。
ngx_array_create会调用ngx_init_t(ngx_array_t *array, ngx_pool_t *pool, ngx_uint_t n, size_t size)
下面看下ngx_init_t怎么实现的:
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;
}
代码很容易读懂,有几个问题:1 static; 此函数定义为只是本文件可见的原因是此函数只是被ngx_array_create使用,所以对外可隐藏起来;2 加了关键字ngx_inline,看下此定义(ngx_config.h line109):
/* TODO: auto_conf: ngx_inline inline __inline __inline__ */
#ifndef ngx_inline
#define ngx_inline inline
#endif
可见,是内联的意思,也就是函数被内联了,这样可以大大提高代码处理的速度,窥见nginx对速度追求。
void ngx_array_destroy(ngx_array_t *a);
只是回收了pool上的内存,实际上此内存并没有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) { //ngx_array_t本身也占用一段内存。
p->d.last = (u_char *) a;
}
}
void *ngx_array_push(ngx_array_t *a) 往数组里增加一个元素,这里会遇到stl的vector类似的问题,就是容器达到上限,处理方法也类似。先判断新加入的元素的大小是否会超过array的大小,不超过直接在末尾加入新元素即可,超过,先开辟一段大小是原来2倍的内存,再把原来的元素复制过去。
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;
}
1.3 ngx_list_t
在nginx的ngx_list_t.h定义了:
typedef struct ngx_list_part_s ngx_list_part_t;
struct ngx_list_part_s {
void *elts;
ngx_uint_t nelts;
ngx_list_part_t *next;
};
typedef struct {
ngx_list_part_t *last;
ngx_list_part_t part;
size_t size;
ngx_uint_t nalloc;
ngx_pool_t *pool;
} ngx_list_t;
与ngx_array_t类似,nelts表示已使用的数组元素个数,size表示每个数组元素的大小,nalloc指数组的大小。ngx_list_t是一个链表,但是链表的元素是数组ngx_list_part.
操作ngx_list_t的函数有:
ngx_list_t *ngx_list_create(ngx_pool_t *pool, ngx_uint_t n, size_t size);
void* ngx_list_push(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)
ngx_list_create创建一个ngx_list_t,里面只含一个ngx_list_part_t。
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;
}
if (ngx_list_init(list, pool, n, size) != NGX_OK) {
return NULL;
}
return list;
}
调用了ngx_list_init:
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);
if (list->part.elts == NULL) {
return NGX_ERROR;
}
list->part.nelts = 0;
list->part.next = NULL;
list->last = &list->part;
list->size = size;
list->nalloc = n;
list->pool = pool;
return NGX_OK;
}
ngx_list_push只是往链表里增加一个元素
void *
ngx_list_push(ngx_list_t *l)
{
void *elt;
ngx_list_part_t *last;
last = l->last;
if (last->nelts == l->nalloc) {
/* the last part is full, allocate a new list part */
last = ngx_palloc(l->pool, sizeof(ngx_list_part_t));
if (last == NULL) {
return NULL;
}
last->elts = ngx_palloc(l->pool, l->nalloc * l->size);
if (last->elts == NULL) {
return NULL;
}
last->nelts = 0;
last->next = NULL;
l->last->next = last;
l->last = last;
}
elt = (char *) last->elts + l->size * last->nelts;
last->nelts++;
return elt;
}
二 复杂的数据结构
2.1 hash的基本数据结构
nginx的哈希表用于快速查找,nginx的hash表只在初始化时建立好,以后只能进行查询。几个数据结构:
typedef struct {
void *value;
u_short len;
u_char name[1];
} ngx_hash_elt_t;
基本的hash数据结构,value指用户要查询的数据,即key/value中的value。
typedef struct {
ngx_hash_elt_t **buckets;
ngx_uint_t size;
} ngx_hash_t;
此表就是hash散列表,采用了一个二级指针,
为什么采用二级指针呢
?这就是nginx的节约内存所在(个人理解)。nginx哈希方式采用的开放寻址法,具体定义可以参考相关资料。如下图,所示,nginx先通过
key = names[n].key_hash % size;
elt = (ngx_hash_elt_t *) ((u_char *) buckets[key] + test[key]);
elt->value = names[n].value;
elt->len = (u_short) names[n].key.len;
计算hash槽, 再在槽里利用开放寻址法构造hash元素。
支持通配符特性,是nginx的hash函数的基本特征之一,正如我们输入“baidu.com”也能找到百度的首页一样,里面采用了通配符进行查找,即在服务器端设置了*.baidu.com的通配符。两个相关结构体:
typedef struct {
ngx_hash_t hash;
void *value;
} ngx_hash_wildcard_t;
typedef struct {
ngx_hash_t hash;
ngx_hash_wildcard_t *wc_head;//前置通配符 “*.baidu.com”
ngx_hash_wildcard_t *wc_tail;//后置通配符 "www.baidu.*"
} ngx_hash_combined_t;
2.2 hash表的初始化
利用三个函数,基本通配符的初始化,前置通配符的初始化和后置通配符的初始化:
ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,
ngx_uint_t nelts);//基本通配符的初始化
ngx_int_t ngx_hash_wildcard_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,
ngx_uint_t nelts);//前置通配符的初始化和后置通配符的初始化
ngx_hash_init_t是初始化结构体,其主要作用是指定hash表的大小等上下文信息
typedef struct {
ngx_hash_t *hash;
ngx_hash_key_pt key;
ngx_uint_t max_size;
ngx_uint_t bucket_size;
char *name;
ngx_pool_t *pool;
ngx_pool_t *temp_pool;
} ngx_hash_init_t;
ngx_hash_key_t主要存储了hash函数得到的哈希值
typedef struct {
ngx_str_t key;
ngx_uint_t key_hash;
void *value;
} ngx_hash_key_t;
利用ngx_hash_key_t对ngx_hash_init进行初始化。那么ngx_hash_key_t怎么来的呢,利用
typedef struct {
ngx_uint_t hsize;
ngx_pool_t *pool;
ngx_pool_t *temp_pool;
ngx_array_t keys;
ngx_array_t *keys_hash;
ngx_array_t dns_wc_head;
ngx_array_t *dns_wc_head_hash;
ngx_array_t dns_wc_tail;
ngx_array_t *dns_wc_tail_hash;
} ngx_hash_keys_arrays_t;
函数ngx_int_t ngx_hash_keys_array_init(ngx_hash_keys_arrays_t *ha, ngx_uint_t type);对ngx_hash_keys_arrays_t进行初始化,然后通过ngx_int_t ngx_hash_add_key(ngx_hash_keys_arrays_t *ha, ngx_str_t *key,
void *value, ngx_uint_t flags);
void *value, ngx_uint_t flags);构造ngx_hash_key_t。最后就可以利用ngx_hash_key_t数组来ngx_hash_init了。完成整个hash表的构造过程。