1. 程式人生 > >Nginx學習筆記 —— 高階資料結構

Nginx學習筆記 —— 高階資料結構

動態陣列

ngx_array_t 表示一塊連續的記憶體,其中存放著陣列元素,概念上和原始陣列很接近

// 定義在 core/ngx_array.h
typedef struct
{
    void *          elts;       // 陣列的記憶體位置,即陣列首地址
    ngx_uint_t      nelts;      // 陣列當前的元素數量
    size_t          size;       // 陣列元素的大小
    ngx_uint_t      nalloc;     // 陣列可容納的最多元素容量
    ngx_pool_t *    pool;       // 陣列可使用的記憶體池
}ngx_array_t;

elts 就是原始的陣列,定義成 void*,使用時應強轉成相應的型別
nelts 相當於 vector.size();
size 相當於 sizeof(T);
nalloc 相當於 vector.capacity();
pool 陣列使用的記憶體池,相當於 vector 的 allocator

數組裡的元素不斷增加,當 nelts > nalloc 時將引起陣列擴容,ngx_array_t 會向記憶體池 pool 申請一塊兩倍原大小的空間————這個策略和 std::vector 是一樣的

但 ngx_array_t 擴容成本太高,它需要重新分配記憶體並且將資料拷貝,所以最好一次性分配足夠的空間,避免動態擴容

操作函式
使用 ngx_array_t.elts 就可以訪問數組裡的元素,不過需要轉換為實際元素型別

auto p = reinterpret_cast<T*>(arr.elts);
cout<<p[0]<<endl;

ngx_array_t 沒有越界檢查,需要自行確保陣列索引的有效性

// 使用記憶體池 p 建立一個可容納 n 個大小為 size 元素的陣列,即分配了一塊 n*size 大小的記憶體塊
ngx_array_t * ngx_array_create(ngx_pool_t * p, ngx_uint_t n, size_t size)
; // 銷燬動態陣列,歸還分配的記憶體 void ngx_array_destory(ngx_array_t * a); // 向陣列新增元素,它們返回一個 void* 指標(可新增元素的位置),需要轉換型別才能再操作 // 不直接使用 elts 操作的原因是防止越界,函式內部會檢查當前陣列容量自動擴容 void * ngx_array_push(ngx_array_t * a); void * ngx_array_push_n(ngx_array_t * a, ngx_uint_t n);

清空陣列可以直接置 nelts 為 0, 但之前分配的記憶體並不會釋放,還可以用來儲存資料

單向連結串列

Nginx 的單向連結串列 ngx_list_t 融合了 ngx_array_t 的特點,在一個節點裡儲存多個元素,降低了連結串列的儲存成本

// 定義在 core/ngx_list.h
struct ngx_list_part_s
{
    void *              elts;       // 陣列元素指標
    ngx_uint_t          nelts;      // 數組裡的元素數量
    ngx_list_part_t *   next;       // 下個節點指標
};

ngx_list_t 定義了連結串列,實際上是 頭結點 + 元資訊:

// 定義在 core/ngx_list.h
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_array_t 陣列結構

// 使用記憶體池建立連結串列,每個節點可容納n個大小為size的元素
ngx_list_t * ngx_list_create(ngx_pool_t * pool, ngx_uint_t n, size_t size);

// 向連結串列裡新增元素,返回的指標需要轉型賦值
void * ngx_list_push(ngx_list_t * list);

eg.

part = &list.part;              // 獲取頭結點
data = part->elts;              // 獲取節點內陣列地址

for(i = 0; ; i++)               // 遍歷連結串列
{
    if(i >= part->nelts)        // 檢查陣列越界
    {
        if(part->next == NULL)  // 檢查是否到連結串列尾
        {
            break;
        }

        part = part->next;      // 跳至下一個節點
        data = part->data;      // 下一個節點的陣列地址
        i = 0;
    }

    ... data[i] ...             // 在本節點訪問元素
}

雙端佇列

在Nginx 裡它被實現為雙向迴圈連結串列 ngx_queue_t,是侵入式容器

// 定義在 core/ngx_queue.h
struct ngx_queue_s
{
    ngx_queue_t *   prev;       // 前驅指標
    ngx_queue_t *   next;       // 後繼指標
};

結構體需要新增它作為成員,為資料結構增加了雙向連結串列的指標

struct X                        // 一個可放入佇列的資料結構
{
    int x = 0;                  // 攜帶的資料
    ngx_queue_t queue;          // ngx_queue_t 成員,名字任意
};

結構體內可以有不止一個 ngx_queue_t 成員,這意味著它可以同時屬於多個不同的雙向連結串列

