nginx源码分析2———基础数据结构二(链表和双向链表)

相关介绍

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,双向链表采用结构体嵌套,可以实现任意结构的双向链表,只要你想,而且都是采用宏,无论是伸缩性和性能都达到了极致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值