TAILQ的使用與源碼分析

TAILQ是Linux中的一種雙向隊列(在libevent中有廣泛引用),能實現操作隊列需要的各種操作:插入元素,刪除元素,遍歷隊列等。這個隊列的優點是插入元素很快。

簡單的例子

#include <sys/queue.h>
#include <stdio.h>

struct item_t {
    int value;
    TAILQ_ENTRY(item_t) entries; // 鏈表的指針域,由TAILQ來控制
};

TAILQ_HEAD(item_head_t, item_t)  tail_head; // 創建鏈表

int main(){
    TAILQ_INIT(&tail_head); //init head
    item_t item1, item2, item3;
    item_t *node;
    item1.value = 1;
    item2.value = 2;
    item3.value = 3;
    TAILQ_INSERT_TAIL(&tail_head, &item1, entries); //鏈表狀態:1
    TAILQ_INSERT_HEAD(&tail_head, &item2, entries); // 鏈表狀態:2 1
    TAILQ_INSERT_AFTER(&tail_head, &item2, &item3, entries); // item3插在item2後面; 鏈表狀態: 2 3 1
    TAILQ_FOREACH(node, &tail_head, entries) {
        printf("%d ", node->value);
    }
    printf("\n"); // 輸出2 3 1

    TAILQ_REMOVE(&tail_head, &item2, entries); // 移除item2, 此時鏈表狀態:3 1
    TAILQ_FOREACH(node, &tail_head, entries) {
        printf("%d ", node->value);
    }
    printf("\n"); // 輸出3 1

}

源碼分析

1. TAILQ_ENTRY TAILQ_HEAD結構體

TAILQ_ENTRY結構體和TAILQ_HEAD結構體基本一致,但是表示的含義不一樣。TAILQ_ENTRY結構體用來表示鏈表節點的指針域(類似於平時編程中的鏈表指針域,單向鏈表中一般含有next指針,而雙向鏈表含有pre,next指針)。
TAILQ_HEAD結構是用來表示鏈表的頭節點和尾節點的指針。

#define TAILQ_HEAD(name, type)			\
struct name {					\
	struct type *tqh_first;			\
	struct type **tqh_last;			\
}

#define TAILQ_ENTRY(type)						\
struct {								\
	struct type *tqe_next;	/* next element */			\
	struct type **tqe_prev;	/* address of previous next element */	\
}

注意到,這裏的tqe_pre和tqh_last都是二級指針。下圖是TAILQ的內部結構(來源:https://blog.csdn.net/ylo523/article/details/43274627)
在這裏插入圖片描述
爲什麼是二級指針?
next是指向的是下一個元素的地址(由於下一個元素的類型是type,所以是一級指針type*)
pre是指向上一個元素next成員的地址(由於next成員類型是type*,所以需要使用指針type**)
跟我們平常實現的鏈表有什麼不同?
我們平常的雙向鏈表大概是這樣子的,

struct item_t {
	int value;
	item_t *pre, *next;
};
// 簡單的3個節點插入過程
item_t head,node1,node2;
head.value=1,node1.value=2,node2.value=3;
head.pre=NULL;head.next=&node1;
node1.pre=&head;node1.next=&node2;
node2.pre=&node1;node2.next=NULL;

指針域都是以及指針,因爲只需要存放下一個元素或者是上一個元素的地址;而TAILQ的pre指針存放的是上一個元素某一個成員的地址,而該成員的類型剛好是type*。

2. 初始化和尾部插入 TAILQ_INIT TAILQ_INSERT_TAILQ

#define	TAILQ_INIT(head) do {						\
	(head)->tqh_first = NULL;					\
	(head)->tqh_last = &(head)->tqh_first;				\
} while (/*CONSTCOND*/0)

#define TAILQ_INSERT_TAIL(head, elm, field) do{            \
   (elm)->field.tqe_next = NULL;                   \
   (elm)->field.tqe_prev = (head)->tqh_last;           \
   *(head)->tqh_last = (elm);                   \
   (head)->tqh_last = &(elm)->field.tqe_next;           \
}while (0)

初始化:將last指向鏈表的first域
尾插入:畫圖之後很好理解。。
在這裏插入圖片描述

3. 頭部插入TAILQ_INSERT_HEAD

#define	TAILQ_INSERT_HEAD(head, elm, field) do {			\
	if (((elm)->field.tqe_next = (head)->tqh_first) != NULL)	\
		(head)->tqh_first->field.tqe_prev =			\
		    &(elm)->field.tqe_next;				\
	else								\
		(head)->tqh_last = &(elm)->field.tqe_next;		\
	(head)->tqh_first = (elm);					\
	(elm)->field.tqe_prev = &(head)->tqh_first;			\
} while (/*CONSTCOND*/0)

這裏的思路與前面的一樣,畫個圖就能看出來,需要注意的是head的可能是空,所以head的tqe_first如果是空的話,則需要單獨處理。

4. 遍歷與逆序遍歷

#define	TAILQ_FOREACH(var, head, field)					\
	for ((var) = ((head)->tqh_first);				\
		(var);							\
		(var) = ((var)->field.tqe_next))

這個就是對TAILQ的進行簡單的遍歷,容易理解。

逆序遍歷比較的複雜,源碼如下:

#define	TAILQ_FOREACH_REVERSE(var, head, headname, field)		\
	for ((var) = (*(((struct headname *)((head)->tqh_last))->tqh_last));	\
		(var);							\
		(var) = (*(((struct headname *)((var)->field.tqe_prev))->tqh_last)))

爲了理解這個源碼,我們需要知道以下:
①假設現在有一個TAILQ,其中某一個節點的指針爲node,如何找到node的上一個節點?
在這裏插入圖片描述
結構體在64位下的內存佈局如上圖所示(64位的指針爲8字節)
根據TAILQ結構的性質,我們可以得到該等式是成立的(不考慮空指針的問題)
*(node->prev) == node
由上面的公式得知,把node->prev看成整體,知道一個節點的prev指針就可以獲取到該節點的指針(這就是用二級指針的原因)。知道這個性質,我們就可以來求解node節點的前置指針了。

首先: 獲取到node節點的前一個節點的next域的地址 node->prev
然後: 以這個地址爲起始,將next和prev看成是一個TAILQ_HEAD的結構體,即可獲取到node前一個節點的prev指針的地址 item_head_t* p1 = (item_head_t*)(node->prev); item_t *p2 = p1->last;
最後,按上面那個公式的套路來獲取指針 *p2

②如何獲取尾節點
按照上面的分析,思路是一樣的。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章