0x00 前言
linux中的雙向鏈表和傳統的雙向鏈表不太一樣,是注入了抽象封裝靈魂的鏈表,
他不是把將數據結構塞入鏈表,而是將鏈表節點塞入數據
之前在windows裏就見識過_ETHREAD結構.....的雙向鏈表,但苦於沒有源碼,所以在linux裏好好康一康。
鏈表的頭文件是list.h(include\linux\list.h),我們重此來探究
參考文章:
https://blog.csdn.net/xu_ya_fei/article/details/49744699
https://www.cnblogs.com/wangzahngjun/p/5556448.html
https://wenku.baidu.com/view/5df5736fa0116c175e0e4817.html
https://blog.csdn.net/wanshilun/article/details/79747710
https://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.html
0x01 傳統雙向鏈表
詳見此文http://data.biancheng.net/view/8.html
就不在此贅述了。
0x02 基礎數據結構
1. 內核鏈表的定義
struct list_head {
struct list_head *next, *prev;
};
可以看出來,linux內核裏並沒有數據域,這正是體現抽象思想的表現,把雙向鏈表的共性(前驅指針,後繼指針)作爲一個基本數據結構。
所以我們就可以如下使用
struct My_List{
void* My_Data;
struct list_head My_List;
};
1) list_head 是一種侵入式鏈表,數據附加在鏈表之上,使得 list_head 數據結構是通用的,使用起來就不需要考慮節點數據的類型。
2)list_head結構體可以添加到結構體的任何位置
3)可以爲 list_head 命名
4)可以添加多個list_head鏈表
2.是否有效
#ifdef CONFIG_DEBUG_LIST
extern bool __list_add_valid(struct list_head *new,
struct list_head *prev,
struct list_head *next);
extern bool __list_del_entry_valid(struct list_head *entry);
#else
static inline bool __list_add_valid(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
return true;
}
static inline bool __list_del_entry_valid(struct list_head *entry)
{
return true;
}
#endif
通常在進行插入和刪除時會首先判斷是否有效
3.WRITE_ONCE && READ_ONCE
在某些情況下CPU對內存中變量讀寫並不是一次完成的,這可能會出現競爭。而READ_ONCE和WRITE_ONCE實現對變量一次性讀取和一次性寫入。
詳情可以看這篇文章:https://blog.csdn.net/cloudblaze/article/details/51676139
4. offsetof && container_of
在下文 "遍歷" 時詳細說明。
0x03 基礎函數
1. 聲明與初始化
上一段描述了鏈表的鏈節點,在LIST_HEAD這個宏中描述了頭節點
在源碼中可以看到倆個宏:
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
第一個:&(name) 前驅指針,後繼指針都是指向自己的指針
第二個:調用了第一個的初始化,使用宏 LIST_HEAD_INIT 進行初始化,這會使用變量name 的地址來填充prev和next 結構體的兩個變量
兩者的使用的不同:第一個宏是初始化,第二個宏是聲明+初始化
除了宏以外還有Linux還提供了一個INIT_LIST_HEAD宏用於運行時初始化鏈表
static inline void
INIT_LIST_HEAD(struct list_head *list)
{
list->next = list->prev = list;
}
2. 插入
在linux內核中一共有倆個插入的函數
static inline void list_add()
static inline void list_add_tail()
list_add在鏈表頭插入,實現棧的功能
list_add_tail在鏈表尾插入,實現隊列的功能
在傳統的雙向鏈表中的插入:
前插
int DlinkIns(DoubleList L,int i,ElemType e)
{
DNode *s,*p;
…/*先檢查待插入的位置i是否合法(實現方法同單鏈表的前插操作)*/
…/*若位置i合法,則讓指針p指向它*/
s=(DNode *)malloc(sizeof(DNode));
if(s)
{
s->data=e;
s->prior=p->prior;p->prior->next=s;
s->next=p;p->prior=s;
return TRUE;
}
else
teturn fALSE;
}
後插也是一堆,這裏略
咱們來看linux內核如何實現:
先設置了一個基礎: __list_add
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
if (!__list_add_valid(new, prev, next))
return;
next->prev = new;
new->next = next;
new->prev = prev;
WRITE_ONCE(prev->next, new);
}
而屬於前插(棧)還是後插(隊列)這由參數來區分:
list_add:
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
list_add_tail:
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}
代碼本身沒有難度,但是封裝的靈魂,讓人很驚歎,__list_add作爲一個基礎操作只管增加鏈節點,不管插入的位置。
而list_add調用__list_add,並確定位置,插在head和head->next之間,實現棧的操作;
list_add_tail調用__list_add,並確定位置,插在head->prev和head之間,實現隊列的操作。
那如何找得將要插入的位置呢?在下文遍歷中會詳細說明。
3. 刪除
同樣貫徹封裝的思想,先封裝刪除操作:
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
WRITE_ONCE(prev->next, next);
}
其中的WRITE_ONCE:把 next寫入prev->next ,在某些情況下CPU對內存中變量讀寫並不是一次完成的,這可能會出現競爭。WRITE_ONCE實現對變量一次性寫入。
list_del:
static inline void __list_del_entry(struct list_head *entry)
{
if (!__list_del_entry_valid(entry))
return;
__list_del(entry->prev, entry->next);
}
static inline void list_del(struct list_head *entry)
{
__list_del_entry(entry);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}
注意:其中在使用list_del時,刪除操作會把所有要刪除的節點(point)的 next 和 prev都指向一個固定的值(LIST_POSITION1和LIST_POSITION2),因爲LIST_POSITION1和LIST_POSITION2的宏定義了一個不可訪問的地址,所以當使用
point = point->next時會因不可訪問而發生頁錯誤。
那爲什麼不把next 和 prev直接free掉呢?
LIST_POSITION1和LIST_POSITION2指向的是內核中的位置,在用戶態給0,這樣list.h可以直接移植到用戶態來用,筆者這裏覺得沒有直接釋放掉,和便於移植有關。
另外,刪除還有另一種情況:刪除列表項並清除'prev'指針
static inline void __list_del_clearprev(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
entry->prev = NULL;
}
。這是網絡代碼中使用的一種特殊用途的列表清除方法,用於按cpu分配的列表,我們不希望產生常規list_del_init()的額外*WRITE ONCE()開銷。使用這個*的代碼需要檢查節點'prev'指針,而不是調用list_empty()。
安全刪除
static inline void list_del_init(struct list_head *entry)
{
__list_del_entry(entry);
INIT_LIST_HEAD(entry);
}
static inline void __list_del_entry(struct list_head *entry)
{
if (!__list_del_entry_valid(entry))
return;
__list_del(entry->prev, entry->next);
}
相對於list_del的簡單調用,list_del_init,不止調用__list_del,之後還會將它初始化(將entry節點的前繼節點和後繼節點都指向entry本身)。
4.空 && 第一 && 最後 && 是否單鏈
這個不多說,看源碼,有兩種
static inline int list_empty(const struct list_head *head)
{
return READ_ONCE(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);
}
不止判斷 後繼 是否是自己,還判斷 前驅 本身和 後繼 一不一樣。
linux還設置了判斷是否是第一個節點的函數:
/**
* list_is_first -- tests whether @list is the first entry in list @head
* @list: the entry to test
* @head: the head of the list
*/
static inline int list_is_first(const struct list_head *list,
const struct list_head *head)
{
return list->prev == head;
}
對應的,判斷是否是最後一個節點:
static inline int list_is_last(const struct list_head *list,
const struct list_head *head)
{
return list->next == head;
}
判斷是否是單個鏈:
static inline int list_is_singular(const struct list_head *head)
{
return !list_empty(head) && (head->next == head->prev);
}
5. 移動
把鏈表A的某節點插入鏈表B(前插,後插),說白了就是利用了刪除與插入:
前插:
static inline void list_move(struct list_head *list, struct list_head *head)
{
__list_del_entry(list);
list_add(list, head);
}
後插:
static inline void list_move_tail(struct list_head *list,
struct list_head *head)
{
__list_del_entry(list);
list_add_tail(list, head);
}
linux也提供了 在一個同一個鏈表中的移動:把鏈表的前一部分,放到鏈表的後部:
static inline void list_bulk_move_tail(struct list_head *head,
struct list_head *first,
struct list_head *last)
{
first->prev->next = last->next;
last->next->prev = first->prev;
head->prev->next = first;
first->prev = head->prev;
last->next = head;
head->prev = last;
}
1)head 參數 指向鏈表頭,
2)first 指向需要移動的多個連續節點的第一個節點,
3)last 指向需要移動的多個 連續節點的最後一個節點。
(環的一部分)
6.替換
list_replace:只把old替換成new
static inline void list_replace(struct list_head *old,
struct list_head *new)
{
new->next = old->next;
new->next->prev = new;
new->prev = old->prev;
new->prev->next = new;
}
list_replace_init:把old替換成new,並初始化old(要被替換的元素)
static inline void list_replace_init(struct list_head *old,
struct list_head *new)
{
list_replace(old, new);
INIT_LIST_HEAD(old);
}
7.交換(兩個鏈表間)
用entry2替換entry1,並在entry2的位置重新添加entry1:
list_swap:
static inline void list_swap(struct list_head *entry1,
struct list_head *entry2)
{
struct list_head *pos = entry2->prev;
list_del(entry2);
list_replace(entry1, entry2);
if (pos == entry1)
pos = entry2;
list_add(entry1, pos);
}
8.反轉 (同一個鏈表內)
不同於上述的交換,反轉是同一個鏈表內,前後節點的交換:
list_rotate_left:(前後相連)
static inline void list_rotate_left(struct list_head *head)
{
struct list_head *first;
if (!list_empty(head)) {
first = head->next;
list_move_tail(first, head);
}
}
list_rotate_to_front:(將鏈節點提到最前面)
static inline void list_rotate_to_front(struct list_head *list,
struct list_head *head)
{
/*
* Deletes the list head from the list denoted by @head and
* places it as the tail of @list, this effectively rotates the
* list so that @list is at the front.
*/
list_move_tail(head, list);
}
9. 剪切
貫徹抽象思想,暫時不管位置,先實現剪切操作
__list_cut_position:(在entry處剪切,並把entry也剪掉)
static inline void __list_cut_position(struct list_head *list,
struct list_head *head, struct list_head *entry)
{
struct list_head *new_first = entry->next;
list->next = head->next;
list->next->prev = list;
list->prev = entry;
entry->next = list;
head->next = new_first;
new_first->prev = head;
}
設置一個新的鏈表list,通過head將第一個節點到entry節點(包括它本身),轉到新鏈表list裏,再將head 鏈表重新指向 entry 的下一個節點。
list_cut position:
static inline void list_cut_position(struct list_head *list,
struct list_head *head, struct list_head *entry)
{
if (list_empty(head))
return;
if (list_is_singular(head) &&
(head->next != entry && head != entry))
return;
if (entry == head)
INIT_LIST_HEAD(list);
else
__list_cut_position(list, head, entry);
}
list_cut_position() 函數用於將一個鏈表切成兩個鏈表。參數 list 指向一個新的鏈表, 參數 head 指向被拆開的鏈表 (原始鏈表),entry 參數指向拆開的位置。函數首先調用 list_empty() 函數確定 head 鏈表是不是空鏈表,如果是則直接返回;如果不是空鏈表, 則調用 list_is_singular() 函數確定 head 鏈表是不是單一節點的鏈表,如果是則判斷 head 鏈表的 next 不指向 entry 參數,並且 head 不是 entry,那麼直接返回;反之 不是,則判斷 entry 與 head 的關係,如果 entry 就是 head,那麼代表新鏈表是空鏈表, 那麼直接調用 INIT_LIST_HEAD() 函數初始化 list 鏈表;反之調用 __list_cut_position() 將 head 鏈表拆成兩段,第一個節點到 entry 節點的鏈表使用 list 指定,entry 到最後一個 節點通過 head 指向。
list_cut_before: (在entry處剪切,不把entry剪掉)
static inline void list_cut_before(struct list_head *list,
struct list_head *head,
struct list_head *entry)
{
if (head->next == entry) {
INIT_LIST_HEAD(list);
return;
}
list->next = head->next;
list->next->prev = list;
list->prev = entry->prev;
list->prev->next = list;
head->next = entry;
entry->prev = head;
}
list_cut_before() 函數用於將 head 拆分成兩個鏈表,新鏈表從 head 的第一個 節點到 entry 的前一個節點,拆分之後的 head 鏈表的第一個節點變成了 entry 節點。 函數首先檢查 head->next 與 entry 之間的關係,如果相等,代表 list 鏈表是一個 空鏈表,因此調用 INIT_LIST_HEAD() 初始化 list 鏈表;如果不相等,就將 entry 之前的鏈表拆分給 list 鏈表,head 鏈表則維護從 entry 之後的鏈表。
10. 合併
說白了就是把整個鏈表插入:
基本插入操作(封裝的思想)
static inline void __list_splice(const struct list_head *list,
struct list_head *prev,
struct list_head *next)
{
struct list_head *first = list->next;
struct list_head *last = list->prev;
first->prev = prev;
prev->next = first;
last->next = next;
next->prev = last;
}
list_splice:前插
static inline void list_splice(const struct list_head *list,
struct list_head *head)
{
if (!list_empty(list))
__list_splice(list, head, head->next);
}
list_splice_tail:後插
static inline void list_splice_tail(struct list_head *list,
struct list_head *head)
{
if (!list_empty(list))
__list_splice(list, head->prev, head);
}
圖解:(一位大佬的圖)
當list1被掛接到list2之後,作爲原表頭指針的list1的next、prev仍然指向原來的節點,爲了避免引起混亂,Linux提供了一個list_splice_init()函數
static inline void list_splice_init(struct list_head *list,
struct list_head *head)
{
if (!list_empty(list)) {
__list_splice(list, head, head->next);
INIT_LIST_HEAD(list);
}
}
該函數在將list合併到head鏈表的基礎上,調用INIT_LIST_HEAD(list)將list設置爲空鏈
對應的還有 list_splice_tail_init ,除了變成了後插,沒有別的區別。
11. 遍歷
對於鏈表,遍歷是很重要的。在上文中,在添加操作時,如何找到將要插入的位置?在這裏得到解答,linux中簡單的宏實現遍歷:
以下是向後遍歷
#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; pos != (head); pos = pos->prev)
pos: 指向list_head的指針。
head :需要遍歷的鏈表的鏈表頭。
那我們可以找到list_head的位置,但怎麼找到對應的數據呢?我們需要另一個宏list_entry
list_entry:
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
其中
ptr是指向該數據中list_head成員的指針,也就是存儲在鏈表中的地址值,
type是數據項的類型,
member則是數據項類型定義中list_head成員的變量名
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
其實它的語法很簡單,只是一些指針的靈活應用,它分兩步:
第一步,首先定義一個臨時的數據類型(通過typeof( ((type *)0)->member )獲得)與ptr相同的指針變量__mptr,然後用它來保存ptr的值。
第二步,用(char *)__mptr減去member在結構體中的偏移量,得到的值就是整個結構體變量的首地址(整個宏的返回值就是這個首地址)
圖解:
ok,那問題來了,這個偏移咋算的,我們跟進offseto().
在include\linux\stddef.h裏
那麼我們可以假設在0地址分配了一個結構體變量struct TYPE a,然後定義結構體指針變量p並指向a(struct TYPE *p = &a),如此我們就可以通過 &(p->MEMBER) 獲得成員MEMBER的地址。由於a的首地址爲0x0,所以獲取的MEMBER的地址就是在結構體中的偏移,最後在強制轉換成(size_t)類型。這樣就拿到了list_head真正的偏移。
這樣我們一路逆向回去,就可以找到節表的位置(data域開始的位置)。
我們拿代碼驗證一下:
#include<stdio.h>
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
struct My_Struct {
int data;
char My_list1;
char My_list2;
};
int main()
{
printf("My_list1相對My_Struct的位移:%d",offsetof(struct My_Struct, My_list1));
printf("My_list2相對My_Struct的位移:%d", offsetof(struct My_Struct, My_list2));
return 0;
}
運行結果:
4和5 沒問題。
0x04 總結
第一次看內核的源碼,感覺好爽,等有時間自己實現一下linux的鏈表,(寫好了再來貼上),有不對的地方望路過的大佬斧正。