libevent數據結構-TAILQ_QUEUE隊列

 

 轉載出處: http://blog.csdn.net/luotuo44/article/details/38374009

Libevent源碼中有一個queue.h文件,位於compat/sys目錄下。該文件裏面定義了5個數據結構,其中TAILQ_QUEUE是使得最廣泛的。本文就說一下這個數據結構。


隊列結構體:

        TAILQ_QUEUE由下面兩個結構體一起配合工作。

[cpp] view plain copy
  1. #define TAILQ_HEAD(name, type)                      \  
  2. struct name {                               \  
  3.     struct type *tqh_first; /* first element */         \  
  4.     struct type **tqh_last; /* addr of last next element */     \  
  5. }  
  6.   
  7. //和前面的TAILQ_HEAD不同,這裏的結構體並沒有name.即沒有結構體名。  
  8. //所以該結構體只能作爲一個匿名結構體。所以,它一般都是另外一個結構體  
  9. //或者共用體的成員  
  10. #define TAILQ_ENTRY(type)                       \  
  11. struct {                                \  
  12.     struct type *tqe_next;  /* next element */          \  
  13.     struct type **tqe_prev; /* address of previous next element */  \  
  14. }  
        由這兩個結構體配合構造出來的隊列一般如下圖所示:

        

        圖中,一級指針指向的是queue_entry_t這個結構體,即存儲queue_entry_t這個結構體的地址值。二級指針存儲的是一級地址變量的地址值。所以二級指針指向的是圖中的一級指針,而非結構體。圖中的1,2, 3爲隊列元素保存的一些值。


隊列操作宏函數以及使用例子:

        除了這兩個結構體,在queue.h文件中,還爲TAILQ_QUEUE定義了一系列的訪問和操作函數。很不幸,它們是一些宏定義。這裏就簡單貼幾個函數(準確來說,不是函數)的代碼。

[cpp] view plain copy
  1. #define TAILQ_FIRST(head)       ((head)->tqh_first)  
  2.   
  3. #define TAILQ_NEXT(elm, field)      ((elm)->field.tqe_next)  
  4.   
  5. #define TAILQ_INIT(head) do {                       \  
  6.     (head)->tqh_first = NULL;                    \  
  7.     (head)->tqh_last = &(head)->tqh_first;                \  
  8. while (0)  
  9.   
  10. #define TAILQ_INSERT_TAIL(head, elm, field) do {            \  
  11.     (elm)->field.tqe_next = NULL;                    \  
  12.     (elm)->field.tqe_prev = (head)->tqh_last;         \  
  13.     *(head)->tqh_last = (elm);                   \  
  14.     (head)->tqh_last = &(elm)->field.tqe_next;            \  
  15. while (0)  
  16.   
  17. #define TAILQ_REMOVE(head, elm, field) do {             \  
  18.     if (((elm)->field.tqe_next) != NULL)             \  
  19.         (elm)->field.tqe_next->field.tqe_prev =           \  
  20.             (elm)->field.tqe_prev;               \  
  21.     else                                \  
  22.         (head)->tqh_last = (elm)->field.tqe_prev;     \  
  23.     *(elm)->field.tqe_prev = (elm)->field.tqe_next;           \  
  24. while (0)  

        這些宏是很難看的,也沒必要直接去看這些宏。下面來看一個使用例子。有例子更容易理解。

