在Nginx高性能的幕后,一个看似简单的双端队列正默默地发挥着关键作用。
什么是双端队列?
队列通常是一种先进先出的数据结构,在实际工作中多用于异步的任务处理。而Nginx的队列由包含头节点的双向循环链表实现,是一种双向队列。
简单来说,双端队列就是一种两端都可以进行插入和删除操作的队列。它打破了传统队列只能从一端进入、另一端出来的限制,赋予了数据管理更大的灵活性。
在Nginx中,双端队列的定义非常简单,只有两个指针:
typedef struct ngx_queue_s ngx_queue_t;
struct ngx_queue_s {
ngx_queue_t *prev; // 指向前一个节点
ngx_queue_t *next; // 指向后一个节点
};
从上述结构体定义可以看出,队列只有前驱和后继节点。这种简洁的设计正是Nginx哲学的一个体现——用最简单的结构解决最复杂的问题。
Nginx双端队列的设计原理
为什么Nginx要选择自己实现一个双端队列,而不是使用现成的数据结构呢?答案就在于性能与资源消耗的平衡。
在Nginx的队列实现中,实质就是具有头节点的双向循环链表,这里的双向链表中的节点是没有数据区的,只有两个指向节点的指针。
这意味着什么?这意味着ngx_queue_t并不直接存储数据,而是作为一个嵌入到其他结构体中的成员,通过指针关系来组织数据。
想象一下,你有一堆书需要管理,传统的方式是准备一个书架,把书放进去。而Nginx的方式是给每本书贴上一个标签,标签之间用绳子连接起来,这样只需要操作标签就能管理书籍,省去了书架的空间和管理开销。
这种设计的精妙之处在于队列的内存分配不是直接从内存池分配的,即没有进行内存池管理,而是需要我们自己管理内存。这给了开发者更大的灵活性,可以根据实际情况选择最合适的内存管理方式。
Nginx双端队列的基本操作
了解了设计原理,让我们来看看Nginx双端队列的具体操作方法是怎样的。这些操作主要通过一系列宏定义来实现,简洁而高效。
初始化队列
在使用任何队列之前,都需要先进行初始化:
ngx_queue_t my_queue;
ngx_queue_init(&my_queue); // 初始化队列
初始化队列的宏定义如下:
#define ngx_queue_init(q) \
(q)->prev = q; \
(q)->next = q
当头节点的prev和next指针都指向自己时,队列是只有一个头节点的空队列。这就像组建一个团队,先选出一个组长,但他暂时没有组员,所以只能自己领导自己。
判断队列是否为空
#define ngx_queue_empty(h) \
(h == (h)->prev)
当头节点的prev指向自身的时候,说明没有有效节点。判断队列是否为空非常重要,可以避免在对空队列进行操作时出现错误。
插入元素
Nginx双端队列提供了多种插入方式,最常见的是头插法和尾插法:
头插法:
#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_tail(h, x) \
(x)->prev = (h)->prev; \
(x)->prev->next = x; \
(x)->next = h; \
(h)->prev = x
以头插法为例,插入一个节点分以下4步:
- 修改新插入节点x的next指针指向h节点的下一个节点;
- 修改x的下一个节点的prev指针指向x;
- 修改x的prev指针指向h节点;
- 修改头指针指向x。
这个过程就像是排队时,突然来了一个VIP客户,他不需要排在队尾,而是直接站到了队伍的最前面,而且队伍中的每个人都欣然接受了这个安排。
删除元素
#define ngx_queue_remove(x) \
(x)->next->prev = (x)->prev; \
(x)->prev->next = (x)->next
删除节点原理更简单,直接修改要删除节点的前后节点指针即可,并没有释放节点内存。释放内存的操作应由开发者自己来完成。
这就像是把一列火车中的一节车厢卸下来——你不需要销毁车厢,只需要把前后车厢重新连接起来,卸下来的车厢可以停到停车场备用。
遍历队列
遍历是队列操作中最常见的任务之一:
ngx_queue_t *q;
for (q = ngx_queue_head(&my_queue);
q != ngx_queue_sentinel(&my_queue);
q = ngx_queue_next(q))
{
// 处理每个元素
}
这个过程就像是跟着一根绳子上的结一个一个摸过去,每摸到一个结就知道这里有一个元素,可以对其进行操作。
关键技巧:如何从队列节点获取实际数据?
前面提到,ngx_queue_t结构本身并不包含数据区,那么如何通过队列节点获取实际的数据呢?这是Nginx双端队列设计中最为精妙的部分。
Nginx提供了ngx_queue_data宏来解决这个问题:
#define ngx_queue_data(q, type, link) \
(type *) ((u_char *) q - offsetof(type, link))
这个宏的定义初看可能有些晦涩,让我们来分解一下:
q:队列节点的指针type:实际数据结构的类型link:在实际数据结构中ngx_queue_t类型的成员名
offsetof(type,link)是C语言的一个库函数,它返回一个结构体成员相对于结构体开头的字节偏移量。而(u_char *) q - offsetof(type, link)则是将指针向前移动,从而得到整个结构体的起始地址。
举个例子,假设我们有一个包含ngx_queue_t的结构体:
typedef struct {
int priority;
char *description;
ngx_queue_t queue;
} ngx_work_item_t;
当我们有一个指向queue成员的指针时,可以通过以下方式获取完整的ngx_work_item_t结构:
ngx_work_item_t *work_item = ngx_queue_data(q, ngx_work_item_t, queue);
这种设计方式的好处是:
- 通用性强:同一个队列可以管理不同类型的数据结构
- 内存高效:不需要为每个节点额外分配内存
- 性能优越:通过简单的指针运算即可获取数据,无需查找
这就像是通过一个人的身份证号码可以找到他的完整信息一样,身份证号码本身并不包含所有信息,但它是指向完整信息的关键。
实际应用示例
为了更好理解Nginx双端队列的使用,让我们来看一个完整的示例。在高并发HTTP反向代理服务器Nginx中,存在着一个跟性能息息相关的模块 - 文件缓存。
经常访问到的文件会被nginx从磁盘缓存到内存,这样可以极大的提高Nginx的并发能力,不过因为内存的限制,当缓存的文件数达到一定程度的时候就会采取淘汰机制,优先淘汰进入时间比较久或是最近访问很少的队列文件。
以下是一个简化的实现:
#include <stdlib.h>
#include <stdio.h>
#include "nginx_queue.h"
// 定义文件缓存结构
typedef struct ngx_cached_open_file_s {
int fd; // 文件描述符
time_t last_accessed; // 最后访问时间
ngx_queue_t queue; // 队列节点
} ngx_cached_file_t;
// 定义文件缓存管理器
typedef struct {
ngx_queue_t expire_queue; // 过期队列
size_t max_files; // 最大文件数
size_t current_files; // 当前文件数
} ngx_open_file_cache_t;
// 初始化文件缓存
ngx_open_file_cache_t* cache_init(size_t max_files) {
ngx_open_file_cache_t *cache = malloc(sizeof(ngx_open_file_cache_t));
ngx_queue_init(&cache->expire_queue);
cache->max_files = max_files;
cache->current_files = 0;
return cache;
}
// 添加文件到缓存
void cache_add_file(ngx_open_file_cache_t *cache, int fd) {
ngx_cached_file_t *file = malloc(sizeof(ngx_cached_file_t));
file->fd = fd;
file->last_accessed = time(NULL);
// 将新文件插入队列头部
ngx_queue_insert_head(&cache->expire_queue, &file->queue);
cache->current_files++;
// 如果超过最大限制,移除最老的文件
if (cache->current_files > cache->max_files) {
ngx_queue_t *last = ngx_queue_last(&cache->expire_queue);
ngx_cached_file_t *old_file = ngx_queue_data(last, ngx_cached_file_t, queue);
printf("移除旧文件: %d\n", old_file->fd);
ngx_queue_remove(last);
cache->current_files--;
free(old_file);
}
}
// 访问文件,更新其位置
void cache_access_file(ngx_open_file_cache_t *cache, int fd) {
ngx_queue_t *q;
for (q = ngx_queue_head(&cache->expire_queue);
q != ngx_queue_sentinel(&cache->expire_queue);
q = ngx_queue_next(q))
{
ngx_cached_file_t *file = ngx_queue_data(q, ngx_cached_file_t, queue);
if (file->fd == fd) {
// 找到文件,将其移动到队列头部
ngx_queue_remove(q);
ngx_queue_insert_head(&cache->expire_queue, q);
file->last_accessed = time(NULL);
printf("已更新文件 %d 的访问时间和位置\n", fd);
return;
}
}
printf("未找到文件 %d\n", fd);
}
// 遍历并打印缓存中的所有文件
void cache_list_files(ngx_open_file_cache_t *cache) {
ngx_queue_t *q;
printf("当前缓存中的文件: ");
for (q = ngx_queue_head(&cache->expire_queue);
q != ngx_queue_sentinel(&cache->expire_queue);
q = ngx_queue_next(q))
{
ngx_cached_file_t *file = ngx_queue_data(q, ngx_cached_file_t, queue);
printf("%d ", file->fd);
}
printf("\n");
}
int main() {
// 初始化缓存,最多保存3个文件
ngx_open_file_cache_t *cache = cache_init(3);
// 添加一些文件
cache_add_file(cache, 101);
cache_add_file(cache, 102);
cache_add_file(cache, 103);
cache_list_files(cache);
// 访问文件102,它应该被移动到队列头部
cache_access_file(cache, 102);
cache_list_files(cache);
// 添加新文件,这将导致最老的文件被移除
cache_add_file(cache, 104);
cache_list_files(cache);
return 0;
}
这个示例展示了Nginx双端队列在实际应用中的威力。通过简单地操作队列,我们就实现了一个高效的文件缓存管理系统,它能够:
- 限制缓存中的文件数量
- 自动淘汰最久未使用的文件
- 在文件被访问时更新其位置
这种设计模式在Nginx中随处可见,是Nginx高性能的关键所在。
高级操作:队列拆分与排序
除了基本操作,Nginx双端队列还提供了一些高级功能,如队列拆分和排序,进一步扩展了其应用场景。
队列拆分
#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;
这段代码的含义为:以数据q为界,将队列h拆分为h和n两个队列,其中拆分后数据q位于第二个队列中。队列拆分实际上是以数据q作为第二个队列的第一个节点。
这就像把一列火车分成两列,以某节车厢为分界点,前面的车厢组成一列火车,后面的车厢组成另一列火车。
队列排序
Nginx提供了对队列的排序功能,使用插入排序算法:
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);
ngx_queue_remove(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_int_t ngx_work_item_comparator(const ngx_queue_t *a, const ngx_queue_t *b) {
ngx_work_item_t *item_a = ngx_queue_data(a, ngx_work_item_t, queue);
ngx_work_item_t *item_b = ngx_queue_data(b, ngx_work_item_t, queue);
return item_a->priority - item_b->priority;
}
// 排序操作
ngx_queue_sort(&my_queue, ngx_work_item_comparator);
在这个例子中,ngx_work_item_comparator是一个比较函数,它根据工作项的优先级对队列进行排序。
性能分析与应用场景
Nginx双端队列之所以被广泛应用于Nginx核心代码中,是因为它具有显著的性能优势:
- 时间复杂度低:插入、删除操作的时间复杂度都是O(1)
- 内存开销小:每个节点只需要两个指针,没有额外的内存开销
- 通用性强:可以用于管理任意类型的数据结构
在Nginx中,双端队列被广泛应用于多种场景:
- 连接管理:维护客户端连接队列
- 定时器管理:管理定时事件队列
- 内存管理:组织内存块队列
- 文件缓存:管理缓存文件队列
总结
Nginx的双端队列是一个设计精妙的数据结构,它体现了Nginx的设计哲学——简单、高效、通用。ngx_queue_t为Nginx提供了简单而强大的双向链表操作功能。其接口简洁,使用方便,适用于各种需要快速插入和删除元素的场景。
虽然它本身只是一个简单的双向队列,但通过巧妙的设计,特别是ngx_queue_data宏的使用,使得它可以灵活地管理各种类型的数据,在Nginx的高并发处理中发挥着关键作用。
通过学习Nginx双端队列的设计和实现,我们不仅能更好地理解Nginx的内部工作机制,还能汲取宝贵的设计思想,应用于自己的项目开发中,构建高性能、可扩展的应用程序。
下次当你使用Nginx时,不妨想一想这个默默工作在幕后的小巧数据结构,正是它和它的伙伴们一起,支撑起了整个高性能Web服务器的世界。
821

被折叠的 条评论
为什么被折叠?



