万字详解Nginx基础数据结构

nginx数据结构

20210709173511720

整型数据类型ngx_int_t

typedef intptr_t        ngx_int_t;
typedef uintptr_t       ngx_uint_t;
typedef intptr_t        ngx_flag_t;

其中,intptr_tuintptr_t 是标准库 stdint.h 中定义的整数类型,它们用于存储指针值。它们的定义如下:

typedef signed long   intptr_t;
typedef unsigned long uintptr_t;
    • intptr_t:这是一个有符号整数类型,能够存储一个指针。它的大小与指针相同,可以安全地存储和操作指针值。
    • uintptr_t:这是一个无符号整数类型,能够存储一个指针。与 intptr_t 类似,它的大小也与指针相同。
基础巩固

什么是有符号整数类型和无符号整数类型?

有符号整数类型

有符号整数类型(Signed Integer Type)是可以表示正数、负数和零的整数类型。它的最高位通常用作符号位(0 表示正,1 表示负)。在 C 语言中,常见的有符号整数类型包括 intshortlong 等。

例如,在一个 8 位(1 字节)的有符号整数中:

  • 最高位是符号位。
  • 值范围是从 -128 到 127。

二进制表示:

  • 01111111 (127)
  • 10000000 (-128)
无符号整数类型

无符号整数类型(Unsigned Integer Type)只能表示非负整数(正数和零)。所有位都用于表示数值,因此可以表示更大的正数范围。常见的无符号整数类型包括 unsigned intunsigned shortunsigned long 等。

例如,在一个 8 位(1 字节)的无符号整数中:

  • 没有符号位,所有位都用于表示数值。
  • 值范围是从 0 到 255。

二进制表示:

  • 00000000 (0)
  • 11111111 (255)

为什么是intptr_t和uintptr_t 而不是 int ?

对于 C 语言的实现,intptr_tuintptr_t 是定义为与平台的指针大小相同的有符号和无符号整数类型。这意味着在 32 位平台上,它们通常是 32 位整数,而在 64 位平台上,它们通常是 64 位整数。

这种类型的定义是为了确保在不同平台上具有相同的大小,从而使得代码在不同平台上具有可移植性

整数类型与指针的关系?

  • 指针类型:指针类型用于存储内存地址,表示变量或数据在内存中的位置。指针的大小取决于系统架构(例如,32 位系统上的指针为 4 字节,64 位系统上的指针为 8 字节)。
  • 整数类型与指针:在某些情况下,可以使用整数类型来存储或操作指针值。例如,intptr_tuintptr_t 是标准库中定义的类型,用于存储可以容纳指针值的有符号和无符号整数。这在需要将指针转化为整数或将整数转化为指针的操作中很有用。

整数类型在不同的系统架构(如 32 位和 64 位)有何区别?

32 位系统
  • 整数类型:典型地,int 为 4 字节,long 为 4 字节,pointer 为 4 字节。
  • 指针类型:指针占用 4 字节,能够表示的地址范围是 0 到 2^32 - 1(4 GB)。
64 位系统
  • 整数类型int 仍然通常为 4 字节,longpointer 为 8 字节。
  • 指针类型:指针占用 8 字节,能够表示的地址范围是 0 到 2^64 - 1(16 EB)。

字符串数据类型ngx_str_t

typedef struct {
    size_t      len;    /* 字符串的长度 */
    u_char     *data;   /* 指向字符串的第一个字符 */
} ngx_str_t;
结构体成员
  • size_t len: 这是一个无符号整数类型,用于存储字符串的长度。size_t 类型通常用于表示对象的大小或数组的长度,它的大小依赖于具体的系统架构(在32位系统上通常是4字节,在64位系统上通常是8字节)。
  • u_char *data: 这是一个指向字符串第一个字符的指针。u_char 通常是 unsigned char 的别名,表示无符号字符类型。这种类型保证了字符的值在 0 到 255 之间。
设计优点

为什么这里要附加一个字符串长度【len】相比较于传统string的优点?

其实这种字符串处理方式在高性能场景下非常有用,尤其是在需要频繁处理字符串的网络服务器中,例如,再看redis的字符串设计:

struct __attribute__ ((__packed__)) sdshdr8 {
    //被使用的长度
    uint8_t len; /* used */
    //除去头跟空终止符分配的空间
    uint8_t alloc; /* excluding the header and null terminator */
    //标识字符,3位用于类型,5位未使用
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    //使用存储字符串的地方
    char buf[];
};

它不依赖于以 '\0' 结尾来标识字符串的结束,而是明确地记录了字符串的长度。这种设计有几个优势:

  1. 性能优化:操作字符串时不需要遍历整个字符串寻找 '\0',可以直接通过长度进行操作。
  2. 支持二进制数据:能够处理包含 '\0' 字节的字符串或二进制数据,因为长度是显式存储的。
  3. 更好的安全性:避免了因缺少字符串结束符导致的缓冲区溢出等安全问题。
基础巩固

是否熟悉以下数据类型?

size_t 是一种无符号整数类型,用于表示对象的大小或数组的长度。它的具体大小取决于系统架构,在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。

unsigned int:无符号整型,在大多数平台上为 4 字节。

unsigned long:无符号长整型,在 32 位系统上通常为 4 字节,在 64 位系统上通常为 8 字节。

u_char 通常是 unsigned char 的别名,用于表示无符号字符类型。它可以存储 0 到 255 之间的整数。大多数情况下,uint8_tunsigned char 可以互换使用。

uint8_t 是一种标准整数类型,主要优势在于它的跨平台一致性。无论是在 32 位系统还是 64 位系统上,uint8_t 总是表示 8 位无符号整数

uint16_t:表示 16 位无符号整数,取值范围为 0 到 65535。它保证在所有平台上都是 16 位,因此非常适合用于需要明确双字节大小的场合,如某些文件格式和协议字段等。

unsigned short:在许多平台上,unsigned short 通常是 16 位无符号整数,取值范围也是 0 到 65535。

uint_least16_t:确保至少有 16 位的无符号整数类型。

uint_fast16_t:确保至少有 16 位且运算速度最快的无符号整数类型。

内存池数据类型ngx_pool_s

    int *ptr;
    int size = 10;  // 分配内存空间的大小
    // 分配内存空间
    ptr = (int *)malloc(size * sizeof(int));
    .......
    free(ptr);

当我们使用malloc为程序申请内存时,会无法避免的出现内存碎片问题。

内存碎片

这对于长时间运行的系统或者需要高性能的系统是致命的。

  1. 内存浪费: 内存碎片导致一些内存无法被有效利用,即使整体上有足够的可用内存,但无法分配给大块连续的内存请求。这种情况下,系统实际可用内存会减少,造成内存资源的浪费。
  2. 性能下降: 内存碎片可能导致内存分配效率下降。当请求大块连续内存时,系统需要搜索并合并碎片化的内存块,这可能会增加内存分配的开销和时间。此外,内存碎片也可能导致页面置换算法的性能下降,因为系统需要更频繁地进行页面换入换出操作。
  3. 内存泄漏: 内存碎片可能导致内存泄漏问题的难以发现和排查。即使系统中存在大量的可用内存,但如果这些内存被分散成了小块碎片,而且这些碎片被长期占用而未被释放,就会导致整体内存资源的浪费和不足。
  4. 系统稳定性降低: 当内存碎片达到一定程度时,可能会导致系统出现内存耗尽的情况,从而导致系统崩溃或者运行异常。特别是在长时间运行的系统中,内存碎片可能会逐渐积累,最终导致系统的稳定性降低。