[cpp] view plain copy
  1. //隊列中的元素結構體。它有一個值,並且有前向指針和後向指針  
  2. //通過前後像指針,把隊列中的節點(元素)連接起來  
  3. struct queue_entry_t  
  4. {  
  5.     int value;  
  6.   
  7.     //從TAILQ_ENTRY的定義可知,它只能是結構體或者共用體的成員變量  
  8.     TAILQ_ENTRY(queue_entry_t)entry;  
  9. };  
  10.   
  11. //定義一個結構體,結構體名爲queue_head_t,成員變量類型爲queue_entry_t  
  12. //就像有頭節點的鏈表那樣,這個是隊列頭。它有兩個指針,分別指向隊列的頭和尾  
  13. TAILQ_HEAD(queue_head_t, queue_entry_t);  
  14.   
  15. int main(int argc, char **argv)  
  16. {  
  17.     struct queue_head_t queue_head;  
  18.     struct queue_entry_t *q, *p, *s, *new_item;  
  19.     int i;  
  20.   
  21.     TAILQ_INIT(&queue_head);  
  22.   
  23.     for(i = 0; i < 3; ++i)  
  24.     {  
  25.         p = (struct queue_entry_t*)malloc(sizeof(struct queue_entry_t));  
  26.         p->value = i;  
  27.   
  28.         //第三個參數entry的寫法很怪,居然是一個成員變量名(field)  
  29.         TAILQ_INSERT_TAIL(&queue_head, p, entry);//在隊尾插入數據  
  30.     }  
  31.   
  32.     q = (struct queue_entry_t*)malloc(sizeof(struct queue_entry_t));  
  33.     q->value = 10;  
  34.     TAILQ_INSERT_HEAD(&queue_head, q, entry);//在隊頭插入數據  
  35.   
  36.     //現在q指向隊頭元素、p指向隊尾元素  
  37.   
  38.     s = (struct queue_entry_t*)malloc(sizeof(struct queue_entry_t));  
  39.     s->value = 20;  
  40.     //在隊頭元素q的後面插入元素  
  41.     TAILQ_INSERT_AFTER(&queue_head, q, s, entry);  
  42.   
  43.   
  44.     s = (struct queue_entry_t*)malloc(sizeof(struct queue_entry_t));  
  45.     s->value = 30;  
  46.     //在隊尾元素p的前面插入元素  
  47.     TAILQ_INSERT_BEFORE(p, s, entry);  
  48.   
  49.     //現在進行輸出  
  50.     //獲取第一個元素  
  51.     s = TAILQ_FIRST(&queue_head);  
  52.     printf("the first entry is %d\n", s->value);  
  53.       
  54.     //獲取下一個元素  
  55.     s = TAILQ_NEXT(s, entry);  
  56.     printf("the second entry is %d\n\n", s->value);  
  57.   
  58.     //刪除第二個元素, 但並沒有釋放s指向元素的內存  
  59.     TAILQ_REMOVE(&queue_head, s, entry);  
  60.     free(s);  
  61.   
  62.   
  63.     new_item = (struct queue_entry_t*)malloc(sizeof(struct queue_entry_t));  
  64.     new_item->value = 100;  
  65.   
  66.     s = TAILQ_FIRST(&queue_head);  
  67.     //用new_iten替換第一個元素  
  68.     TAILQ_REPLACE(&queue_head, s, new_item, entry);  
  69.   
  70.   
  71.     printf("now, print again\n");  
  72.     i = 0;  
  73.     TAILQ_FOREACH(p, &queue_head, entry)//用foreach遍歷所有元素  
  74.     {  
  75.         printf("the %dth entry is %d\n", i, p->value);  
  76.     }  
  77.   
  78.     p = TAILQ_LAST(&queue_head, queue_head_t);  
  79.     printf("last is %d\n", p->value);  
  80.   
  81.     p = TAILQ_PREV(p, queue_head_t, entry);  
  82.     printf("the entry before last is %d\n", p->value);  
  83. }  

        例子並不難看懂。這裏就不多講了。


展開宏函數:

        下面把這些宏翻譯一下(即展開),顯示出它們的本來面貌。這當然不是用人工方式去翻譯。而是用gcc 的-E選項。

        閱讀代碼時要注意,tqe_prev和tqh_last都是二級指針,行爲會有點難理解。平常我們接觸到的雙向鏈表,next和prev成員都是一級指針。對於像鏈表A->B->C(把它們想象成雙向鏈表),通常B的prev指向A這個結構體本身。此時,B->prev->next指向了本身。但隊列Libevent的TAILQ_QUEUE,B的prev是一個二級指向,它指向的是A結構體的next成員。此時,*B->prev就指向了本身。當然,這並不能說用二級指針就方便。我覺得用二級指針理解起來更難,編寫代碼更容易出錯。

