程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> C語言的那些小秘密之鏈表(四)

C語言的那些小秘密之鏈表(四)

編輯:關於C語言

 

大多數的讀者在學習編程語言的時候都不喜歡那些枯燥的文字描述,包括我自己在開始學習編程的時候也是這樣,對於代碼的熱情遠遠高於文字,所以我在我寫東西的時候也不喜歡用枯燥的文字描述來向讀者講解,更喜歡用代碼加上適當的文字描述的方式進行講解,因為有些東西可能用枯燥的文字描述半天還不如實實在在的給讀者呈現出一段簡單的代碼,讓讀者理解得更加的透徹些。但是並不是說文字描述就沒用,文字描述也很重要,只是絕大部分讀者都更加的希望直接達到最終的效果,都想跳過那些中間的步驟。接下來我們接著上一篇博客《C語言的那些小秘密之鏈表(三)》的內容繼續講解linux內核雙向循環鏈表。

特此說明:我會把我在文章中編寫代碼時候用到的頭文件list.h上傳到我的空間,免積分下載,有需要的讀者可以自己去下載,當然也可以自己上網下載或者從自己安裝的linux系統中得到。下載地址:http://download.csdn.net/user/bigloomy

 

static inline int list_empty(const struct list_head *head)

{

        return head->next == head;

}

 

static inline int list_empty_careful(const struct list_head *head)

{

        struct list_head *next = head->next;

        return (next == head) && (next == head->prev);

}

list_empty()函數和list_empty_careful()函數都是用來檢測鏈表是否為空的。但是稍有區別的就是第一個鏈表使用的檢測方法是判斷表頭的結點的下一個結點是否為其本身,如果是則返回為true,否則返回false。第二個函數使用的檢測方法是判斷表頭的前一個結點和後一個結點是否為其本身,如果同時滿足則返回false,否則返回值為true。說多了可能讀者就會沒耐心了,那麼接下來我來看看下面的代碼。

 

#include <stdio.h>

#include <stdlib.h>

#include "list.h"

 

typedef struct _stu

{

    char name[20];

    int num;

    struct list_head list;

}stu;

 

int main()

{

       stu *pstu;

       stu *tmp_stu;

       struct list_head stu_list;

       struct list_head *pos;

       int i = 0;

      

       INIT_LIST_HEAD(&stu_list);

      

       pstu = malloc(sizeof(stu)*5);

      

       for(i=0;i<5;i++)

       {

               sprintf(pstu[i].name,"Stu%d",i+1);

              pstu[i].num = i+1;

              list_add( &(pstu[i].list), &stu_list);

       }

       list_for_each(pos,&stu_list)

       {

              tmp_stu = list_entry(pos, stu, list);

              printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

       }

       if(list_empty(&stu_list))

              printf("使用list_empty()檢測,鏈表為空\n");

       else

              printf("使用list_empty()檢測,鏈表非空\n");

 

       if(list_empty_careful(&stu_list))

              printf("使用list_empty_careful()檢測,鏈表為空\n");

       else

              printf("使用list_empty_careful()檢測,鏈表非空\n");

       free(pstu);

       return 0;

}

 

運行結果為:

 

root@ubuntu:/home/paixu/dlist_node# ./a

student num: 5       student name: Stu5

student num: 4       student name: Stu4

student num: 3       student name: Stu3

student num: 2       student name: Stu2

student num: 1       student name: Stu1

使用list_empty()檢測,鏈表非空

使用list_empty_careful()檢測,鏈表非空

看看代碼就知道如何使用了,接下來看看鏈表的合成。

 

static inline void __list_splice(struct list_head *list,

                                 struct list_head *head)

{

        struct list_head *first = list->next;

        struct list_head *last = list->prev;

        struct list_head *at = head->next;

 

        first->prev = head;

        head->next = first;

 

        last->next = at;

        at->prev = last;

}

這個地方我覺得最好的方法就是使用圖示來進行講解,直觀易懂,如果用文字描述半天還不如讀者看一眼圖。

 

將一個鏈表插入到另外一個鏈表中。不作鏈表是否為空的檢查,由調用者默認保證。因為每個鏈表只有一個頭節點,將空鏈表插入到另外一個鏈表中是沒有意義的。但被插入的鏈表可以是空的。

 