解决方案之一就是使用内存池,这里推荐大家一个手写内存池项目

简短的来说,内存池通过预先分配一定大小的内存块,并按需分配和释放这些内存块,以减少内存碎片的产生。

现在再回到Nginx的内存池上:

/* 文件 core/ngx_palloc.h */
typedef struct ngx_pool_s            ngx_pool_t;
//指向以 void *data 作为参数并且没有返回值的函数的指针。
typedef void (*ngx_pool_cleanup_pt)(void *data);
typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;

struct ngx_pool_cleanup_s {
    ngx_pool_cleanup_pt   handler;  // 清理函数指针,用于在释放内存时执行清理操作
    void                 *data;     // 清理函数的参数
    ngx_pool_cleanup_t   *next;     // 指向下一个清理结构体的指针,构成清理链表
};

typedef struct ngx_pool_large_s  ngx_pool_large_t;

struct ngx_pool_large_s {
    ngx_pool_large_t     *next;     // 指向下一个大块内存结构体的指针,构成大块内存链表
    void                 *alloc;    // 分配的大块内存的指针
};

typedef struct {
    u_char               *last;     // 当前内存分配的结束位置,下一段可分配内存的起始位置
    u_char               *end;      // 内存池的结束位置
    ngx_pool_t           *next;     // 指向下一个内存池的指针
    ngx_uint_t            failed;   // 记录内存池内存分配失败的次数
} ngx_pool_data_t;

struct ngx_pool_s {
    ngx_pool_data_t       d;        // 内存池的数据块,包括可分配内存的起始位置、结束位置和下一个内存池的指针等信息
    size_t                max;      // 内存池数据块的最大值
    ngx_pool_t           *current;  // 指向当前内存池的指针
    ngx_chain_t          *chain;    // 指向一个数据链表的指针
    ngx_pool_large_t     *large;    // 指向一个大块内存链表的指针
    ngx_pool_cleanup_t   *cleanup;  // 指向内存池清理结构体的指针,用于执行内存清理操作
    ngx_log_t            *log;      // 内存分配相关的日志信息
};

typedef struct {
    ngx_fd_t              fd;       // 文件描述符
    u_char               *name;     // 文件名
    ngx_log_t            *log;      // 内存分配相关的日志信息
} ngx_pool_cleanup_file_t;

内存池各组件结构之间的关系:

nginx内存池其中这里的ngx_pool_cleanup_pt是一个比较好的设计,通过定义这种类型的函数指针,可以将不同的清理函数关联到内存池中,以便在内存池销毁时调用。比如说:处理内存池销毁时需要执行的清理任务。例如,关闭打开的文件,释放分配的资源等。

关于内存池相关的函数,可能以后会具体开一章讲解。这里先附上执行流程图

nginx内存池函数流程

这里说一个在内存池设计中,常见的方法:以特定对齐方式分配内存

//调用 posix_memalign 函数,该函数会尝试以特定的对齐方式 alignment 分配指定大小 size 的内存块,并将分配的内存地址存储在 p 中。
posix_memalign(&p, alignment, size)

内存对齐

在内存中我们一般读取数据不是按内存来读取,一般都是按内存块来读取。

未对齐的情况下,当需要访问int类型的数据时,需要CPU访问2次内存块(内存块1和内存块2)

对齐的情况下,当访问int类型的数据时,只需要CPU访问1次内存块(内存块2)即可

是一种空间换时间的做法

缓冲区数据类型ngx_buf_s

nginx缓冲区

typedef void *            ngx_buf_tag_t;  // 缓冲区标签类型,用于标识缓冲区的特定类型

typedef struct ngx_buf_s  ngx_buf_t;  // Nginx 缓冲区结构体

struct ngx_buf_s {
    u_char          *pos;            // 缓冲区数据在内存的起始位置
    u_char          *last;           // 缓冲区数据在内存的结束位置
    off_t            file_pos;       // 文件读取偏移量
    off_t            file_last;      // 文件读取结束位置

    u_char          *start;          /* 缓冲区的起始地址 */
    u_char          *end;            /* 缓冲区的结束地址 */
    ngx_buf_tag_t    tag;            // 缓冲区的标签
    ngx_file_t      *file;           // 缓冲区关联的文件对象
    ngx_buf_t       *shadow;         // 当前缓冲区的影子缓冲区
    unsigned         temporary:1;    // 缓冲区数据可以被修改的标志
    unsigned         memory:1;       // 缓冲区数据在内存中的标志,且不可修改
    unsigned         mmap:1;         // 缓冲区数据是通过 mmap 映射的,且不可修改
    unsigned         recycled:1;     // 缓冲区是否已经被回收
    unsigned         in_file:1;      // 缓冲区的数据是否在文件中
    unsigned         flush:1;        // 缓冲区是否需要刷新
    unsigned         sync:1;         // 缓冲区是否需要同步
    unsigned         last_buf:1;     // 缓冲区是否为最后一个缓冲区
    unsigned         last_in_chain:1; // 缓冲区是否是链表中的最后一个
    unsigned         last_shadow:1;   // 缓冲区是否是影子缓冲区链表中的最后一个
    unsigned         temp_file:1;     // 缓冲区是否是临时文件
    /* STUB */ int   num;             // 缓冲区编号(占位符)
};

typedef struct ngx_chain_s {
    ngx_buf_t    *buf;               // 缓冲区指针
    ngx_chain_t  *next;              // 下一个链表节点指针
} ngx_chain_t;                        // Nginx 链表节点结构体

typedef struct {
    ngx_int_t    num;                // 缓冲区数量
    size_t       size;               // 缓冲区大小
} ngx_bufs_t;                        // 缓冲区参数结构体

其中,ngx_chain_t 数据类型是与缓冲区类型 ngx_buf_t 相关的链表结构,定义如下:

struct ngx_chain_s {
    ngx_buf_t    *buf;  /* 指向当前缓冲区 */
    ngx_chain_t  *next; /* 指向下一个chain,形成chain链表 */
};

chain链表

动态数组数据结构ngx_array_t

typedef struct {
    void        *elts;  /* 指向数组数据区域的首地址 */
    ngx_uint_t   nelts; /* 数组实际数据的个数 */
    size_t       size;  /* 单个元素所占据的字节大小 */
    ngx_uint_t   nalloc;/* 数组容量 */
    ngx_pool_t  *pool;  /* 数组对象所在的内存池 */
} ngx_array_t;

动态数组

整个动态数组的创建,使用,销毁过程中都是基于上文的内存池数据类型进行的。

设计优点