[cpp] view plain copy
  1. //隊列中的元素結構體。它有一個值,並且有前向指針和後向指針  
  2. //通過前後像指針,把隊列中的節點連接起來  
  3. struct queue_entry_t  
  4. {  
  5.     int value;  
  6.   
  7.     struct  
  8.     {  
  9.         struct queue_entry_t *tqe_next;  
  10.         struct queue_entry_t **tqe_prev;  
  11.     }entry;  
  12. };  
  13.   
  14. //就像有頭節點的鏈表那樣,這個是隊列頭。它有兩個指針,分別指向隊列的頭和尾  
  15. struct queue_head_t  
  16. {  
  17.     struct queue_entry_t *tqh_first;  
  18.     struct queue_entry_t **tqh_last;  
  19. };  
  20.   
  21. int main(int argc, char **argv)  
  22. {  
  23.     struct queue_head_t queue_head;  
  24.     struct queue_entry_t *q, *p, *s, *new_item;  
  25.     int i;  
  26.   
  27.     //TAILQ_INIT(&queue_head);  
  28.     do  
  29.     {  
  30.         (&queue_head)->tqh_first = 0;  
  31.         //tqh_last是二級指針,這裏指向一級指針  
  32.         (&queue_head)->tqh_last = &(&queue_head)->tqh_first;  
  33.     }while(0);  
  34.   
  35.     for(i = 0; i < 3; ++i)  
  36.     {  
  37.         p = (struct queue_entry_t*)malloc(sizeof(struct queue_entry_t));  
  38.         p->value = i;  
  39.   
  40.         //TAILQ_INSERT_TAIL(&queue_head, p, entry);在隊尾插入數據  
  41.         do  
  42.         {  
  43.             (p)->entry.tqe_next = 0;  
  44.             //tqh_last存儲的是最後一個元素(隊列節點)tqe_next成員  
  45.             //的地址。所以,tqe_prev指向了tqe_next。  
  46.             (p)->entry.tqe_prev = (&queue_head)->tqh_last;  
  47.   
  48.             //tqh_last存儲的是最後一個元素(隊列節點)tqe_next成員  
  49.             //的地址,所以*(&queue_head)->tqh_last修改的是最後一個  
  50.             //元素的tqe_next成員的值,使得tqe_next指向*p(新的隊列  
  51.             //節點)。  
  52.             *(&queue_head)->tqh_last = (p);  
  53.             //隊頭結構體(queue_head)的tqh_last成員保存新隊列節點的  
  54.             //tqe_next成員的地址值。即讓tqh_last指向tqe_next。  
  55.             (&queue_head)->tqh_last = &(p)->entry.tqe_next;  
  56.         }while(0);  
  57.     }  
  58.   
  59.     q = (struct queue_entry_t*)malloc(sizeof(struct queue_entry_t));  
  60.     q->value = 10;  
  61.   
  62.     //TAILQ_INSERT_HEAD(&queue_head, q, entry);在隊頭插入數據  
  63.     do {  
  64.         //queue_head隊列中已經有節點(元素了)。要對第一個元素進行修改  
  65.         if(((q)->entry.tqe_next = (&queue_head)->tqh_first) != 0)  
  66.             (&queue_head)->tqh_first->entry.tqe_prev = &(q)->entry.tqe_next;  
  67.         else//queue_head隊列目前是空的,還沒有任何節點(元素)。修改queue_head即可  
  68.             (&queue_head)->tqh_last = &(q)->entry.tqe_next;  
  69.   
  70.         //queue_head的first指針指向要插入的節點*q  
  71.         (&queue_head)->tqh_first = (q);  
  72.         (q)->entry.tqe_prev = &(&queue_head)->tqh_first;  
  73.     }while(0);  
  74.   
  75.     //現在q指向隊頭元素、p指向隊尾元素  
  76.   
  77.     s = (struct queue_entry_t*)malloc(sizeof(struct queue_entry_t));  
  78.     s->value = 20;  
  79.   
  80.     //TAILQ_INSERT_AFTER(&queue_head, q, s, entry);在隊頭元素q的後面插入元素  
  81.     do  
  82.     {  
  83.         //q不是最後隊列中最後一個節點。要對q後面的元素進行修改  
  84.         if (((s)->entry.tqe_next = (q)->entry.tqe_next) != 0)  
  85.             (s)->entry.tqe_next->entry.tqe_prev = &(s)->entry.tqe_next;  
  86.         else//q是最後一個元素。對queue_head修改即可  
  87.             (&queue_head)->tqh_last = &(s)->entry.tqe_next;  
  88.   
  89.         (q)->entry.tqe_next = (s);  
  90.         (s)->entry.tqe_prev = &(q)->entry.tqe_next;  
  91.     }while(0);  
  92.   
  93.   
  94.   
  95.     s = (struct queue_entry_t*)malloc(sizeof(struct queue_entry_t));  
  96.     s->value = 30;  
  97.   
  98.     //TAILQ_INSERT_BEFORE(p, s, entry); 在隊尾元素p的前面插入元素  
  99.     do  
  100.     {  
  101.         //無需判斷節點p前面是否還有元素。因爲即使沒有元素,queue_head的兩個  
  102.         //指針從功能上也相當於一個元素。這點是採用二級指針的一大好處。  
  103.         (s)->entry.tqe_prev = (p)->entry.tqe_prev;  
  104.         (s)->entry.tqe_next = (p);  
  105.         *(p)->entry.tqe_prev = (s);  
  106.         (p)->entry.tqe_prev = &(s)->entry.tqe_next;  
  107.     }while(0);  
  108.   
  109.   
  110.     //現在進行輸出  
  111.   
  112.     // s = TAILQ_FIRST(&queue_head);  
  113.     s = ((&queue_head)->tqh_first);  
  114.     printf("the first entry is %d\n", s->value);  
  115.   
  116.     // s = TAILQ_NEXT(s, entry);  
  117.     s = ((s)->entry.tqe_next);  
  118.     printf("the second entry is %d\n\n", s->value);  
  119.   
  120.     //刪除第二個元素, 但並沒有釋放s指向元素的內存  
  121.     //TAILQ_REMOVE(&queue_head, s, entry);  
  122.     do  
  123.     {  
  124.         if (((s)->entry.tqe_next) != 0)  
  125.             (s)->entry.tqe_next->entry.tqe_prev = (s)->entry.tqe_prev;  
  126.         else (&queue_head)->tqh_last = (s)->entry.tqe_prev;  
  127.   
  128.         *(s)->entry.tqe_prev = (s)->entry.tqe_next;  
  129.     }while(0);  
  130.   
  131.     free(s);  
  132.   
  133.   
  134.     new_item = (struct queue_entry_t*)malloc(sizeof(struct queue_entry_t));  
  135.     new_item->value = 100;  
  136.   
  137.     //s = TAILQ_FIRST(&queue_head);  
  138.     s = ((&queue_head)->tqh_first);  
  139.   
  140.     //用new_iten替換第一個元素  
  141.     //TAILQ_REPLACE(&queue_head, s, new_item, entry);  
  142.     do  
  143.     {  
  144.         if (((new_item)->entry.tqe_next = (s)->entry.tqe_next) != 0)  
  145.             (new_item)->entry.tqe_next->entry.tqe_prev = &(new_item)->entry.tqe_next;  
  146.         else  
  147.             (&queue_head)->tqh_last = &(new_item)->entry.tqe_next;  
  148.   
  149.         (new_item)->entry.tqe_prev = (s)->entry.tqe_prev;  
  150.         *(new_item)->entry.tqe_prev = (new_item);  
  151.     }while(0);  
  152.   
  153.   
  154.     printf("now, print again\n");  
  155.     i = 0;  
  156.     //TAILQ_FOREACH(p, &queue_head, entry)//用foreach遍歷所有元素  
  157.     for((p) = ((&queue_head)->tqh_first); (p) != 0;  
  158.         (p) = ((p)->entry.tqe_next))  
  159.     {  
  160.         printf("the %dth entry is %d\n", i, p->value);  
  161.     }  
  162.   
  163.     //p = TAILQ_LAST(&queue_head, queue_head_t);  
  164.     p = (*(((struct queue_head_t *)((&queue_head)->tqh_last))->tqh_last));  
  165.     printf("last is %d\n", p->value);  
  166.   
  167.   
  168.     //p = TAILQ_PREV(p, queue_head_t, entry);  
  169.     p = (*(((struct queue_head_t *)((p)->entry.tqe_prev))->tqh_last));  
  170.     printf("the entry before last is %d\n", p->value);  
  171. }  

        代碼中有一些註釋,不懂的可以看看。其實對於鏈表操作,別人用文字說再多都對自己理解幫助不大。只有自己動手一步步把鏈表操作都畫出來,這樣才能完全理解。

 

