目录
一、基本概述
二、数据结构
三、接口描述与实现
1、相关宏接口
2、ngx_queue_middle
3、ngx_queue_sort
四、使用案例
整理自 nginx 1.9.2 源码 和 《深入理解 Nginx:模块开发与架构解析》
一、基本概述
双向链表的优势是可以快速进行数据插入、删除与合并操作,但其查询操作没有数组性能高。
nginx 下 ngx_queue_t 还具备如下优点:
(1)实现了排序功能;
(2)不负责节点元素的内存分配操作,只提供轻量级的节点管理功能;
(3)内存空间占用较小,每个节点元素只占用两个指针的内存损耗;
二、数据结构
typedef struct ngx_queue_s ngx_queue_t;
struct ngx_queue_s {ngx_queue_t *prev;ngx_queue_t *next;
};
Nginx 在设计 ngx_queue_t
时,由于容器与元素共用了 ngx_queue_t
结构体,为了避免 ngx_queue_t
结构体成员的意义混乱,Nginx封装了链表容器与元素的所有方法,这种情况非常少见,其他容器都需要直接使用成员变量来访问,唯有 ngx_queue_t
双向链表只能使用 API 接口进行数据访问。
三、接口描述与实现
接口大多使用宏进行封装。
1、相关宏接口
// 初始化链表容器 q,并置为空
#define ngx_queue_init(q) \(q)->prev = q; \(q)->next = q// 检测链表容器是否为空,返回结果为 0 表示链表为空
#define ngx_queue_empty(h) \(h == (h)->prev)// 将 x 节点插到 h 节点的后面一位
#define ngx_queue_insert_head(h, x) \(x)->next = (h)->next; \(x)->next->prev = x; \(x)->prev = h; \(h)->next = x// 将 x 节点插入 q 节点之后,此处可以直接复用 ngx_queue_insert_head
#define ngx_queue_insert_after ngx_queue_insert_head// 将 x 插入 h 节点前面,链表首尾相连
#define ngx_queue_insert_tail(h, x) \(x)->prev = (h)->prev; \(x)->prev->next = x; \(x)->next = h; \(h)->prev = x// 返回链表容器 h 中的第一个元素节点 ngx_queue_t 指针
#define ngx_queue_head(h) \(h)->next// 返回链表容器 h 中最后一个元素节点 ngx_queue_t 指针
#define ngx_queue_last(h) \(h)->prev// 返回容器链表结构体的指针
#define ngx_queue_sentinel(h) \(h)// 返回 q 元素的下一个元素
#define ngx_queue_next(q) \(q)->next// 返回 q 元素的前一个元素
#define ngx_queue_prev(q) \(q)->prev// 从链表中移除 x 节点,注意因为是双向链表,所以只需要 x 节点作为参数即可
#define ngx_queue_remove(x) \(x)->next->prev = (x)->prev; \(x)->prev->next = (x)->next/* h 为链表容器,q 为链表 h 中的一个元素,这个方法可以将链表 h 以元素 q 为界拆分为两个链表 h 和n,其中 h 由原链表的前半部分组成(不包含 q),而 n 由后半部分组成,q 为首元素,如果以前 n 有成员,则新的 n 为从 h 中拆分的部分加上 n 原有的数据
*/
#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;// 将链表 n 合并到 h 链表的末尾
#define ngx_queue_add(h, n) \(h)->prev->next = (n)->next; \(n)->next->prev = (h)->prev; \(h)->prev = (n)->prev; \(h)->prev->next = h;/*返回 q 元素(ngx_queue_t类型)所属结构体的地址。q 为链表中某个节点指针 ngx_queue_t 类型;type 为链表元素的结构体类型名称(该结构体中必须包含 ngx_queue_t 类型的成员);1ink 是上面这个结构体中 ngx_queue_t 类型的成员名字;例如:typedef struct {u_char* str;ngx_queue_t qEle;int num;} TestNode;
*//* Offset of member MEMBER in a struct of type TYPE. */
#define offsetof(TYPE, MEMBER) __builtin_offsetof (TYPE, MEMBER)
#define ngx_queue_data(q, type, link) \(type *) ((u_char *) q - offsetof(type, link))
2、ngx_queue_middle
返回链表的中心元素,例如链表共有 N 个元素,则 ngx_queue_middle
将返回第(N/2 + 1)个元素。
ngx_queue_t *
ngx_queue_middle(ngx_queue_t *queue)
{ngx_queue_t *middle, *next;middle = ngx_queue_head(queue);if (middle == ngx_queue_last(queue)) {return middle;}next = ngx_queue_head(queue);/*middle 指针每次循环探索一步、next 指针每次循环探索两步;当 next 抵达链表尾部时,middle 正好在链表中心位置。*/for ( ;; ) {middle = ngx_queue_next(middle);next = ngx_queue_next(next);if (next == ngx_queue_last(queue)) {return middle;}next = ngx_queue_next(next);if (next == ngx_queue_last(queue)) {return middle;}}
}
3、ngx_queue_sort
void
ngx_queue_sort(ngx_queue_t *queue,ngx_int_t (*cmp)(const ngx_queue_t *, const ngx_queue_t *))
{ngx_queue_t *q, *prev, *next;q = ngx_queue_head(queue);if (q == ngx_queue_last(queue)) {return;}for (q = ngx_queue_next(q); q != ngx_queue_sentinel(queue); q = next) {prev = ngx_queue_prev(q);next = ngx_queue_next(q);// q 节点是当前需要排序的节点ngx_queue_remove(q); // 下面循环将决定把 q 节点插入到什么位置;// 从 q 节点的前面节点开始比较,找到合适的位置再插入。do {// 自定义排序函数,可以降序或升序if (cmp(prev, q) <= 0) {break;}prev = ngx_queue_prev(prev);} while (prev != ngx_queue_sentinel(queue)); //查找这个元素需要插入到前面依据拍好序的队列的那个地方// 找到合适位置后插入该节点ngx_queue_insert_after(prev, q);}
}
四、使用案例
定义一个简单的链表,并使用 ngx_queue_sort
方法对所有元素排序。在这个例子中,可以看到如何定义、初始化 ngx_queue_t
容器,如何定义任意类型的链表元素,如何遍历链表,如何自定义排序方法并执行排序。
// 链表元素结构体中必须包含 ngx_queue_t 类型的成员,它可以在任意位置
typedef struct
{u_char* str;ngx_queue_t qEle;int num;
} TestNode;// 升序排序
ngx_int_t compTestNode(const ngx_queue_t* a, const ngx_queue_t* b)
{/*首先使用 ngx_queue_data 方法由 ngx_queue_t 变量获取元素结构体 TestNode 的地址 */TestNode* aNode = ngx_queue_data(a, TestNode, qEle);TestNode* bNode = ngx_queue_data(b, TestNode, qEle);//返回 num 成员的比较结果return aNode->num > bNode->num;
}// 定义双向链表容器 queueContainer,并将其初始化为空链表
// 注意,ngx_queue_t 结构体遍历必须使用 ngx_queue_init 初始化
ngx_queue_t queueContainer;
ngx_queue_init(&queueContainer);
ngx_queue_t
双向链表是完全不负责分配内存的,每一个链表元素必须自己管理自己所占用的内存。因此,本例在进程栈中定义了 5 个 TestNode
结构体作为链表元素,并把它们的 num 成员初始化为 0,1,2,3,4, 如下所示。
int i = 0;
TestNode node[5];
for (; i <5; i++)
{node[i].num = i;
}
下面把这 5 个 TestNode
结构体添加到 queueContainer
链表中,注意,这里同时使用了ngx_queue_insert_tail
、ngx_queue_insert_head
、ngx_queue_insert_after
3 个添加方法,链表中元素顺序以 num 标识应该为:3、1、0、2、4。
ngx_queue_insert_tail(&queueContainer, &node[0] qEle);
ngx_queue_insert_head(&queueContainer, &node[1].qEle);
ngx_queue_insert_tail(&queueContainer, &node[2].qEle);
// 在头节点之后插入
ngx_queue_insert_after(&queueContainer, &node[3].qEle);
ngx_queue_insert_tail(&queueContainer, &node[4].qEle);
先排序,再从链表头部遍历到链表尾部。反向遍历可以使用 ngx_queue_last 和 ngx_queue_prev 实现。
// 升序排序
ngx_queue_sort(&queueContainer, compTestNode);
// 遍历链表
ngx_queue_t* q;
for (q = ngx_queue_head(&queueContainer); q != ngx_queue_sentinel(&queueContainer);q = ngx_queue_next(q))
{TestNode* eleNode = ngx_queue_data(q, TestNode, qEle);// 处理当前的链表元素 eleNode// ...
}
使用案例还可以参考:Nginx 源码学习-ngx的基本容器-ngx_queue-xueliangfei-ChinaUnix博客