1,动态扩容

  • ngx_array_t 是动态分配内存的,它的大小可以根据需要自动增长,而普通的数组在定义时就需要确定大小,无法动态改变大小。
ngx_array_push(ngx_array_t *a)
{
    void        *elt, *new;
    size_t       size;
    ngx_pool_t  *p;
    
    //函数会检查数组是否已满(即已存储的元素个数 nelts 是否等于数组的容量 nalloc)。如果数组已满,则需要进行扩容操作:
    if (a->nelts == a->nalloc) {

        //计算当前数组的总大小(size = a->size * a->nalloc)
        size = a->size * a->nalloc;

        p = a->pool;
        
        //检查内存池是否有足够的空间用于扩容,如果内存池的末尾可以容纳新的数组分配,并且内存池的末尾加上新的数组分配大小不会超出内存池的结束位置,则在内存池的末尾直接分配新的数组空间。
        if ((u_char *) a->elts + size == p->d.last
            && p->d.last + a->size <= p->d.end)
        {
            p->d.last += a->size;
            a->nalloc++;

        } else {
            //如果内存池无法容纳新的数组分配,则需要重新分配一个新的数组空间,大小是当前数组大小的两倍,并将原数组数据复制到新数组中。
            new = ngx_palloc(p, 2 * size);
            if (new == NULL) {
                return NULL;
            }

            ngx_memcpy(new, a->elts, size);
            a->elts = new;
            a->nalloc *= 2;
        }
    }
    //最后,将新元素添加到数组末尾,并更新已存储的元素个数 nelts。
    elt = (u_char *) a->elts + a->size * a->nelts;
    a->nelts++;

    return elt;
}

2,内存管理

  • ngx_array_t 使用内存池进行内存分配,而不是直接调用系统的 malloc() 函数,这样可以减少内存碎片的产生,提高内存管理的效率。可以在内存池的生命周期内进行动态的内存分配和释放,而普通的数组需要手动管理内存的分配和释放。

3,数组元数据

  • ngx_array_t 中的 nelts 表示当前数组中实际存储的元素个数,nalloc 表示数组的容量(即分配的内存空间可以容纳的元素个数),通过这两个值可以确定数组的使用情况,从而进行动态的内存管理。
扩展视野

类似的自动扩容设计在redis上的动态字符串(sds)也有出现:

struct sdshdr {
    //记录buf数组中已使用字节的数量
    //等于SDS所保存字符串的长度
    unsigned int len;

    //记录buf数组中未使用字节的数量
    unsigned int free;

    //char数组,用于保存字符串
    char buf[];
};

sds

自动扩容:

sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
    void *sh, *newsh; // 分别为原始字符串头部和新字符串头部的指针
    size_t avail = sdsavail(s); // 可用空间大小
    size_t len, newlen, reqlen; // 分别为字符串当前长度、新长度、所需长度
    char type, oldtype = s[-1] & SDS_TYPE_MASK; // 分别为新类型和旧类型
    int hdrlen; // 头部长度
    size_t usable; // 实际可用空间大小

    /* Return ASAP if there is enough space left. */
    if (avail >= addlen) return s; // 如果可用空间大于等于所需空间,则直接返回原字符串

    len = sdslen(s); // 获取当前字符串长度
    sh = (char*)s-sdsHdrSize(oldtype); // 计算原字符串头部位置
    reqlen = newlen = (len+addlen); // 计算所需长度为当前长度加上新增长度
    assert(newlen > len);   /* Catch size_t overflow */ // 检查 size_t 溢出

    if (greedy == 1) {
        if (newlen < SDS_MAX_PREALLOC) // 如果新长度小于最大预分配长度,则将新长度翻倍
            newlen *= 2;
        else
            newlen += SDS_MAX_PREALLOC; // 否则直接增加最大预分配长度
    }

    type = sdsReqType(newlen); // 获取新类型

    /* Don't use type 5: the user is appending to the string and type 5 is
     * not able to remember empty space, so sdsMakeRoomFor() must be called
     * at every appending operation. */
    if (type == SDS_TYPE_5) type = SDS_TYPE_8; // 如果新类型为 5,则改为 8,因为 5 类型无法记录空闲空间

    hdrlen = sdsHdrSize(type); // 计算新头部长度
    assert(hdrlen + newlen + 1 > reqlen);  /* Catch size_t overflow */ // 检查 size_t 溢出

    if (oldtype==type) { // 如果新旧类型相同
        newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable); // 重新分配空间
        if (newsh == NULL) return NULL; // 分配失败则返回空指针
        s = (char*)newsh+hdrlen; // 更新字符串指针位置
    } else {
        /* Since the header size changes, need to move the string forward,
         * and can't use realloc */
        newsh = s_malloc_usable(hdrlen+newlen+1, &usable); // 分配新空间
        if (newsh == NULL) return NULL; // 分配失败则返回空指针
        memcpy((char*)newsh+hdrlen, s, len+1); // 将原字符串拷贝到新空间
        s_free(sh); // 释放原空间
        s = (char*)newsh+hdrlen; // 更新字符串指针位置
        s[-1] = type; // 更新新类型
        sdssetlen(s, len); // 更新字符串长度
    }
    usable = usable-hdrlen-1; // 计算实际可用空间
    if (usable > sdsTypeMaxSize(type)) // 如果实际可用空间大于类型最大限制,则设置为类型最大限制
        usable = sdsTypeMaxSize(type);
    sdssetalloc(s, usable); // 设置字符串实际分配空间大小
    return s; // 返回更新后的字符串
}

链表数据结构 ngx_list_t

// 定义一个结构体类型 ngx_list_part_s,并将其别名定义为 ngx_list_part_t。
typedef struct ngx_list_part_s  ngx_list_part_t;

// 定义结构体 ngx_list_part_s。
struct ngx_list_part_s {
    void             *elts;   // 指向元素的指针,每个元素的类型未指定。
    ngx_uint_t        nelts;  // 当前存储的元素个数。
    ngx_list_part_t  *next;   // 指向下一个链表部分的指针。
};

// 定义一个链表结构 ngx_list_t。
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;

链表数据结构如下图所示:

nginx链表

链表主要函数:

image-20240524181000980

需要注意的是:由于链表的内存分配是基于内存池,所有内存的销毁由内存池进行,即链表没有销毁操作。

其中主要是的内存分配函数就是*ngx_palloc(ngx_pool_t pool, size_t size)

基础巩固
什么是static?
1. static 变量
  • 在函数内

    • static 变量在函数内部声明时,它是一个局部变量,但与普通局部变量不同的是,它的生命周期是整个程序运行期间。它只在第一次执行到声明语句时初始化,以后即使函数多次调用也不会重新初始化,并且其值在函数调用之间保持不变。
    void func() {
        static int count = 0; // 只在第一次调用时初始化
        count++;
        printf("%d\n", count);
    }
    
  • 在文件作用域内

    • static 变量在文件的顶层(即不在任何函数内)声明时,它的作用域仅限于该文件。它不能被其他文件访问,即使在其他文件中有相同名字的变量也不会冲突。
    static int global_count = 0; // 只能在该文件中访问
    