特殊指針操作:

        最後那兩個操作宏函數有點難理解,現在來講一下。在講之前,先看一個關於C語言指針的例子。
[cpp] view plain copy
  1. #include<stdio.h>  
  2.   
  3. struct item_t  
  4. {  
  5.     int a;  
  6.     int b;  
  7.     int c;  
  8. };  
  9.   
  10. struct entry_t  
  11. {  
  12.     int a;  
  13.     int b;  
  14. };  
  15.   
  16.   
  17. int main()  
  18. {  
  19.     struct item_t item = { 1, 2, 3};  
  20.   
  21.     entry_t *p = (entry_t*)(&item.b);  
  22.     printf("a = %d, b = %d\n", p->a, p->b);  
  23.   
  24.     return 0;  
  25. }  
        代碼輸出的結果是:a = 2, b = 3

        對於entry_t *p, 指針p指向的內存地址爲&item.b。此時對於編譯器來說,它認爲從&item.b這個地址開始,是一個entry_t結構體的內存區域。並且把前4個字節當作entry_t成員變量a的值,後4個字節當作entry_t成員變量b的值。所以就有了a = 2, b = 3這個輸出。

        好了,現在開始講解那兩個難看懂的宏。先看一張圖。

        

        雖然本文最前面的圖佈局更好看一點,但這張圖才更能反映文中這兩個結構體的內存佈局。不錯,tqe_next是在tqe_prev的前面。這使得tqe_next、tqe_prev於tqh_first、tqh_last的內存佈局一樣。一級指針在前,二級指針在後。

        現在來解析代碼中最後兩個宏函數。


