相关介绍
nginx提供了大量的基础数据结构,本章将着重介绍链表和队列,这两种数据结构和我们了解的实现有些许区别,该两种数据结构也贯穿整个nginx。
基础数据结构
链表
链表结构
ngx_list_t封装的是链表结构,在nginx使用比较广泛,http首部就是使用该数据结构。
struct ngx_list_part_s {
void *elts;
ngx_uint_t nelts;
ngx_list_part_t *next;
};
ngx_list_part_s是链表中的单个节点
elts 节点中存放具体元素的内存地址。
nelts 节点中已有元素个数。
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_list_t代表整个链表
last 指向该链表的最后一个节点。
part 该链表的首个存放具体元素的节点。
size 链表中存放的具体元素所需内存大小。
nalloc 每个节点所含的固定大小的数组的容量。
pool 该list使用的分配内存的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;
}
//初始化链表结构,接下来分析这个函数
if (ngx_list_init(list, pool, n, size) != NGX_OK) {
return NULL;
}
return 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);
if (list->part.elts == NULL) {
return NGX_ERROR;
}
/*part为该链表的首个元素存放具体元素的节点,由于元素中没有小元素,所以nelts为0,下一个元素为空 */
list->part.nelts = 0;
list->part.next = NULL;
//指向最后一个元素的指针也指向part,因为目前只有一个元素
list->last = &list->part;
//初始化单个元素的大小
list->size = size;
//容量为传入的n
list->nalloc = n;
//赋值分配内存的内存池
list->pool = pool;
return NGX_OK;
}
链表的压入小元素
压入小元素,元素有可能需要新分配,有可能不需要新元素
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;
}
//当前元素的小元素数量为0
last->nelts = 0;
//当前元素头节点下一个为空
last->next = NULL;
//在链表的尾部插入元素(是元素不是小元素)
l->last->next = last;
//当前元素为最后一个元素
l->last = last;
}
/*新增加的小元素的位置为 (最后一个元素的第一个小元素的地址 加上 最后一个元素已经使用的内存)
1,如果最后一个元素不是新分配的,的就按照上面的
2, 当最后一个元素是新分配的,那么该元素的使用量为0,直接返回第一个小元素的地址
*/
elt = (char *) last->elts + l->size * last->nelts;
//最后一个元素的使用量加1
last->nelts++;
return elt;
}
链表总结
其实链表的结构比一般的链表的结构复杂,因为每一个链表的内部都有一个数组(元素),该数组内部的一个元素的存放具体的内容(小元素),当然理解起来很简单的。
注意list->nalloc不是整个链表的容量,而是链表单个元素存放小元素的容量,在初始化以后不会改变。
双向链表
队列结构
typedef struct ngx_queue_s ngx_queue_t;
struct ngx_queue_s {
ngx_queue_t *prev;
ngx_queue_t *next;
};
从结构可以看出,该结构只包含了前向和后向的指针,并不包含实际数据,我们肯定对它的使用充满了疑问。
初始化
在初始化的时候,调用ngx_queue_init。
//初始化,将该queue的prev和next都指向自己,q相当于head
#define ngx_queue_init(q) \
(q)->prev = q; \
(q)->next = q
我们来看一看初始化的源码,队列操作定义了很多红宏。
双向链表插入
可见初始的时候节点的 prev 和 next 都指向自己,因此其实是一个空链表。
//检验该链表是不是empty
#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
(h)->prev指向的是双向链表的尾部。
双向链表的移除元素
移除很简单。
#define ngx_queue_remove(x) \
(x)->next->prev = (x)->prev; \
(x)->prev->next = (x)->next
相关示例
如果你还在犯迷糊,那么我们可以看一看limit_req模块对它的使用,
这是一个包含双向链表头的结构,在代码中用&ctx->sh表示。
typedef struct {
ngx_rbtree_t rbtree;
ngx_rbtree_node_t sentinel;
ngx_queue_t queue;
} ngx_http_limit_req_shctx_t;
这是一个双向链表节点,在代码中用&lr表示
typedef struct {
u_char color;
u_char dummy;
u_short len;
ngx_queue_t queue;
ngx_msec_t last;
/* integer value, 1 corresponds to 0.001 r/s */
ngx_uint_t excess;
ngx_uint_t count;
u_char data[1];
} ngx_http_limit_req_node_t;
注意,上面有两个结构,其实第一个结构里面放置了ngx_queue_t结构体,但是它并不是当作节点来使用,而是当作双向链表的头,就相当于第一个结构体里面放了一个双向链表。
而第二个结构里面也有ngx_queue_t,但是意义不一样,它代表它是一个节点,而不是代表一个双向链表或者双向链表的头。
在limit_req中初始化的时候,调用ngx_queue_init。此次初始化只是初始化双向链表。双向链表中没有元素。
ngx_queue_init(&ctx->sh->queue);
这是将元素(lr)从队列中移除,然后放到&ctx->sh->queue这个双向链表头部。
ngx_queue_remove(&lr->queue);
ngx_queue_insert_head(&ctx->sh->queue, &lr->queue);
从双向链表头部中取出元素,再通过元素获取数据,我贴出获取数据的宏
//取出元素
q = ngx_queue_last(&ctx->sh->queue);
//获取数据
lr = ngx_queue_data(q, ngx_http_limit_req_node_t, queue);
//取出数据就是使用offsetof减出结构体的首地址。
#define ngx_queue_data(q, type, link) \
(type *) ((u_char *) q - offsetof(type, link))
对于双向链表,一定要记住,ngx_queue_t结构体是有两个角色的,一个是代表一个元素,另一个代表的是整个双向链表。双向链表使用宏操作,而且有很强的伸缩性,只要是结构体,就可以当成双向链表操作。
总结
nginx的链表和队列都比较有趣味,和我们熟知的有一些区别,熟悉这两个数据结构,对于我们无论是了解nginx源码,还是做模块开发都有很好的帮助。
1,链表中每一个元素,都是可以存放多个实际的小元素数组(实际的数据)。
2,双向链表采用结构体嵌套,可以实现任意结构的双向链表,只要你想,而且都是采用宏,无论是伸缩性和性能都达到了极致。