2. static 函数
  • 在文件作用域内:

    • static 函数在文件中定义时,它的作用域也仅限于该文件。其他文件不能调用这个函数。
    static void helper_function() {
        // 只能在该文件中调用
    }
    
什么是inline?
1. inline 函数
  • 在函数声明前使用 inline

    • inline 关键字用于建议编译器在调用函数时将函数体内联展开,而不是进行正常的函数调用。这样可以减少函数调用的开销,尤其是在小函数和频繁调用的情况下。
    inline int add(int a, int b) {
        return a + b;
    }
    
    • 需要注意的是,inline 只是一个建议,编译器可能会忽略它,特别是当函数体太大或者包含复杂的逻辑时。
2. inline 和多重定义
  • 在头文件中定义 inline 函数:

    • inline 函数在头文件中定义并在多个源文件中包含时,通常不会导致重复定义错误,因为每个源文件的 inline 函数都被认为是独立的。编译器会处理这种情况以避免链接错误。
    // header.h
    inline int multiply(int a, int b) {
        return a * b;
    }
    

队列链表结构ngx_queue_t

队列链表

// 定义一个结构体类型 ngx_queue_s,并将其别名定义为 ngx_queue_t。
typedef struct ngx_queue_s  ngx_queue_t;

// 定义结构体 ngx_queue_s,用于实现双向循环链表。
struct ngx_queue_s {
    ngx_queue_t  *prev; // 指向前一个队列节点的指针
    ngx_queue_t  *next; // 指向下一个队列节点的指针
};

通过观察ngx_queue_s结构体可以看出与我们普通的双向链表的区别在于:

// 定义双向链表节点结构
typedef struct Node {
    int data;           // 数据域
    struct Node* prev;  // 指向前一个节点的指针
    struct Node* next;  // 指向下一个节点的指针
} Node;

1.这里的双向链表中的节点是没有数据区的,只有两个指向节点的指针

2.同样队列链表时选择不使用内存池管理

个人认为这样的设计主要从这几各方面考虑:

  1. 节省内存:在某些情况下,队列节点本身并不需要携带数据,而只需包含指向数据的指针。如果每个节点都分配一定大小的数据区域,将会造成内存浪费。通过不为节点分配数据区域,可以节省内存,并使得队列更加轻量级。
  2. 减少数据拷贝:通过在节点中存储指向数据的指针,而不是存储数据本身,可以减少数据在内存中的拷贝次数。这对于处理大量数据或频繁操作数据的场景下,能够提高性能和效率。
  3. 灵活性:将数据存储在节点外部,允许队列节点更通用。这样设计使得节点可以存储不同类型的数据,从而提高了队列的灵活性。这样的设计适用于 Nginx 中很多情况下需要处理各种类型数据的场景。
  4. 内存池适用于大量的、相对固定大小的对象分配,例如 HTTP 请求和响应的内存管理。但对于一些特殊的数据结构,如队列链表,其大小不固定,每个节点的内存消耗可能不同,因此直接使用内存池可能不够灵活。

当然,在使用时这种节点通常与实际数据结构结合使用,通过嵌入 ngx_queue_t 结构体来实现链表操作。例如,一个实际的数据结构可能如下:

typedef struct {
    ngx_queue_t queue; // 嵌入 ngx_queue_t 用于链表操作
    int data;          // 实际数据
} my_data_t;

哈希表结构ngx_hash_t

什么是哈希表?

哈希表(Hash Table)是一种用于实现关联数组(键值对存储)的数据结构。它利用哈希函数将键映射到表中的位置,从而实现快速的数据存取。哈希表支持高效的插入、删除和查找操作,通常时间复杂度为O(1)。

哈希表的基本概念

  1. 键(Key)和值(Value):哈希表通过键来存储和检索相应的值。
  2. 哈希函数(Hash Function):将键转换为哈希值(通常是一个整数),然后用这个哈希值确定在表中的位置。
  3. 哈希冲突(Hash Collision):不同的键可能会映射到同一个位置,这种情况称为冲突。哈希表需要处理冲突以确保每个键值对都能被正确存储和访问。

哈希表的结构

哈希表通常由一个数组和一个哈希函数组成。数组的每个位置称为一个桶(bucket)。

哈希冲突解决方法

  1. 链地址法(Chaining):每个桶中存储一个链表,所有映射到同一桶的元素都放入这个链表中。
  2. 开放地址法(Open Addressing):当冲突发生时,寻找下一个空桶来存储元素。常见的探查方式包括线性探查、二次探查和双重哈希。

hashtab

接着我们来看nginx是如何设计哈希表的。

img

可以看出nginx的哈希表是由三层结构组成而来

typedef struct {
    void             *value;     // 指向存储在哈希表中的值,可以是任何类型。
    u_short           len;       // name字段的长度。
    u_char            name[1];   // 键的第一个字符,后续的字符在分配时动态决定,这是一个灵活数组成员。
} ngx_hash_elt_t;

typedef struct {
    ngx_hash_elt_t  **buckets;   // 指向哈希表桶数组的指针,每个桶是一个链表的头节点。
    ngx_uint_t        size;      // 哈希表中桶的数量。
} ngx_hash_t;

typedef struct {
    ngx_hash_t        hash;      // 嵌入的ngx_hash_t结构,用于存储哈希表。
    void             *value;     // 指向存储在通配符哈希表中的值,可以是任何类型。
} ngx_hash_wildcard_t;

typedef struct {
    ngx_str_t         key;       // 键,字符串类型。
    ngx_uint_t        key_hash;  // 键的哈希值。
    void             *value;     // 与键关联的值,可以是任何类型。
} ngx_hash_key_t;

typedef ngx_uint_t (*ngx_hash_key_pt) (u_char *data, size_t len);  // 函数指针类型,指向一个哈希函数,该函数接受一个字符数组(键)和其长度作为参数,返回一个无符号整数类型的哈希值。

typedef struct {
    ngx_hash_t            hash;      // 嵌入的ngx_hash_t结构,用于存储哈希表。
    ngx_hash_wildcard_t  *wc_head;   // 指向通配符哈希表的头部。
    ngx_hash_wildcard_t  *wc_tail;   // 指向通配符哈希表的尾部。
} ngx_hash_combined_t;

typedef struct {
    ngx_hash_t       *hash;         // 指向一个ngx_hash_t结构的指针,用于存储初始化后的哈希表。
    ngx_hash_key_pt   key;          // 指向一个哈希函数的指针,用于计算键的哈希值。

    ngx_uint_t        max_size;     // 哈希表允许的最大键数量。
    ngx_uint_t        bucket_size;  // 每个桶的大小。

    char             *name;         // 哈希表的名称,用于调试或日志记录。
    ngx_pool_t       *pool;         // 指向一个内存池,用于分配哈希表所需的内存。
    ngx_pool_t       *temp_pool;    // 指向一个临时内存池,用于在哈希表初始化过程中分配临时内存。
} ngx_hash_init_t;