隊尾節點:

[cpp] view plain copy
  1. //p = TAILQ_LAST(&queue_head, queue_head_t);  
  2. p = (*(((struct queue_head_t *)((&queue_head)->tqh_last))->tqh_last));  

        首先是(&queue_head)->tqh_last,它的值是最後一個元素的tqe_next這個成員變量的地址。然後把這個值強制轉換成struct queue_head_t *指針。此時,相當於有一個匿名的struct queue_head_t類型指針q。它指向的地址爲隊列的最後一個節點的tqe_next成員變量的地址。無論一級還是二級指針,其都是指向另外一個地址。只是二級指針只能指向一個一級指針的地址。

        此時,在編譯器看來,從tqe_next這個變量的地址開始,是一個struct queue_head_t結構體的內存區域。並且可以將代碼簡寫成:
[cpp] view plain copy
  1. p = (*(q->tqh_last));  

        

        回想一下剛纔的那個例子。q->tqh_last的值就是上圖中最後一個節點的tqe_prev成員變量的值。所以*(q->tqh_last))就相當於*tqe_prev。注意,變量tqe_prev是一個二級指針,它指向倒數第二個節點的tqe_next成員。所以*tqe_prev獲取了倒數第二個節點的tqe_next成員的值。它的值就是最後一個節點的地址。最後,將這個地址賦值給p,此時p指向最後一個節點。完成了任務。好複雜的過程。