static inline void list_splice(struct list_head *list, struct list_head *head)

{

        if (!list_empty(list))

                __list_splice(list, head);

}

在這種情況下會丟棄list所指向的頭結點,因為兩個鏈表有兩個頭結點,所以我們必須要去掉其中一個頭結點。只要list非空鏈,head無任何限制,該函數就能實現鏈表的合並。

 

static inline void list_splice_init(struct list_head *list,

                                    struct list_head *head)

{

        if (!list_empty(list)) {

                __list_splice(list, head);

                INIT_LIST_HEAD(list);

        }

}

以上函數的功能是將一個鏈表list的有效信息合並到另外一個鏈表head後,重新初始化被去掉的空的鏈表頭。這樣的描述可能不是太好理解,接下來看看一段代碼。

 

#include <stdio.h>

#include <stdlib.h>

#include "list.h"

 

typedef struct _stu

{

    char name[20];

    int num;

    struct list_head list;

}stu;

 

int main()

{

       stu *pstu,*pstu2;

       stu *tmp_stu;

       struct list_head stu_list,stu_list2;

       struct list_head *pos;

 

       int i = 0;

      

       INIT_LIST_HEAD(&stu_list);

       INIT_LIST_HEAD(&stu_list2);

 

       pstu = malloc(sizeof(stu)*3);

       pstu2 = malloc(sizeof(stu)*3);

      

       for(i=0;i<3;i++)

       {

               sprintf(pstu[i].name,"Stu%d",i+1);

              sprintf(pstu2[i].name,"Stu%d",i+4);

              pstu[i].num = i+1;

              pstu2[i].num = i+4;

              list_add( &(pstu[i].list), &stu_list);

              list_add( &(pstu2[i].list), &stu_list2);

       }

       printf("stu_list  鏈表\n");

       list_for_each(pos,&stu_list)

       {

              tmp_stu = list_entry(pos, stu, list);

              printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

       }

       printf("stu_list2 鏈表\n");

       list_for_each(pos,&stu_list2)

       {

              tmp_stu = list_entry(pos, stu, list);

              printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

       }

       printf("stu_list鏈表和stu_list2 鏈表合並以後\n");

       list_splice(&stu_list2,&stu_list);

       list_for_each(pos,&stu_list)

       {

              tmp_stu = list_entry(pos, stu, list);

              printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

       }

 

       free(pstu);

       return 0;

}

 

運行結果為:

 

root@ubuntu:/home/paixu/dlist_node# ./a

stu_list  鏈表

student num: 3       student name: Stu3

student num: 2       student name: Stu2

student num: 1       student name: Stu1

stu_list2 鏈表

student num: 6       student name: Stu6

student num: 5       student name: Stu5

student num: 4       student name: Stu4

stu_list鏈表和stu_list2 鏈表合並以後

student num: 6       student name: Stu6

student num: 5       student name: Stu5

student num: 4       student name: Stu4

student num: 3       student name: Stu3

student num: 2       student name: Stu2

student num: 1       student name: Stu1

 

有了直觀的代碼和運行結果,理解起來也更加的容易了。

 

有了上面的這些操作,但是我們還一直沒有講到我們最終所關心的宿主結構,那麼接下來我們一起來看看我們該如何取出宿主結構的指針呢?這也是我認為linux內核雙向循環鏈表實現最為巧妙的地方了。

 

#define list_entry(ptr, type, member)  \

    ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

看看上面的代碼,發現一個很熟悉的身影(unsigned long)(&((type *)0)->member)),這個我在前一篇博客《C語言的那些小秘密之字節對齊》中已經講解過了,多以在此就不再做過多的講解,如果有不明白的讀者可以回過去看看講解再回過來閱讀。通過(unsigned long)(&((type *)0)->member))我們得出了成員變量member的偏移量,而ptr為指向member的指針,因為指針類型不同的原因,所以我們再次要先進行(char*)的轉換之後再進行計算。所以我們用ptr減去member的偏移量就得到了宿主結構體的指針,這就是一個非常巧妙的地方,這也就使得linux內核雙向循環鏈表能夠區別於傳統鏈表的關鍵所在。可能看到這兒的時候讀者已經感覺非常的枯燥了,但是別放棄,堅持看完,因為雖然這樣的講解枯燥了點,但是非常有用。所以堅持堅持吧!

 