typedef struct {
    ngx_uint_t        hsize;              // 哈希表的大小,即桶的数量。

    ngx_pool_t       *pool;               // 内存池,用于分配哈希表所需的内存。
    ngx_pool_t       *temp_pool;          // 临时内存池,用于在哈希表初始化过程中分配临时内存。

    ngx_array_t       keys;               // 存储普通键的数组。
    ngx_array_t      *keys_hash;          // 指向哈希表中的普通键数组。

    ngx_array_t       dns_wc_head;        // 存储DNS通配符键(头部)的数组。
    ngx_array_t      *dns_wc_head_hash;   // 指向哈希表中的DNS通配符键(头部)数组。

    ngx_array_t       dns_wc_tail;        // 存储DNS通配符键(尾部)的数组。
    ngx_array_t      *dns_wc_tail_hash;   // 指向哈希表中的DNS通配符键(尾部)数组。
} ngx_hash_keys_arrays_t;

typedef struct ngx_table_elt_s  ngx_table_elt_t;  // 前向声明结构类型ngx_table_elt_t。

struct ngx_table_elt_s {
    ngx_uint_t        hash;           // 键的哈希值。
    ngx_str_t         key;            // 键,字符串类型。
    ngx_str_t         value;          // 与键关联的值,字符串类型。
    u_char           *lowcase_key;    // 存储小写形式的键,用于不区分大小写的查找。
    ngx_table_elt_t  *next;           // 指向下一个元素的指针,形成链表,用于处理哈希冲突。
};

注意:Nginx 的哈希表设计主要是为了高效地存储和查找键值对。

初始化操作:

hash 初始化由 ngx_hash_init() 函数完成,其 names 参数是 ngx_hash_key_t 结构的数组,即键-值对 <key,value> 数组,nelts 表示该数组元素的个数。该函数初始化的结果就是将 names 数组保存的键-值对<key,value>,通过 hash 的方式将其存入相应的一个或多个 hash 桶(即代码中的 buckets )中。hash 桶里面存放的是 ngx_hash_elt_t 结构的指针(hash元素指针),该指针指向一个基本连续的数据区。该数据区中存放的是经 hash 之后的键-值对<key’,value’>,即 ngx_hash_elt_t 结构中的字段 <name,value>。每一个这样的数据区存放的键-值对<key’,value’>可以是一个或多个。

#define NGX_HASH_ELT_SIZE(name)                                               \
    (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))

/* 初始化hash结构函数 */
/* 参数hinit是hash表初始化结构指针;
 * name是指向待添加在hash表结构的元素数组;
 * nelts是待添加元素数组中元素的个数;
 */
ngx_int_t
ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)
{
    u_char          *elts;
    size_t           len;
    u_short         *test;
    ngx_uint_t       i, n, key, size, start, bucket_size;
    ngx_hash_elt_t  *elt, **buckets;

    for (n = 0; n < nelts; n++) {
        /* 若每个桶bucket的内存空间不足以存储一个关键字元素,则出错返回
         * 这里考虑到了每个bucket桶最后的null指针所需的空间,即该语句中的sizeof(void *),
         * 该指针可作为查找过程中的结束标记
         */
        if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *))
        {
            ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
                          "could not build the %s, you should "
                          "increase %s_bucket_size: %i",
                          hinit->name, hinit->name, hinit->bucket_size);
            return NGX_ERROR;
        }
    }

    /* 临时分配sizeof(u_short)*max_size的test空间,即test数组总共有max_size个元素,即最大bucket的数量,
     * 每个元素会累计落到相应hash表位置的关键字长度,
     * 当大于256字节,即u_short所表示的字节大小,
     * 则表示bucket较少
     */
    test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);
    if (test == NULL) {
        return NGX_ERROR;
    }

    /* 每个bucket桶实际容纳的数据大小,
     * 由于每个bucket的末尾结束标志是null,
     * 所以bucket实际容纳的数据大小必须减去一个指针所占的内存大小
     */
    bucket_size = hinit->bucket_size - sizeof(void *);

    /* 估计hash表最少bucket数量;
     * 每个关键字元素需要的内存空间是 NGX_HASH_ELT_SIZE(&name[n]),至少需要占用两个指针的大小即2*sizeof(void *)
     * 这样来估计hash表所需的最小bucket数量
     * 因为关键字元素内存越小,则每个bucket所容纳的关键字元素就越多
     * 那么hash表的bucket所需的数量就越少,但至少需要一个bucket
     */
    start = nelts / (bucket_size / (2 * sizeof(void *)));
    start = start ? start : 1;

    
    if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) {
        start = hinit->max_size - 1000;
    }

    /* 以前面估算的最小bucket数量start,通过测试数组test估算hash表容纳 nelts个关键字元素所需的bucket数量
     * 根据需求适当扩充bucket的数量
     */
    for (size = start; size <= hinit->max_size; size++) {

        ngx_memzero(test, size * sizeof(u_short));

        for (n = 0; n < nelts; n++) {
            if (names[n].key.data == NULL) {
                continue;
            }

            /* 根据关键字元素的hash值计算存在到测试数组test对应的位置中,即计算bucket在hash表中的编号key,key取值为0~size-1 */
            key = names[n].key_hash % size;
            test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));

#if 0
            ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                          "%ui: %ui %ui \"%V\"",
                          size, key, test[key], &names[n].key);
#endif

            /* test数组中对应的内存大于每个桶bucket最大内存,则需扩充bucket的数量
             * 即在start的基础上继续增加size的值
             */
            if (test[key] > (u_short) bucket_size) {
                goto next;
            }
        }

        /* 若size个bucket桶可以容纳name数组的所有关键字元素,则表示找到合适的bucket数量大小即为size */
        goto found;

    next:

        continue;
    }

    ngx_log_error(NGX_LOG_WARN, hinit->pool->log, 0,
                  "could not build optimal %s, you should increase "
                  "either %s_max_size: %i or %s_bucket_size: %i; "
                  "ignoring %s_bucket_size",
                  hinit->name, hinit->name, hinit->max_size,
                  hinit->name, hinit->bucket_size, hinit->name);

