Nginx基础教程(18)Nginx高级数据结构之双端队列:揭秘Nginx双端队列:小数据结构的威力何以惊人?

在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步:

  1. 修改新插入节点x的next指针指向h节点的下一个节点;
  2. 修改x的下一个节点的prev指针指向x;
  3. 修改x的prev指针指向h节点;
  4. 修改头指针指向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);

这种设计方式的好处是:

  1. 通用性强:同一个队列可以管理不同类型的数据结构
  2. 内存高效:不需要为每个节点额外分配内存
  3. 性能优越:通过简单的指针运算即可获取数据,无需查找

这就像是通过一个人的身份证号码可以找到他的完整信息一样,身份证号码本身并不包含所有信息,但它是指向完整信息的关键。

实际应用示例

为了更好理解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双端队列在实际应用中的威力。通过简单地操作队列,我们就实现了一个高效的文件缓存管理系统,它能够:

  1. 限制缓存中的文件数量
  2. 自动淘汰最久未使用的文件
  3. 在文件被访问时更新其位置

这种设计模式在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核心代码中,是因为它具有显著的性能优势:

  1. 时间复杂度低:插入、删除操作的时间复杂度都是O(1)
  2. 内存开销小:每个节点只需要两个指针,没有额外的内存开销
  3. 通用性强:可以用于管理任意类型的数据结构

在Nginx中,双端队列被广泛应用于多种场景:

  • 连接管理:维护客户端连接队列
  • 定时器管理:管理定时事件队列
  • 内存管理:组织内存块队列
  • 文件缓存:管理缓存文件队列

总结

Nginx的双端队列是一个设计精妙的数据结构,它体现了Nginx的设计哲学——简单、高效、通用。ngx_queue_t为Nginx提供了简单而强大的双向链表操作功能。其接口简洁,使用方便,适用于各种需要快速插入和删除元素的场景。

虽然它本身只是一个简单的双向队列,但通过巧妙的设计,特别是ngx_queue_data宏的使用,使得它可以灵活地管理各种类型的数据,在Nginx的高并发处理中发挥着关键作用。

通过学习Nginx双端队列的设计和实现,我们不仅能更好地理解Nginx的内部工作机制,还能汲取宝贵的设计思想,应用于自己的项目开发中,构建高性能、可扩展的应用程序。

下次当你使用Nginx时,不妨想一想这个默默工作在幕后的小巧数据结构,正是它和它的伙伴们一起,支撑起了整个高性能Web服务器的世界。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

值引力

持续创作,多谢支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值