前一個節點:

        現在來看一下最後那個宏函數,代碼如下:

[cpp] view plain copy
  1. //p = TAILQ_PREV(p, queue_head_t, entry);  
  2. p = (*(((struct queue_head_t *)((p)->entry.tqe_prev))->tqh_last));  
        注意,右邊的p此時是指向最後一個節點(元素)的。所以(p)->entry.tqe_prev就是倒數第二個節點tqe_next成員的地址。然後又強制轉換成struct queue_head_t指針。同樣,假設一個匿名的struct queue_head_t *q;此時,宏函數可以轉換成:

[cpp] view plain copy
  1. p = (*((q)->tqh_last));  

        同樣,在編譯器看來,從倒數第二個參數節點tqe_next的地址開始,是一個structqueue_head_t結構體的內存區域。所以tqh_last實際值是tqe_prev變量上的值,即tqe_prev指向的地址。*((q)->tqh_last)就是*tqe_prev,即獲取tqe_prev指向的倒數第三個節點的tqe_next的值。而該值正是倒數第二個節點的地址。將這個地址賦值給p,此時,p就指向了倒數第二個節點。完成了TAILQ_PREV函數名的功能。

 

        這個過程確實有點複雜。而且還涉及到強制類型轉換。

        其實,在TAILQ_LAST(&queue_head, queue_head_t);中,既然都可以獲取最後一個節點的tqe_next的地址值,那麼直接將該值 + 4就可以得到tqe_precv的地址值了(假設爲pp)。有了該地址值pp,那麼直接**pp就可以得到最後一個節點的地址了。代碼如下:
[cpp] view plain copy
  1. struct queue_entry_t **pp = (&queue_head)->tqh_last;  
  2. pp += 1; //加1個指針的偏移量,在32位的系統中,就等於+4  
  3.   
  4. //因爲這裏得到的是二級指針的地址值,所以按理來說,得到的是一個  
  5. //三級指針。故要用強制轉換成三級指針。  
  6. struct queue_entry_t ***ppp = (struct queue_entry_t ***)pp;  
  7.   
  8. s = **ppp;  
  9. printf("the last is %d\n", s->value);  

        該代碼雖然能得到正確的結果,但總感覺直接加上一個偏移量的方式太粗暴了。

        有一點要提出,+1那裏並不會因爲在64位的系統就不能運行,一樣能正確運行的。因爲1不是表示一個字節,而是一個指針的偏移量。在64位的系統上一個指針的偏移量爲8字節。這種”指針 + 數值”,實際其增加的值爲:數值 + sizeof(*指針)。不信的話,可以試一下char指針、int指針、結構體指針(結構體要有多個成員)。

 

 

        好了,還是回到最開始的問題上吧。這個TAILQ_QUEUE隊列是由兩部分組成:隊列頭和隊列節點。在Libevent中,隊列頭一般是event_base結構體的一個成員變量,而隊列節點則是event結構體。比如event_base結構體裏面有一個struct event_list eventqueue;其中,結構體struct event_list如下定義:
[cpp] view plain copy
  1. //event_struct.h  
  2. TAILQ_HEAD (event_list, event);  
  3.   
  4. //所以event_list的定義展開後如下:  
  5. struct event_list  
  6. {  
  7.     struct event *tqh_first;  
  8.     struct event **tqh_last;  
  9. };  
        在event結構體中,則有幾個TAILQ_ENTRY(event)類型的成員變量。這是因爲根據不同的條件,採用不同的隊列把這些event結構體連在一起,放到一條隊列中。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章