ngx_queue_t 使用一個頭結點來表示佇列,這個頭節點可以是單純的 ngx_queue_t 結構

// 函式巨集 ngx_queue_init() 初始化頭結點,把兩個指標指向自身
#define ngx_queue_init(q)   \
    (q)->prev = q;          \
    (q)->next = q

// 函式巨集 ngx_queue_sentinel() 返回節點自身,對於頭結點就相當於哨兵
#define ngx_queue_sentinel(h) (h)

// ngx_queue_empty() 檢查頭結點的前驅指標,判斷是否為空佇列
#define ngx_queue_empty(h)  \
    (h == (h)->prev)

// 函式巨集 ngx_queue_insert_head()ngx_queue_insert_tail() 用來向頭尾插入資料節點
#define ngx_queue_insert_head(h, x)
#define ngx_queue_insert_tail(h, x)

// 函式巨集 ngx_queue_head()ngx_queue_last() 獲取佇列的頭尾指標
// 可以用來實現佇列正向或反向遍歷,直到遇到頭結點 ngx_queue_sentinel()
#define ngx_queue_head(h) (h)->next
#define ngx_queue_last(h) (h)->prev

// 函式 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_insert_head
#define ngx_queue_insert_after  ngx_queue_insert_head

// 刪除節點,實際上只是調整了節點的指標,把節點從佇列中摘除
#define ngx_queue_remove(x)     \
    (x)->next->prev = (x)->prev;\
    (x)->prev->next = (x)->next;

// 獲取節點資料
#define ngx_queue_data(q, type, link)   \
    (type *)((u_char *) q - offsetof(type, link))   // 返回結構體指標(offsetof是一個巨集,計算結構裡成員的偏移量)

可以把雙端佇列分解為 節點、迭代器和佇列容器三個概念:
節點儲存資料,迭代器遍歷資料,而佇列容器則是頭節點。
這三個概念可以使用C++封裝成不同的類,達到解耦的目的

紅黑樹

在Nginx裡紅黑樹主要用在事件機制裡的定時器,檢查連線超時,此外還在 reslover、cache裡用於快速查詢

// 定義在 core/ngx_rbtree.h
typedef ngx_uint_t      ngx_rbtree_key_t;
typedef ngx_int_t       ngx_rbtree_key_int_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;      // 1 紅色 / 0 黑色
    u_char                  data;       // 節點資料,只有一位元組,通常無意義
};

與 ngx_queue_t 一樣,ngx_rbtree_node_t 也要作為結構體的一個成員,以侵入方式來使用
例如儲存字串的紅黑樹節點:

typedef struct
{
    ngx_rbtree_node_t   node;   // 紅黑樹節點,不必是第一個成員
    ngx_str_t           str;    // 節點的其他資訊
} ngx_str_node_t;

// 節點的插入方法,函式指標型別
typedef void (*ngx_rbtree_insert_pt) (ngx_rbtree_node_t * root, ngx_rbtree_node_t, ngx_rbtree_sentinel);

struct ngx_rbtree_s
{
    ngx_rbtree_node_t *     root;           // 紅黑樹的根節點
    ngx_rbtree_node_t *     sentinel;       // 哨兵節點,相當於空指標、空物件
    ngx_rbtree_insert_pt    insert;         // 節點的插入方法
};

insert決定了紅黑樹的節點插入操作,使用者可以針對不同的節點型別實現不同的插入方法,但必須符合 ngx_rbtree_insert_pt 的定義

// 紅黑樹鍵值是標準的 uint/int
void ngx_rbtree_insert_value(root, node, sentinel);

// 定時器紅黑樹專用插入函式,鍵值是毫秒值
void ngx_rbtree_insert_timer_value(...);

// 字串紅黑樹專用插入函式,鍵值是字串的hash值
void ngx_str_rbtree_insert_value(...);

參考這三個函式可以實現自己的插入函式:

void ngx_rbtree_insert_value(...)       // 插入標準的紅黑樹,鍵值是整數
{
    ngx_rbtree_node_t ** p;             // 樹節點指標
    for(;;)
    {                                   // 比較當前節點與插入節點,選擇走左/右
        p = (node->key < temp->key) ? &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);
}
// 初始化巨集,初始化後紅黑樹中僅有一個哨兵節點 s ,同時也是根節點
#define ngx_rbtree_init(tree, s, i)     // tree 使用 s 作為哨兵節點,插入方法是 i

// 紅黑樹的插入
void ngx_rbtree_insert(ngx_rbtree * tree, ngx_rbtree_node_t * node);
// 紅黑樹的刪除
void ngx_rbtree_delete(ngx_rbtree * tree, ngx_rbtree_node_t * node);
  • 操作後若樹的平衡性被破壞會自動旋轉以保持平衡
