天天看點

07Nginx源碼分析之雙向連結清單結構(ngx_queue.c)

一 雙向連結清單結構

Nginx的連結清單結構非常小巧和簡單。設計的非常精巧。通過連結清單的簡單和精巧的設計,讓Nginx的連結清單的資料結構和具體業務依賴進行了解耦。

1 雙向連結清單的資料結構

非常簡單,不存任何其它類型的内容,隻包含了連結清單的前後節點,使用時通過作為其它資料結構的成員使用。

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

例如雙向連結清單在業務上被使用在網絡中。

/**
 * 該結構體用于描述一個網絡連接配接
 */
struct ngx_connection_s {
	void *data; //連接配接未使用時,data用于充當連接配接池中空閑連結清單中的next指針。連接配接使用時由子產品而定,HTTP中,data指向ngx_http_request_t
	ngx_event_t *read; //連接配接對應的讀事件
	ngx_event_t *write; //連接配接對應的寫事件
 
	ngx_socket_t fd; //套接字句柄
 
	ngx_recv_pt recv; //直接接受網絡位元組流
	ngx_send_pt send; //直接發送網絡位元組流
	ngx_recv_chain_pt recv_chain; //網絡位元組流接收連結清單
	ngx_send_chain_pt send_chain; //網絡位元組流發送連結清單
 
	/*用來将目前連接配接以雙向連結清單元素的形式添加到ngx_cycle_t核心結構體
	 * 的reuseable_connection_queue雙向連結清單中,表示可以重用的連接配接*/
	ngx_queue_t queue;
 
	/* 省去部分 */
};
           

雙向連結清單圖,對于下面了解非常有幫助。

07Nginx源碼分析之雙向連結清單結構(ngx_queue.c)

二 具體函數實作

Nginx的連結清單主要使用宏來代替函數的操作。

//注意雙向連結清單的有效節點不包含h頭節點。

/**
 * 初始化一個雙向連結清單。
 */
#define ngx_queue_init(q)                                                     \
    (q)->prev = q;                                                            \
    (q)->next = q


/**
 * 判斷雙向連結清單是否為空,隻有一個節點而沒有實際的節點也是空,例如隻有h為空,h->a共h與a節點則不是空。
 */
#define ngx_queue_empty(h)                                                    \
    (h == (h)->prev)

/**
 * 往h後面插入一個節點x。
 */
#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

/**
 * 往h末尾插入一個節點x。
 * 注意第一行的(h)->prev代表末尾節點,因為h是一個雙向連結清單。
 */
#define ngx_queue_insert_tail(h, x)                                           \
    (x)->prev = (h)->prev;                                                    \
    (x)->prev->next = x;                                                      \
    (x)->next = h;                                                            \
    (h)->prev = x

/**
 * 擷取雙向連結清單的頭節點,即h的下一節點。
 */
#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


/**
 * 移除連結清單的節點x。
 */
#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 *));
           

1 解釋ngx_queue_split

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

h為連結清單,q為連結清單h中的一個元素,這個方法可以将連結清單h以元素q為界拆分為兩個連結清單h和n,其中h由原連結清單的前半部分組成(不包含q),而n由後半部分組成,n為節點頭,q為包含實際資料的首元素。最終通過分割後擷取到兩個連結清單,即h->有效節點,n->有效節點。

大家可以根據下圖自己畫一下即可,我這裡不畫了。并且注意,本章使用畫圖了解是非常必要的。

07Nginx源碼分析之雙向連結清單結構(ngx_queue.c)

2 解釋ngx_queue_add

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

該宏函數的作用是在連結清單h尾部添加一個新的連結清單,但是不包含節點n,即不包含無效的節點n,非常牛逼,我開始看的時候看得很懵,不知道為啥不包含節點n,最終看完整篇文章才知道連結清單的首節點是無效節點才了解。n=1無意義。

如圖解釋:

07Nginx源碼分析之雙向連結清單結構(ngx_queue.c)

3 解釋ngx_queue_data

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

ngx_queue_data就簡單得多了,實際上該函數就是調用offsetof函數去實作的。

例子:

/**
 * 通過連結清單可以找到結構體所在的指針
 * typedef struct {
 * 		ngx_queue_s queue;
 * 		char * x;
 * 		....
 * } TYPE
 * 
 * 調用時:
 * TYPE *a = ngx_queue_data(&type->queue, TYPE, queue);
 * 其中:參1為結構體變量,參2為該變量的類型,參3為結構體的成員名(必須與定義時一緻,否則報錯)。
 * 注意:
 * offsetof(TYPE, link))傳回的是結構體的偏移量,((struct_name*)0為絕對偏移位址0。
 * 是以我們這個例子最終的結果就是&type->queue-0=&type->queue,也就是該結構體變量的首位址。牛逼吧,即使将成員queue不放在第一個位置,結果也是一樣。
 * 
 * #define OFFSETOF(struct_name, member_name) 	\
 * (int) &( ((struct_name*)0)->member_name )  
 */
           

說白了,這個宏主要是考你對結構體的偏移位址的了解,絕對偏移位址為0。

關于類似ngx_queue_data的用法,使用結構體變量某個成員的位址來擷取變量的首位址的具體說明,請參考以下部落格。

4 解釋ngx_queue_t *ngx_queue_middle

ngx_queue_t *
ngx_queue_middle(ngx_queue_t *queue)
{
    ngx_queue_t  *middle, *next;

    middle = ngx_queue_head(queue);//擷取首個有效節點

    if (middle == ngx_queue_last(queue)) {//隻有一個有效節點或者為空連結清單則直接傳回
        return middle;
    }

    next = ngx_queue_head(queue);

	//此時middle和next均指向連結清單的首個有效節點
    for ( ;; ) {
        middle = ngx_queue_next(middle);

        next = ngx_queue_next(next);

        if (next == ngx_queue_last(queue)) {
            return middle;
        }

        next = ngx_queue_next(next);

        if (next == ngx_queue_last(queue)) {
            return middle;
        }
    }
}
           
  • 1)該函數作用:擷取queue連結清單的中間節點。
  • 2)算法的基本思想: middle指針每次後移一個節點,而next指針每次後移兩個節點。時間複雜度就是O(N)。這是一道經典的面試題,今天在這裡看到了源碼,似成相識啊,果然經典面試題目都不是憑空而來。
  • 3)通過代入8個節點發現(包括無效資料的頭節點),看下面:
2個以下時直接傳回了。是以從3開始判斷(建議必須畫圖觀察):

節點數=3時,middle下标=2;
節點數=4時,middle下标=2;
節點數=5時,middle下标=3;
節點數=6時,middle下标=3;
節點數=7時,middle下标=4;
節點數=8時,middle下标=4;

總結公式有:
1)總節點為奇數時:n/2+1。
2)總節點為偶數時:n/2。

這~就是nginx源碼的魅力。
           

5 解釋void ngx_queue_sort

ngx_queue_sort的作用是穩定将雙向連結清單内的元素進行排序,規則由傳進的回調函數設定,可從小到大或者從大到小。該函數使用的排序方法為插入排序法,兩兩比較,每次确定一個元素進行插入。

/* the stable insertion sort */

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;
    }
	
	/*
		主要思想是:通過p和q操作兩兩比較(next為自增不考慮分析避免繁瑣),每次确定一個元
		素放在h連結清單的最後面,注意此時h可以認為是新的連結清單,舊的連結清單由p,q儲存,若出現不
		滿足if條件的情況,則一直往排序好的元素前面繼續比較。
		
		例如,這裡以從小到大舉例,即回調函數傳回prev>q即可。元素:10,5,11 (注:這裡的數字是儲存
		的一個結構體的,為了友善直接寫出來了。)
		
		1)首先p指向10,q指向5.
		2)由于10>5傳回ture故不滿足if,繼續往排序好的連結清單前查找,由于第一次排序好的連結清單為空,故
		直接跳出循環(dowhile),将5插入prev(此時prev=h),即h->5->10->11
		3)然後繼續循環,p指向10,q指向11,滿足if直接跳出。然後将11插入10的後面,即h->5->10->11
		4)然後繼續循環,由于q=next時指向h(h為哨兵節點),是以for循環退出,共排n-1次。
		
		明白後以後可以這樣直接分析:
		1)q=5,p=10,n=11. 結果=>h->5->10->11
		2)q=11,p=10(dowhile向前導緻),n=h. 結果=>h->5->10->11
		3)for循環條件不滿足,直接退出。
	*/
    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_sentinel(queue)為哨兵,即連結清單頭結點h。

        ngx_queue_insert_after(prev, q);//依次往已經排好的節點後面添加節點。
    }
}
           

繼續閱讀