found:

    /* 到此已经找到合适的bucket数量,即为size
     * 重新初始化test数组元素,初始值为一个指针大小
     */
    for (i = 0; i < size; i++) {
        test[i] = sizeof(void *);
    }

    /* 计算每个bucket中关键字所占的空间,即每个bucket实际所容纳数据的大小,
     * 必须注意的是:test[i]中还有一个指针大小
     */
    for (n = 0; n < nelts; n++) {
        if (names[n].key.data == NULL) {
            continue;
        }

        /* 根据hash值计算出关键字放在对应的test[key]中,即test[key]的大小增加一个关键字元素的大小 */
        key = names[n].key_hash % size;
        test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
    }

    len = 0;

    /* 调整成对齐到cacheline的大小,并记录所有元素的总长度 */
    for (i = 0; i < size; i++) {
        if (test[i] == sizeof(void *)) {
            continue;
        }

        test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));

        len += test[i];
    }

    /*
     * 向内存池申请bucket元素所占的内存空间,
     * 注意:若前面没有申请hash表头结构,则在这里将和ngx_hash_wildcard_t一起申请
     */
    if (hinit->hash == NULL) {
        hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t)
                                             + size * sizeof(ngx_hash_elt_t *));
        if (hinit->hash == NULL) {
            ngx_free(test);
            return NGX_ERROR;
        }

        /* 计算buckets的起始位置 */
        buckets = (ngx_hash_elt_t **)
                      ((u_char *) hinit->hash + sizeof(ngx_hash_wildcard_t));

    } else {
        buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *));
        if (buckets == NULL) {
            ngx_free(test);
            return NGX_ERROR;
        }
    }

    /* 分配elts,对齐到cacheline大小 */
    elts = ngx_palloc(hinit->pool, len + ngx_cacheline_size);
    if (elts == NULL) {
        ngx_free(test);
        return NGX_ERROR;
    }

    elts = ngx_align_ptr(elts, ngx_cacheline_size);

    /* 将buckets数组与相应的elts对应起来,即设置每个bucket对应实际数据的地址 */
    for (i = 0; i < size; i++) {
        if (test[i] == sizeof(void *)) {
            continue;
        }

        buckets[i] = (ngx_hash_elt_t *) elts;
        elts += test[i];

    }

    /* 清空test数组,以便用来累计实际数据的长度,这里不计算结尾指针的长度 */
    for (i = 0; i < size; i++) {
        test[i] = 0;
    }

    /* 依次向各个bucket中填充实际数据 */
    for (n = 0; n < nelts; n++) {
        if (names[n].key.data == NULL) {
            continue;
        }

        key = names[n].key_hash % size;
        elt = (ngx_hash_elt_t *) ((u_char *) buckets[key] + test[key]);

        elt->value = names[n].value;
        elt->len = (u_short) names[n].key.len;

        ngx_strlow(elt->name, names[n].key.data, names[n].key.len);

        /* test[key]记录当前bucket内容的填充位置,即下一次填充的起始位置 */
        test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
    }

    /* 设置bucket结束位置的null指针 */
    for (i = 0; i < size; i++) {
        if (buckets[i] == NULL) {
            continue;
        }

        elt = (ngx_hash_elt_t *) ((u_char *) buckets[i] + test[i]);

        elt->value = NULL;
    }

    ngx_free(test);

    hinit->hash->buckets = buckets;
    hinit->hash->size = size;

#if 0

    for (i = 0; i < size; i++) {
        ngx_str_t   val;
        ngx_uint_t  key;

        elt = buckets[i];

        if (elt == NULL) {
            ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                          "%ui: NULL", i);
            continue;
        }

        while (elt->value) {
            val.len = elt->len;
            val.data = &elt->name[0];

            key = hinit->key(val.data, val.len);

            ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                          "%ui: %p \"%V\" %ui", i, elt, &val, key);

            elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,
                                                   sizeof(void *));
        }
    }

#endif

    return NGX_OK;
}
查找操作---->ngx_hash_find
image-20240525123018812

处理链表:在哈希表中,哈希桶可能是通过链表实现的。在遍历链表时,需要确保从正确的位置开始访问下一个元素。由于每个元素的长度不固定(因为键的长度不同),所以不能简单地使用固定的偏移量来访问下一个元素,而需要进行对齐操作以确保指针的正确位置。

其中注意:

#define ngx_align_ptr(p, a)                                                   \
    (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))

宏的实现逻辑如下:

将指针 p 转换为 uintptr_t 类型,以获取指针的无符号整数表示。
将对齐方式 a 减 1,得到比对齐方式小一个单位的值。
将指针加上 a - 1 的值,相当于向上取整到最近的对齐倍数。
使用按位与操作符 & 将结果与 ~(a - 1) 进行按位与运算,将低位的偏移部分清零,从而实现对齐到指定的对齐方式。
最后将结果转换回 u_char * 类型,并返回对齐后的指针。

红黑树结构 ngx_rbtree_t

附上红黑树的四条规则:

  • 必须为二叉搜索树(左<根<右)
  • 根和叶子结点都是黑色
  • 不存在连续的两个红色结点
  • 任意到根节点路径上的黑色结点数目相同

红黑树删除结点流程:

image-20240525151542705

红黑树结构
typedef struct ngx_rbtree_node_s  ngx_rbtree_node_t;

struct ngx_rbtree_node_s {
    ngx_rbtree_key_t       key;     /* 节点的键值 */
    ngx_rbtree_node_t     *left;    /* 节点的左孩子 */
    ngx_rbtree_node_t     *right;   /* 节点的右孩子 */
    ngx_rbtree_node_t     *parent;  /* 节点的父亲 */
    u_char                 color;   /* 节点的颜色 */
    u_char                 data;    /* */
};

typedef struct ngx_rbtree_s  ngx_rbtree_t;

typedef void (*ngx_rbtree_insert_pt) (ngx_rbtree_node_t *root,
    ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);

/* 红黑树结构 */
struct ngx_rbtree_s {
    ngx_rbtree_node_t     *root;    /* 指向树的根节点 */
    ngx_rbtree_node_t     *sentinel;/* 指向树的叶子节点NIL */
    ngx_rbtree_insert_pt   insert;  /* 添加元素节点的函数指针,解决具有相同键值,但不同颜色节点的冲突问题;
                                     * 该函数指针决定新节点的行为是新增还是替换原始某个节点*/
};

红黑树的操作
初始化操作
/* 给节点着色,1表示红色,0表示黑色  */
#define ngx_rbt_red(node)               ((node)->color = 1)
#define ngx_rbt_black(node)             ((node)->color = 0)
/* 判断节点的颜色 */
#define ngx_rbt_is_red(node)            ((node)->color)
#define ngx_rbt_is_black(node)          (!ngx_rbt_is_red(node))
/* 复制某个节点的颜色 */
#define ngx_rbt_copy_color(n1, n2)      (n1->color = n2->color)

/* 节点着黑色的宏定义 */
/* a sentinel must be black */

#define ngx_rbtree_sentinel_init(node)  ngx_rbt_black(node)

/* 初始化红黑树,即为空的红黑树 */
/* tree 是指向红黑树的指针,
 * s 是红黑树的一个NIL节点,
 * i 表示函数指针,决定节点是新增还是替换
 */
#define ngx_rbtree_init(tree, s, i)                                           \
    ngx_rbtree_sentinel_init(s);                                              \
    (tree)->root = s;                                                         \
    (tree)->sentinel = s;                                                     \
    (tree)->insert = i

旋转操作

同平衡二叉树(AVL)的旋转逻辑相同

/* 左旋转操作 */
static ngx_inline void
ngx_rbtree_left_rotate(ngx_rbtree_node_t **root, ngx_rbtree_node_t *sentinel,
    ngx_rbtree_node_t *node)
{
    ngx_rbtree_node_t  *temp;

    temp = node->right;/* temp为node节点的右孩子 */
    node->right = temp->left;/* 设置node节点的右孩子为temp的左孩子 */

    if (temp->left != sentinel) {
        temp->left->parent = node;
    }

    temp->parent = node->parent;

    if (node == *root) {
        *root = temp;

    } else if (node == node->parent->left) {
        node->parent->left = temp;

    } else {
        node->parent->right = temp;
    }

    temp->left = node;
    node->parent = temp;
}