// 查詢最小節點,順著指標找最左邊的節點
ngx_rbtree_node_t * ngx_rbtree_min()

ngx_rbtree_min() 只會返回 ngx_rbtree_node_t* 型別,若想得到完整的結構體指標,則需要利用巨集 offsetof 計算偏移量再強制型別轉換

// Nginx還提供查詢下一個節點的功能,利用它可以實現正序遍歷紅黑樹
ngx_rbtree_node_t * ngx_rbtree_next(ngx_rbtree_t * tree, ngx_rbtree_node_t * node);

// 對於常用的字串紅黑樹,Nginx提供了專用的查詢函式,它可以在樹裡找到任意字串,不存在則返回 nullptr
ngx_str_node_t * ngx_str_rbtree_lookup(*rbtree, *name, hash);

緩衝區

作為web伺服器,Nginx需要頻繁收發處理大量的資料,這些資料有時是連續的記憶體塊,有時是分散的記憶體塊,甚至有時資料過大,記憶體無法存放,只能儲存成磁碟檔案

ngx_str_t 結構可以表示記憶體塊,但不能應對複雜的情景,所以Nginx實現了 ngx_buf_t 和 ngx_chain_t

// ngx_buf_t 表示一個單塊的緩衝區,既可以是記憶體也可以是檔案
// 它的結構分為兩個部分:緩衝區資訊和標誌位資訊
typedef void *      ngx_buf_tag_t;

// 定義在 core/ngx_buf.h
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;        // void* 指標,可以是任意關聯物件
    ngx_file_t *    file;       // 儲存資料的檔案物件

    ...                         // 標誌位資訊
};

因為Nginx的緩衝資料可能在記憶體或者磁碟檔案中,所以 ngx_buf_t 使用 pos/last 和 file_pos/file_last 來指定資料在記憶體或檔案中的具體位置,究竟資料在哪裡則要靠後面的標誌位資訊來確定

start 和 end 兩個成員變數標記了資料所在記憶體塊的邊界,如果記憶體塊是可修改的,那麼在操作時必須防止越界

tag 通常指向的是使用該緩衝區的物件

// ngx_buf_t 的標誌位都是bool值,使用位域的方式節約記憶體

struct ngx_buf_s
{   ...                         // 緩衝區資訊

    unsigned    temporary:1;    // 記憶體塊臨時資料,可以修改
    unsigned    memory:1;       // 記憶體塊資料,不允許修改
    unsigned    mmap:1;         // 記憶體對映資料,不允許修改

    unsigned    in_file:1;      // 緩衝區在檔案裡
    unsigned    flush:1;        // 要求Nginx立即輸出本緩衝區
    unsigned    sync:1;         // 要求Nginx同步操作本緩衝區
    unsigned    last_buf:1;     // 最後一塊緩衝區
    unsigned    last_in_chain:1;// 鏈裡最後一塊緩衝區
    unsigned    temp_file:1;    // 緩衝區在臨時檔案裡
};

其中 last_buf 表示整個處理過程的最後一塊緩衝區,標誌著 TCP/HTTP 請求處理的結束;

而 last_in_chain 表示當前資料塊鏈(ngx_chain_t)裡的最後一塊,之後可能還有資料需要處理。

從 ngx_buf_t 的定義可以看到,一個有資料的緩衝區不是在記憶體裡就是在檔案裡,所以記憶體標誌位成員變數(temporary/memory/mmap)和檔案標誌成員變數(in_file/temp_file)不能全為0,否則Nginx 會認為這是個特殊(special)或無效的緩衝區。

如果緩衝區既不在記憶體也不在檔案裡,那麼它就不含有有效資料,只起到控制作用,例如重新整理(flush)或者同步(sync)

// 從記憶體池裡分配一塊 size 大小的緩衝區
ngx_buf_t * ngx_create_temp_buf(ngx_pool_t * pool, size_t size);

函式返回的 ngx_buf_t 結構內成員都已經初始化好了, pos 和 last 都指向記憶體塊的首位置,表示空緩衝區,而temporary 標誌位是1。

// 從記憶體池建立一個 ngx_buf_t 結構,然後手工指定它的成員,關聯到已經存在的記憶體
#define ngx_alloc_buf(pool)     ngx_palloc(pool, sizeof(ngx_buf_t))
#define ngx_calloc_buf(pool)    ngx_pcalloc(pool, sizeof(ngx_buf_t)

// 檢查緩衝區是否在記憶體裡
#define ngx_buf_in_memory(b)        (b->temporary || b->memory || b->mmap)
#define ngx_buf_in_memory_only(b)   (ngx_buf_in_memory(b) && !b->in_file)

// 判斷起控制作用的特殊緩衝區
#define ngx_buf_special(b)

// 計算緩衝區大小,根據是否在記憶體裡使用恰當的指標
#define ngx_buf_size(b)

// 拷貝記憶體,返回拷貝資料後的終點位置,在連續複製多段資料時很方便
// 定義在 core/ngx_string.h
#define ngx_cpymem(dst, src, n)     (((u_char*)memcpy(dst, src, n)) + (n))

// 設定記憶體
#define ngx_memzero(buf, n)         (void) memset(buf, 0, n)
#define ngx_memset(buf, c, n)       (void) memset(buf, c, n)

資料塊鏈

在處理HTTP/TCP請求時會經常建立多個緩衝區來存放資料,Nginx 把緩衝區塊簡單地組織為一個單向連結串列

ngx_chain_t 把多個分散的 ngx_buf_t 連線為一個順序的資料塊鏈:

// 定義在 core/ngx_buf.h
struct ngx_chain_s
{
    ngx_buf_t *     buf;        // 緩衝區指標
    ngx_chain_t *   next;       // 下一個連結串列節點
};

// 從記憶體池裡獲取 ngx_chain_t 物件
ngx_chain_t * ngx_alloc_chain_link(ngx_pool_t * pool);  // 內部呼叫 ngx_palloc(),獲得的物件buf/next可能是任意值

// 釋放 ngx_chain_t 物件
#define ngx_free_chain(pool, cl)

由於 ngx_chain_t 在 Nginx 裡應用的很頻繁,所以 Nginx 對此進行了優化。
在記憶體池裡儲存了一個空閒 ngx_chain_t 連結串列,分配時從這個連結串列中摘取,釋放時再掛上去

typedef struct
{
    ngx_int_t   num;        // 緩衝區的數量
    size_t      size;       // 緩衝區的大小
} ngx_bufs_t;               // 建立連結串列的引數結構

// 建立多個緩衝區,返回一個連結好的資料塊連結串列
ngx_chain_t  * ngx_create_chain_of_bufs(ngx_pool_t * pool, ngx_bufs_t * bufs);

仍然把 ngx_chain_t 分解為節點、迭代器和容器三個概念,不同C++類封裝不同的操作

鍵值對

鍵值對是一種對映關係,C++使用std::pair 來表示,並且使用 std::map / std::unordered_map 儲存這樣的資料;

而 Nginx 提供兩個結構:ngx_keyval_t 和 ngx_table_elt_t ,再結合 ngx_array_t 或 ngx_list_t 應用在不同的場合

ngx_keyval_t 是一個簡單的鍵值對結構,主要用在 Nginx 的配置解析環節,儲存配置檔案裡成對的配置。

// 定義在 core/ngx_string.h
typedef struct
{
    ngx_str_t   key;
    ngx_str_t   value;
} ngx_keyval_t;

在 Nginx 裡,通常使用 ngx_array_t 來儲存 ngx_keyval_t,相當於

typedef NgxArray<ngx_keyval_t>      NgxKvArray;

散列表鍵值對

// 定義在 core/ngx_hash.h
typedef struct
{
    ngx_uint_t      hash;           // 雜湊(雜湊)標記
    ngx_str_t       key;            // 鍵
    ngx_str_t       value;          // 值
    u_char *        lowcase_key;    // key 的小寫字串指標
} ngx_table_elt_t;

ngx_table_elt_t 主要用來表示HTTP頭部資訊,例如
“Server:nginx” 這樣的字串對應到 ngx_table_elt_t 就是 key = “Server”,value = “nginx”

成員 hash 是一個雜湊標記,Nginx使用它在散列表中快速查詢資料,
可以簡單地把它置為非零值(通常為1),也可以使用下面兩個函式計算雜湊值:

ngx_uint_t ngx_hash_key(u_char * data, size_t len);     // 計算雜湊值

ngx_uint_t ngx_hash_key_lc(u_char * data, size_t len);  // 小寫後再計算

成員 lowcase_key 指向一個全小寫的字串,在大小寫無關比較時可避免重複計算。

ngx_uint_t ngx_hash_strlow(u_char * dst, u_char * src, size_t n);   // 小寫化同時計算雜湊值

Nginx 在處理HTTP請求時使用 ngx_list_t 儲存了HTTP 頭部資訊,相當於:

typedef NgxList<ngx_table_elt_t>    NgxHeaderList;