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
②如何獲取尾節點
按照上面的分析,思路是一樣的。。