#define list_for_each(pos, head) \

        for (pos = (head)->next; prefetch(pos->next), pos != (head); \

                pos = pos->next)

 

#define __list_for_each(pos, head) \

        for (pos = (head)->next; pos != (head); pos = pos->next)

 

#define list_for_each_prev(pos, head) \

        for (pos = (head)->prev; prefetch(pos->prev), pos != (head); \

                pos = pos->prev)

遍歷是雙循環鏈表的基本操作,head為頭節點,遍歷過程中首先從(head)->next開始,當pos==head時退出,故head節點並沒有訪問,這和鏈表的結構設計有關,通常頭節點都不含有其它有效信息,因此可以把頭節點作為雙向鏈表遍歷一遍的檢測標志來使用。在list_for_each宏中讀者可能發現一個比較陌生的面孔,我們在此就不將prefetch展開了講解了,有興趣的讀者可以自己查看下它的實現,其功能是預取內存的內容,也就是程序告訴CPU哪些內容可能馬上用到,CPU預先其取出內存操作數,然後將其送入高速緩存,用於優化,是的執行速度更快。接下來的__list_for_each()宏和list_for_each_prev()宏就不在此做一一的講解了,和list_for_each()宏類似。 就是遍歷的方向有所改變或者不使用預取。

 

通過上面的講解和前面那麼多的代碼,相信讀者這個時候對於list_for_each()已經不再感到陌生了。上面的但三個遍歷鏈表的宏都類似,繼續往下看。

 

#define list_for_each_safe(pos, n, head) \

        for (pos = (head)->next, n = pos->next; pos != (head); \

                pos = n, n = pos->next)

以上list_for_each_safe()宏也同樣是用於遍歷的,不同的是裡邊多出了一個n暫存pos下一個節點的地址,避免了因為pos節點被釋放而造成的斷鏈,這也就體現出了safe。也就是說你可以遍歷完當前節點後將其刪除,同時可以接著訪問下一個節點,遍歷完畢後就只剩下一個頭節點。當然這有一個最為典型的應用,那就是我們在多進程編程時候,多個進程等待在同一個等待隊列上,若事件發生時喚醒所有進程,則可以喚醒後將其依次從等待隊列中刪除。

 

#include <stdio.h>

#include <stdlib.h>

#include "list.h"

 

typedef struct _stu

{

    char name[20];

    int num;

    struct list_head list;

}stu;

 

int main()

{

       stu *pstu;

       stu *tmp_stu;

       struct list_head stu_list;

       struct list_head *pos,*n;

 

       int i = 0;

      

       INIT_LIST_HEAD(&stu_list);

 

       pstu = malloc(sizeof(stu)*3);

      

       for(i=0;i<3;i++)

       {

               sprintf(pstu[i].name,"Stu%d",i+1);

              pstu[i].num = i+1;

              list_add( &(pstu[i].list), &stu_list);

       }

       printf("通過list_for_each_safe()遍歷使用list_del(pos)刪除結點前\n");

       list_for_each_safe(pos, n, &stu_list)

       {

 

              tmp_stu = list_entry(pos, stu, list);

              printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

              list_del(pos);

       }

       printf("通過list_for_each()遍歷使用list_del(pos)刪除結點後\n");

       list_for_each(pos,&stu_list)

       {

              tmp_stu = list_entry(pos, stu, list);

              printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

       }

 

       free(pstu);

       return 0;

}

 

運行結果為:

 

root@ubuntu:/home/paixu/dlist_node# ./a

通過list_for_each_safe()遍歷使用list_del(pos)刪除結點前

student num: 3       student name: Stu3

student num: 2       student name: Stu2

student num: 1       student name: Stu1

通過list_for_each()遍歷使用list_del(pos)刪除結點後

 

讀者可以結合運行結果再去閱讀上面的文字描述部分。

 