static ngx_inline void
ngx_rbtree_right_rotate(ngx_rbtree_node_t **root, ngx_rbtree_node_t *sentinel,
    ngx_rbtree_node_t *node)
{
    ngx_rbtree_node_t  *temp;

    temp = node->left;
    node->left = temp->right;

    if (temp->right != sentinel) {
        temp->right->parent = node;
    }

    temp->parent = node->parent;

    if (node == *root) {
        *root = temp;

    } else if (node == node->parent->right) {
        node->parent->right = temp;

    } else {
        node->parent->left = temp;
    }

    temp->right = node;
    node->parent = temp;
}
插入操作
/* 获取红黑树键值最小的节点 */
static ngx_inline ngx_rbtree_node_t *
ngx_rbtree_min(ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel)
{
    while (node->left != sentinel) {
        node = node->left;
    }

    return node;
}

/* 插入节点 */
/* 插入节点的步骤:
 * 1、首先按照二叉查找树的插入操作插入新节点;
 * 2、然后把新节点着色为红色(避免破坏红黑树性质5);
 * 3、为维持红黑树的性质,调整红黑树的节点(着色并旋转),使其满足红黑树的性质;
 */
void
ngx_rbtree_insert(ngx_thread_volatile ngx_rbtree_t *tree,
    ngx_rbtree_node_t *node)
{
    ngx_rbtree_node_t  **root, *temp, *sentinel;

    /* a binary tree insert */

    root = (ngx_rbtree_node_t **) &tree->root;
    sentinel = tree->sentinel;

    /* 若红黑树为空,则比较简单,把新节点作为根节点,
     * 并初始化该节点使其满足红黑树性质
     */
    if (*root == sentinel) {
        node->parent = NULL;
        node->left = sentinel;
        node->right = sentinel;
        ngx_rbt_black(node);
        *root = node;

        return;
    }

    /* 若红黑树不为空,则按照二叉查找树的插入操作进行
     * 该操作由函数指针提供
     */
    tree->insert(*root, node, sentinel);

    /* re-balance tree */

    /* 调整红黑树,使其满足性质,
     * 其实这里只是破坏了性质4:若一个节点是红色,则孩子节点都为黑色;
     * 若破坏了性质4,则新节点 node 及其父亲节点 node->parent 都为红色;
     */
    while (node != *root && ngx_rbt_is_red(node->parent)) {

        /* 若node的父亲节点是其祖父节点的左孩子 */
        if (node->parent == node->parent->parent->left) {
            temp = node->parent->parent->right;/* temp节点为node的叔叔节点 */

            /* case1:node的叔叔节点是红色 */
            /* 此时,node的父亲及叔叔节点都为红色;
             * 解决办法:将node的父亲及叔叔节点着色为黑色,将node祖父节点着色为红色;
             * 然后沿着祖父节点向上判断是否会破会红黑树的性质;
             */
            if (ngx_rbt_is_red(temp)) {
                ngx_rbt_black(node->parent);
                ngx_rbt_black(temp);
                ngx_rbt_red(node->parent->parent);
                node = node->parent->parent;

            } else {
                /* case2:node的叔叔节点是黑色且node是父亲节点的右孩子 */
                /* 则此时,以node父亲节点进行左旋转,使case2转变为case3;
                 */
                if (node == node->parent->right) {
                    node = node->parent;
                    ngx_rbtree_left_rotate(root, sentinel, node);
                }

                /* case3:node的叔叔节点是黑色且node是父亲节点的左孩子 */
                /* 首先,将node的父亲节点着色为黑色,祖父节点着色为红色;
                 * 然后以祖父节点进行一次右旋转;
                 */
                ngx_rbt_black(node->parent);
                ngx_rbt_red(node->parent->parent);
                ngx_rbtree_right_rotate(root, sentinel, node->parent->parent);
            }

        } else {/* 若node的父亲节点是其祖父节点的右孩子 */
            /* 这里跟上面的情况是对称的,就不再进行讲解了
             */
            temp = node->parent->parent->left;

            if (ngx_rbt_is_red(temp)) {
                ngx_rbt_black(node->parent);
                ngx_rbt_black(temp);
                ngx_rbt_red(node->parent->parent);
                node = node->parent->parent;

            } else {
                if (node == node->parent->left) {
                    node = node->parent;
                    ngx_rbtree_right_rotate(root, sentinel, node);
                }

                ngx_rbt_black(node->parent);
                ngx_rbt_red(node->parent->parent);
                ngx_rbtree_left_rotate(root, sentinel, node->parent->parent);
            }
        }
    }

    /* 根节点必须为黑色 */
    ngx_rbt_black(*root);
}

/* 这里只是将节点插入到红黑树中,并没有判断是否满足红黑树的性质;
 * 类似于二叉查找树的插入操作,这个函数为红黑树插入操作的函数指针;
 */
void
ngx_rbtree_insert_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node,
    ngx_rbtree_node_t *sentinel)
{
    ngx_rbtree_node_t  **p;

    for ( ;; ) {

        /* 判断node节点键值与temp节点键值的大小,以决定node插入到temp节点的左子树还是右子树 */
        p = (node->key < temp->key) ? &temp->left : &temp->right;

        if (*p == sentinel) {
            break;
        }

        temp = *p;
    }

    /* 初始化node节点,并着色为红色 */
    *p = node;
    node->parent = temp;
    node->left = sentinel;
    node->right = sentinel;
    ngx_rbt_red(node);
}

void
ngx_rbtree_insert_timer_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node,
    ngx_rbtree_node_t *sentinel)
{
    ngx_rbtree_node_t  **p;

    for ( ;; ) {

        /*
         * Timer values
         * 1) are spread in small range, usually several minutes,
         * 2) and overflow each 49 days, if milliseconds are stored in 32 bits.
         * The comparison takes into account that overflow.
         */

        /*  node->key < temp->key */

        p = ((ngx_rbtree_key_int_t) (node->key - temp->key) < 0)
            ? &temp->left : &temp->right;

        if (*p == sentinel) {
            break;
        }

        temp = *p;
    }

    *p = node;
    node->parent = temp;
    node->left = sentinel;
    node->right = sentinel;
    ngx_rbt_red(node);
}

