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;