如果只提供對list_head結構的遍歷操作是遠遠不夠的,我們希望實現的是對宿主結構的遍歷,即在遍歷時直接獲得當前鏈表節點所在的宿主結構項,而不是每次要同時調用list_for_each()和list_entry()。為此Linux特地提供了list_for_each_entry()宏來實現

 

#define list_for_each_entry(pos, head, member)                                \

        for (pos = list_entry((head)->next, typeof(*pos), member);        \

             prefetch(pos->member.next), &pos->member != (head);         \

             pos = list_entry(pos->member.next, typeof(*pos), member))

第一個參數為傳入的遍歷指針,指向宿主數據結構,第二個參數為鏈表頭,為list_head結構,第三個參數為list_head結構在宿主結構中的成員名。有時候做過多的講解反而沒有看看代碼的效果好,我們還是用段代碼來說明下吧。

 

#include <stdio.h>

#include <stdlib.h>

#include "list.h"

 

typedef struct _stu

{

    char name[20];

    int num;

    struct list_head list;

}stu;

 

int main()

{

       stu *pstu;

       stu *tmp_stu;

       struct list_head stu_list;

       struct list_head *pos,*n;

 

       int i = 0;

      

       INIT_LIST_HEAD(&stu_list);

 

       pstu = malloc(sizeof(stu)*3);

      

       for(i=0;i<3;i++)

       {

               sprintf(pstu[i].name,"Stu%d",i+1);

              pstu[i].num = i+1;

              list_add( &(pstu[i].list), &stu_list);

       }

       list_for_each_entry(tmp_stu, &stu_list, list)

              printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

 

       free(pstu);

       return 0;

}

 

運行結果為:

 

root@ubuntu:/home/paixu/dlist_node# ./a

student num: 3       student name: Stu3

student num: 2       student name: Stu2

student num: 1       student name: Stu1

 

如果讀者一開始對於文字描述感到陌生的話,那麼就再次結合代碼去閱讀。

 

接下來再來看看最後幾個。

 

#define list_for_each_entry_reverse(pos, head, member)                        \

        for (pos = list_entry((head)->prev, typeof(*pos), member);        \

             prefetch(pos->member.prev), &pos->member != (head);         \

             pos = list_entry(pos->member.prev, typeof(*pos), member))

 

#define list_prepare_entry(pos, head, member) \

        ((pos) ? : list_entry(head, typeof(*pos), member))

 

#define list_for_each_entry_continue(pos, head, member)                 \

        for (pos = list_entry(pos->member.next, typeof(*pos), member);        \

             prefetch(pos->member.next), &pos->member != (head);        \

             pos = list_entry(pos->member.next, typeof(*pos), member))

 

#define list_for_each_entry_safe(pos, n, head, member)                        \

        for (pos = list_entry((head)->next, typeof(*pos), member),        \

                n = list_entry(pos->member.next, typeof(*pos), member);        \

             &pos->member != (head);                                         \

             pos = n, n = list_entry(n->member.next, typeof(*n), member))

以上幾個與list_for_each_entry類似,只是其中略有差別,list_prepare_entry()中含有prefetch(),它的作用在上面已經講解,有什麼疑惑可以返回去閱讀下,在此不再做過多的講解;list_for_each_entry_continue()和list_for_each_entry()的區別主要是list_for_each_entry_continue()可以不從鏈表頭開始遍歷,而是從已知的某個pos結點的下一個結點開始遍歷。在某些時候如果不是從頭結點開始遍歷,那麼為了保證pos的初始值有效,必須使用list_prepare_entry()。其含義就是如果pos非空,那麼pos的值就為其本身,如果pos為空,那麼就從鏈表頭強制擴展一個虛pos指針,讀者自己分析list_prepare_entry()的實現就明白了。list_for_each_entry_safe()要求調用者另外提供一個與pos同類型的指針n,在for循環中暫存pos下一個節點的宿主結構體的地址,避免因pos節點被釋放而造成的斷鏈。

 

到此我們linux內核雙向循環鏈表的旅程就結束了,下一篇我們將開始一個新的旅程。由於本人水平有限,博客中的不妥或錯誤之處在所難免,殷切希望讀者批評指正。同時也歡迎讀者共同探討相關的內容,如果樂意交流的話請留下你寶貴的意見。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved