nginx 雙向鏈表

ngx_queue_t雙向鏈表是Nginx提供的輕量級鏈表容器,它與Nginx的內存池無關,因此,這個鏈表將不會負責分配內存來存放鏈表元素。這意味著,任何鏈表元素都需要通過其他方式來分配它所需要的內存空間,不要指望ngx_queue_t幫助存儲元素。ngx_queue_t只是把這些已經分配好內存的元素用雙向鏈表連接起來。ngx_queue_t的功能雖然很簡單,但它非常輕量級,對每個用戶數據而言,只需要增加兩個指針的空間即可,消耗的內存很少。同時,ngx_queue_t還提供了一個非常簡易的插入排序法,雖然不太適合超大規模數據的排序,但它勝在簡單實用。ngx_queue_t作為C語言提供的通用雙向鏈表,其設計思路值得用戶參考。

為什麼設計 ngx_queue_t 雙向鏈表

鏈表作為順序容器的優勢在於,它可以高效地執行插入、刪除、合併等操作,在移動鏈

表中的元素時只需要修改指針的指向,因此,它很適合頻繁修改容器的場合。在Nginx中,鏈表是必不可少的,而ngx_queue_t雙向鏈表就被設計用於達成以上目的。

相對於Nginx其他順序容器,ngx_queue_t容器的優勢在於:

  • 實現了排序功能。
  • 它非常輕量級,是一個純粹的雙向鏈表。它不負責鏈表元素所佔內存的分配,與Nginx封裝的ngx_pool_t內存池完全無關。
  • 支持兩個鏈表間的合併。

ngx_queue_t容器的實現只用了一個數據結構ngx_queue_t,它僅有兩個成員:prev、next,如下所示:

typedef struct ngx_queue_s ngx_queue_t;

struct ngx_queue_s {
ngx_queue_t *prev;
ngx_queue_t *next;
};

因此,對於鏈表中的每個元素來說,空間上只會增加兩個指針的內存消耗。

使用ngx_queue_t時可能會遇到有些讓人費解的情況,因為鏈表容器自身是使用ngx_queue_t來標識的,而鏈表中的每個元素同樣使用ngx_queue_t結構來標識自己,並以ngx_queue_t結構維持其與相鄰元素的關係。下面開始介紹ngx_queue_t的使用方法。

雙向鏈表的使用方法

Nginx在設計這個雙向鏈表時,由於容器與元素共用了ngx_queue_t結構體,為了避免ngx_queue_t結構體成員的意義混亂,Nginx封裝了鏈表容器與元素的所有方法,這種情況非常少見,而且從接下來的幾節中可以看到,其他容器都需要直接使用成員變數來訪問,唯有ngx_queue_t雙向鏈表只能使用如下中列出的方法訪問容器。

#define ngx_queue_init(q)
(q)->prev = q;
(q)->next = q

#define ngx_queue_empty(h)
(h == (h)->prev)

#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_after ngx_queue_insert_head

#define ngx_queue_insert_tail(h, x)
(x)->prev = (h)->prev;
(x)->prev->next = x;
(x)->next = h;
(h)->prev = x

#define ngx_queue_head(h)
(h)->next

#define ngx_queue_last(h)
(h)->prev

#define ngx_queue_sentinel(h)
(h)

#define ngx_queue_next(q)
(q)->next

#define ngx_queue_prev(q)
(q)->prev

#if (NGX_DEBUG)

#define ngx_queue_remove(x)
(x)->next->prev = (x)->prev;
(x)->prev->next = (x)->next;
(x)->prev = NULL;
(x)->next = NULL

#else

#define ngx_queue_remove(x)
(x)->next->prev = (x)->prev;
(x)->prev->next = (x)->next

#endif

#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;

#define ngx_queue_add(h, n)
(h)->prev->next = (n)->next;
(n)->next->prev = (h)->prev;
(h)->prev = (n)->prev;
(h)->prev->next = h;

#define ngx_queue_data(q, type, link)
(type *) ((u_char *) q - offsetof(type, link))

ngx_queue_t *ngx_queue_middle(ngx_queue_t *queue);
void ngx_queue_sort(ngx_queue_t *queue,
ngx_int_t (*cmp)(const ngx_queue_t *, const ngx_queue_t *));

使用雙向鏈表容器時,需要用一個ngx_queue_t結構體表示容器本身,而這個結構體有若干方法可供使用。

對於鏈表中的每一個元素,其類型可以是任意的struct結構體,但這個結構體中必須要有一個ngx_queue_t類型的成員,在向鏈表容器中添加、刪除元素時都是使用的結構體中ngx_queue_t類型成員的指針。當ngx_queue_t作為鏈表的元素成員使用時,它具有表7-2中列出的4種方法。

如上代碼已經列出了鏈表支持的所有方法,下面將以一個簡單的例子來說明如何使用ngx_queue_t雙向鏈表。

使用雙向鏈表排序的例子

本節定義一個簡單的鏈表,並使用ngx_queue_sort方法對所有元素排序。在這個例子中,可以看到如何定義、初始化ngx_queue_t容器,如何定義任意類型的鏈表元素,如何遍歷鏈表,如何自定義排序方法並執行排序。

首先,定義鏈表元素的結構體,如下面的TestNode結構體:

typedef struct {
u_char* str;
ngx_queue_t qEle;
int num;
} TestNode;

鏈表元素結構體中必須包含ngx_queue_t類型的成員,當然它可以在任意的位置上。本例中它的上面有一個char*指針,下面有一個整型成員num,這樣是允許的。

排序方法需要自定義。下面以TestNode結構體中的num成員作為排序依據,實現compTestNode方法作為排序過程中任意兩元素間的比較方法。

ngx_int_t compTestNode(const ngx_queue_t* a, const ngx_queue_t* b)
{
/*
*首先使用ngx_queue_data方法由ngx_queue_t變數獲取元素結構體TestNode的地址
*/
TestNode aNode = ngx_queue_data(a, TestNode, qEle);
TestNode* bNode = ngx_queue_data(b, TestNode, qEle);
/*
* 返回num成員的比較結果
*/
return aNode->num > bNode->num;
}

這個比較方法結合ngx_queue_sort方法可以把鏈表中的元素按照num的大小以升序排列。

在此例中,可以看到ngx_queue_data的用法,即可以根據鏈表元素結構體TestNode中的qEle成員地址換算出TestNode結構體變數的地址,這是面向過程的C語言編寫的ngx_queue_t鏈表之所以能夠通用化的關鍵。下面來看一下ngx_queue_data的定義:

#define ngx_queue_data(q, type, link)
(type *) ((u_char *) q - offsetof(type, link))

此處offset會返回link成員在type結構體中的偏移量。

這裡可以看出,ngx_queue_data中,可以通過ngx_queue_t類型的指針減去qEle相對於TestNode的地址偏移量,得到TestNode結構體的地址。

下面開始定義雙向鏈表容器queueContainer,並將其初始化為空鏈表,如下所示。

ngx_queue_t queueContainer;
ngx_queue_init(&queueContainer);

鏈表容器以ngx_queue_t定義即可。注意,對於表示鏈表容器的ngx_queue_t結構體,必須調用ngx_queue_init進行初始化。

ngx_queue_t雙向鏈表是完全不負責分配內存的,每一個鏈表元素必須自己管理自己所佔用的內存。因此,本例在進程棧中定義了5個TestNode結構體作為鏈表元素,並把它們的num成員初始化為0、1、2、3、4,如下所示。

int i = 0;
TestNode node[5];
for (; i < 5; i++)
{
node[i].num = i;
}

下面把這5個TestNode結構體添加到queueContainer鏈表中,注意,這裡同時使用了ngx_queue_insert_tail、ngx_queue_insert_head、ngx_queue_insert_after 3個添加方法,讀者不妨思考一下鏈表中元素的順序是什麼樣的。

ngx_queue_insert_tail(&queueContainer, &node[0].qEle);
ngx_queue_insert_head(&queueContainer, &node[1].qEle);
ngx_queue_insert_tail(&queueContainer, &node[2].qEle);
ngx_queue_insert_after(&queueContainer, &node[3].qEle);
ngx_queue_insert_tail(&queueContainer, &node[4].qEle);

根據表7-1中介紹的方法可以得出,如果此時的鏈表元素順序以num成員標識,那麼應該是這樣的:3、1、0、2、4。如果有疑問,不妨寫個遍歷鏈表的程序檢驗一下順序是否如此。下面就根據如上方法說明編寫一段簡單的遍歷鏈表的程序。

ngx_queue_t* q;
for (q = ngx_queue_head(&queueContainer); q != ngx_queue_sentinel(&queueContainer); q = ngx_queue_next(q))
{
TestNode* eleNode = ngx_queue_data(q, TestNode, qEle);
// 處理當前的鏈表元素eleNode

}

上面這段程序將會依次從鏈表頭部遍歷到尾部。反向遍歷也很簡單。讀者可以嘗試使用ngx_queue_last和ngx_queue_prev方法編寫相關代碼。

下面開始執行排序,代碼如下所示。

ngx_queue_sort(&queueContainer, compTestNode);

這樣,鏈表中的元素就會以0、1、2、3、4(num成員的值)的升序排列了。

雙向鏈表是如何實現的

本節將說明ngx_queue_t鏈表容器以及元素中prev成員、next成員的意義,整個鏈表就是通過這兩個指針成員實現的。

下面先來看一下ngx_queue_t結構體作為容器時其prev成員、next成員的意義。當容器為空時,prev和next都將指向容器本身。如下所示,如果在某個結構體中定義了ngx_queue_t容器,其prev指針和next指針都會指向ngx_queue_t成員的地址。

空容器時ngx_queue_t結構體成員的值

當容器不為空時,ngx_queue_t容器的next指針會指向鏈表的第1個元素,而prev指針會指向鏈表的最後1個元素。如下圖所示,這時鏈表中只有1個鏈表元素,容器的next指針和prev指針都將指向這個唯一的鏈表元素。

當僅含1個元素時,容器、元素中的ngx_queue_t結構體成員的值

對於每個鏈表元素來說,其prev成員都指向前一個元素(不存在時指向鏈表容器),而next成員則指向下一個元素(不存在時指向鏈表容器),這如上可以看到。

當容器中有兩個元素時,prev和next的指向如下圖所示。

[當含有兩個或多個元素時,容器、元素中的ngx_queue_t結構體中prev、next成員的值

如上很好地詮釋了前面的定義,容器中的prev成員指向最後1個也就是第2個元素,next成員指向第1個元素。第1個元素的prev成員指向容器本身,而其next成員指向第2個元素。第2個元素的prev成員指向第1個元素,其next成員則指向容器本身。

ngx_queue_t的實現就是這麼簡單,但它的排序演算法ngx_queue_sort使用的插入排序,並不適合為龐大的數據排序。


本文暫取自代碼、網路或書籍,只用作學習備忘,不作盈利等他用。

如有侵權,請聯繫Linkerist@163.com


推薦閱讀:

TAG:Web伺服器 | Nginx | 互聯網 |