1 鏈表的數據結構
鏈表是通過指針將一系列數據節點連接成一條數據鏈的數據結構。相對於數組,鏈表具有更好的動態性。且對增加刪除操作效率高於數組。
1.1 單鏈表
單鏈表是最簡單的一類鏈表,特點是僅有一個指針域指向後續節點。數據結構如圖所示:
1.2 雙鏈表
雙鏈表設計了兩個指針域,前驅和後繼分別指向上一個節點和下一個節點。雙鏈表可以從兩個方向遍歷,其數據結構圖如下:
1.3 循環鏈表
循環鏈表的特點是尾節點的後繼指向首節點,
2 Linux的內核鏈表實現(3.4)
Linux kernel中的內核鏈表實現位於include/linux/list.h
中,其實現爲循環雙向鏈表,且帶有表頭指針。
2.1數據結構定義
頭結點的定義位於include/linux/type.h
struct list_head {
struct list_head *next, *prev;
};
這裏定義的鏈表節點沒有數據域:由於Linux內核中,不是在鏈表節點包含數據域,而是在數據域中包含鏈表節點。即鏈表節點包含一個struct list_head
的節點。
2.2鏈表的申明和初始化
Linux只是定義了鏈表節點,而未定義專門的鏈表頭。鏈表頭式通過如下宏定義初始化的。
#define LIST_HEAD_INIT(name) { &(name), &(name) } //頭節點和尾節點都指向自己。
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
內核中還提供了一個方法來初始化鏈表
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
2.3 鏈表判空
內核中提供了兩個方法判空的,list_empty
簡單的判斷當前節點的next節點是不是指向自己本身;list_empty_careful
在上面的基礎上,還判斷了當前節點的prev和當前節點的next指向同一個節點。
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);
}
2.4 鏈表插入
插入操作有兩種:在表頭插入和在表尾插入。
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
2.5 刪除
list刪除節點後,當前節點的prev、next指針分別被設爲LIST_POSITION2
和LIST_POSITION1
兩個特殊值,這樣設置是爲了保證不在鏈表中的節點項不可訪問鏈表中的節點–對LIST_POSITION1
和LIST_POSITION2
的訪問都將引起頁故障;
list_del_init 將節點從鏈表中刪除之後,調用INIT_LIST_HEAD()
將節點置爲空鏈狀態。
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
static inline void __list_del_entry(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
}
static inline void list_del(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}
static inline void list_del_init(struct list_head *entry)
{
__list_del_entry(entry);
INIT_LIST_HEAD(entry);
}
2.6 移動
內核鏈表提供了將原來屬於一個鏈表的節點移動到另一個鏈表的操作。
list_move
會把節點從第一個鏈表刪除,插入到另一個鏈表頭部。
list_move_tail
會把節點從第一個鏈表刪除,插入到另一個鏈表尾部。
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);
}
2.7 鏈表合併
鏈表合併,也就是整個鏈表的插入功能,list_splice
:在list1非空時,list1鏈表的內容將被掛接在list2鏈表上,位於list2和list2.next(原list2表的第一個節點)之間,也就是list2頭部,原表頭指針的list1的next、prev仍然指向原來的節點。如下(虛箭頭爲next指針):
list_splice_init
在list_splice
的基礎上,調用INIT_LIST_HEAD(list1)
將list1設置爲空鏈。
同時提供了list_splice_tail
,用於在list1非空時,把list1插入到list2尾部。也提供了list_splice_tail_init
,在插入基礎上,調用INIT_LIST_HEAD(list1)
將list1設置爲空鏈。
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;
}
static inline void list_splice(const struct list_head *list,
struct list_head *head)
{
if (!list_empty(list))
__list_splice(list, head, head->next);
}
static inline void list_splice_tail(struct list_head *list,
struct list_head *head)
{
if (!list_empty(list))
__list_splice(list, head->prev, head);
}
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);
}
}
static inline void list_splice_tail_init(struct list_head *list,
struct list_head *head)
{
if (!list_empty(list)) {
__list_splice(list, head->prev, head);
INIT_LIST_HEAD(list);
}
}
2.8 遍歷
Linux鏈表將遍歷操作抽象成幾個宏。
1、Linux提供了由鏈表節點到數據項的宏list_entry
:
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
#define list_first_entry(ptr, type, member) \
list_entry((ptr)->next, type, member)
container_of
的宏定義位於include/linux/kernel.h
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
offsetof
宏定義在include/linux/stddef.h
中
#ifdef __compiler_offsetof
#define offsetof(TYPE,MEMBER) __compiler_offsetof(TYPE,MEMBER)
#else
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#endif
這裏使用的是一個利用編譯器技術的小技巧,即先求得結構成員在與結構中的偏移量,然後根據成員變量的地址反過來得出屬主結構變量的地址。
container_of()
和offsetof()
並不僅用於鏈表操作,這裏最有趣的地方是((type *)0)->member
,它將0地址強制”轉換”爲type結構的指針,再訪問到type結構中的member成員。在container_of
宏中,它用來給typeof()提供參數(typeof()是gcc的擴展,和sizeof()類似,typeof返回的是傳入對象的數據類型),以獲得member成員的數據類型;在offsetof()中,這個member成員的地址實際上就是type數據結構中member成員相對於結構變量的偏移量。
對於給定一個結構,offsetof(type,member)
是一個常量,list_entry()
正是利用這個不變的偏移量來求得鏈表數據項的變量地址。如下圖所示:
2.遍歷的宏定義list_for_each()
和list_for_each_entry
,前者的pos指向節點,用於遍歷節點;後者的pos指向節點對應的數據,用於遍歷節點,並獲取節點的數據成員。
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)
#define list_for_each_entry(pos, head, member) \
for (pos = list_entry((head)->next, typeof(*pos), member); \
&pos->member != (head); \
pos = list_entry(pos->member.next, typeof(*pos), member))
list_for_each_entry_continue
用於遍歷不是從鏈表頭開始,而是從已知的某個節點pos開始的情況。
list_for_each_entry_reverse
反向遍歷。
2.9 其他
list_replace(struct list_head *old, struct list_head *new)
將old替換爲new
list_is_last(const struct list_head *list,
測試list是否是head鏈表的最後一項。
const struct list_head *head)
3 HASH鏈表hlist
hlist與list的區別是hlist是單指針表頭鏈表,如下圖:
3.1 hlist的初始化
struct hlist_head {
struct hlist_node *first;
};
struct hlist_node {
struct hlist_node *next, **pprev;
};
#define HLIST_HEAD_INIT { .first = NULL }
#define HLIST_HEAD(name) struct hlist_head name = { .first = NULL }
#define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL)
static inline void INIT_HLIST_NODE(struct hlist_node *h)
{
h->next = NULL;
h->pprev = NULL;
}
hlist的其他操作與list類似,只是因爲表頭和節點的數據結構不同,插入操作如果發生在表頭和首節點之間,以往的方法就行不通了:表頭的first指針必須修改指向新插入的節點,卻不能使用類似list_add()這樣統一的描述。爲此,hlist節點的prev不再是指向前一個節點的指針,而是指向前一個節點(可能是表頭)中的next(對於表頭則是first)指針(struct list_head **pprev)
,從而在表頭插入的操作可以通過一致的*(node->pprev)
訪問和修改前驅節點的next(或first)指針。
4 read-copy update(RCU)
RCU通過延遲寫操作來提高同步性能。list中xxx_rcu
的方法就是使用了此特性,具體可以查看RCU相關文檔。