删除操作
/* 删除节点 */
void
ngx_rbtree_delete(ngx_thread_volatile ngx_rbtree_t *tree,
    ngx_rbtree_node_t *node)
{
    ngx_uint_t           red;
    ngx_rbtree_node_t  **root, *sentinel, *subst, *temp, *w;

    /* a binary tree delete */

    root = (ngx_rbtree_node_t **) &tree->root;
    sentinel = tree->sentinel;

    /* 下面是获取temp节点值,temp保存的节点是准备替换节点node ;
     * subst是保存要被替换的节点的后继节点;
     */

    /* case1:若node节点没有左孩子(这里包含了存在或不存在右孩子的情况)*/
    if (node->left == sentinel) {
        temp = node->right;
        subst = node;

    } else if (node->right == sentinel) {/* case2:node节点存在左孩子,但是不存在右孩子 */
        temp = node->left;
        subst = node;

    } else {/* case3:node节点既有左孩子,又有右孩子 */
        subst = ngx_rbtree_min(node->right, sentinel);/* 获取node节点的后续节点 */

        if (subst->left != sentinel) {
            temp = subst->left;
        } else {
            temp = subst->right;
        }
    }

    /* 若被替换的节点subst是根节点,则temp直接替换subst称为根节点 */
    if (subst == *root) {
        *root = temp;
        ngx_rbt_black(temp);

        /* DEBUG stuff */
        node->left = NULL;
        node->right = NULL;
        node->parent = NULL;
        node->key = 0;

        return;
    }

    /* red记录subst节点的颜色 */
    red = ngx_rbt_is_red(subst);

    /* temp节点替换subst 节点 */
    if (subst == subst->parent->left) {
        subst->parent->left = temp;

    } else {
        subst->parent->right = temp;
    }

    /* 根据subst是否为node节点进行处理 */
    if (subst == node) {

        temp->parent = subst->parent;

    } else {

        if (subst->parent == node) {
            temp->parent = subst;

        } else {
            temp->parent = subst->parent;
        }

        /* 复制node节点属性 */
        subst->left = node->left;
        subst->right = node->right;
        subst->parent = node->parent;
        ngx_rbt_copy_color(subst, node);

        if (node == *root) {
            *root = subst;

        } else {
            if (node == node->parent->left) {
                node->parent->left = subst;
            } else {
                node->parent->right = subst;
            }
        }

        if (subst->left != sentinel) {
            subst->left->parent = subst;
        }

        if (subst->right != sentinel) {
            subst->right->parent = subst;
        }
    }

    /* DEBUG stuff */
    node->left = NULL;
    node->right = NULL;
    node->parent = NULL;
    node->key = 0;

    if (red) {
        return;
    }

    /* 下面开始是调整红黑树的性质 */
    /* a delete fixup */

    /* 根据temp节点进行处理 ,若temp不是根节点且为黑色 */
    while (temp != *root && ngx_rbt_is_black(temp)) {

        /* 若temp是其父亲节点的左孩子 */
        if (temp == temp->parent->left) {
            w = temp->parent->right;/* w为temp的兄弟节点 */

            /* case A:temp兄弟节点为红色 */
            /* 解决办法:
             * 1、改变w节点及temp父亲节点的颜色;
             * 2、对temp父亲节的做一次左旋转,此时,temp的兄弟节点是旋转之前w的某个子节点,该子节点颜色为黑色;
             * 3、此时,case A已经转换为case B、case C 或 case D;
             */
            if (ngx_rbt_is_red(w)) {
                ngx_rbt_black(w);
                ngx_rbt_red(temp->parent);
                ngx_rbtree_left_rotate(root, sentinel, temp->parent);
                w = temp->parent->right;
            }

            /* case B:temp的兄弟节点w是黑色,且w的两个子节点都是黑色 */
            /* 解决办法:
             * 1、改变w节点的颜色;
             * 2、把temp的父亲节点作为新的temp节点;
             */
            if (ngx_rbt_is_black(w->left) && ngx_rbt_is_black(w->right)) {
                ngx_rbt_red(w);
                temp = temp->parent;

            } else {/* case C:temp的兄弟节点是黑色,且w的左孩子是红色,右孩子是黑色 */
                /* 解决办法:
                 * 1、将改变w及其左孩子的颜色;
                 * 2、对w节点进行一次右旋转;
                 * 3、此时,temp新的兄弟节点w有着一个红色右孩子的黑色节点,转为case D;
                 */
                if (ngx_rbt_is_black(w->right)) {
                    ngx_rbt_black(w->left);
                    ngx_rbt_red(w);
                    ngx_rbtree_right_rotate(root, sentinel, w);
                    w = temp->parent->right;
                }

                /* case D:temp的兄弟节点w为黑色,且w的右孩子为红色 */
                /* 解决办法:
                 * 1、将w节点设置为temp父亲节点的颜色,temp父亲节点设置为黑色;
                 * 2、w的右孩子设置为黑色;
                 * 3、对temp的父亲节点做一次左旋转;
                 * 4、最后把根节点root设置为temp节点;*/
                ngx_rbt_copy_color(w, temp->parent);
                ngx_rbt_black(temp->parent);
                ngx_rbt_black(w->right);
                ngx_rbtree_left_rotate(root, sentinel, temp->parent);
                temp = *root;
            }

        } else {/* 这里针对的是temp节点为其父亲节点的左孩子的情况 */
            w = temp->parent->left;

            if (ngx_rbt_is_red(w)) {
                ngx_rbt_black(w);
                ngx_rbt_red(temp->parent);
                ngx_rbtree_right_rotate(root, sentinel, temp->parent);
                w = temp->parent->left;
            }

            if (ngx_rbt_is_black(w->left) && ngx_rbt_is_black(w->right)) {
                ngx_rbt_red(w);
                temp = temp->parent;

            } else {
                if (ngx_rbt_is_black(w->left)) {
                    ngx_rbt_black(w->right);
                    ngx_rbt_red(w);
                    ngx_rbtree_left_rotate(root, sentinel, w);
                    w = temp->parent->left;
                }

                ngx_rbt_copy_color(w, temp->parent);
                ngx_rbt_black(temp->parent);
                ngx_rbt_black(w->left);
                ngx_rbtree_right_rotate(root, sentinel, temp->parent);
                temp = *root;
            }
        }
    }

    ngx_rbt_black(temp);
}
          * 1、将w节点设置为temp父亲节点的颜色,temp父亲节点设置为黑色;
             * 2、w的右孩子设置为黑色;
             * 3、对temp的父亲节点做一次左旋转;
             * 4、最后把根节点root设置为temp节点;*/
            ngx_rbt_copy_color(w, temp->parent);
            ngx_rbt_black(temp->parent);
            ngx_rbt_black(w->right);
            ngx_rbtree_left_rotate(root, sentinel, temp->parent);
            temp = *root;
        }

    } else {/* 这里针对的是temp节点为其父亲节点的左孩子的情况 */
        w = temp->parent->left;

        if (ngx_rbt_is_red(w)) {
            ngx_rbt_black(w);
            ngx_rbt_red(temp->parent);
            ngx_rbtree_right_rotate(root, sentinel, temp->parent);
            w = temp->parent->left;
        }

        if (ngx_rbt_is_black(w->left) && ngx_rbt_is_black(w->right)) {
            ngx_rbt_red(w);
            temp = temp->parent;

        } else {
            if (ngx_rbt_is_black(w->left)) {
                ngx_rbt_black(w->right);
                ngx_rbt_red(w);
                ngx_rbtree_left_rotate(root, sentinel, w);
                w = temp->parent->left;
            }

            ngx_rbt_copy_color(w, temp->parent);
            ngx_rbt_black(temp->parent);
            ngx_rbt_black(w->left);
            ngx_rbtree_right_rotate(root, sentinel, temp->parent);
            temp = *root;
        }
    }
}

ngx_rbt_black(temp);

}


  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值