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相关文档。