文章目录
前言
在我们经常使用的双链表中,next和prev指针作为成员嵌入在结构体内部,结构常常如下所示:
struct student {
char name[16];
int id;
struct student *next, *prev;
};
这样定义带来的问题在于我们对链表进行操作时,参数为特定的student类型,而当我们有teacher的需求时,我们还需要定义针对teacher这一特定类型的链表操作。重复书写代码。
在linux内核中,为了定义一个更加通用的结构,双向循环链表的定义与我们平常使用大有不同。用法如下:
struct list_head {
struct list_head *next, *prev;
};
struct student
{
char name[16];
int id;
struct list_head list;
};
在这样的链表定义下我们对链表的使用更加灵活,而不用对某一特定场景定义单独的指针和具体针对具体某一结构为参数的链表操作。通用性大大增强。我们还可以定义另外的链表如下:
struct user
{
char name[16];
char passwd[16];
struct list_head list;
}
此外,linux内核中针对双向循环链表的操作(不包括链表的遍历)都在O(1)的时间复杂度中完成,非常值得我们学习借鉴。
一、链表基本操作
链表初始化
本地变量的静态初始化
#define LIST_HEAD_INIT(name) { &(name), &(name) }
具体用法表现在上文中student结构体中可以为:
struct student = {
.name="jack",
.id=1,
.list=LIST_HEAD_INIT(list)
}
其中,初始化链表部分宏展开为:list = {&(list), &(list)}
,也就是初始化两个指针指向自身。
动态申请变量的初始化
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
具体用法表现在上文中student结构体中可以为:
struct student* pStu = (struct student*)malloc(sizeof(struct student));
pStu->name="jack";
pStu->id=1;
INIT_LIST_HEAD(&pStu->list);
这里不可以用静态方法初始化的原因在于使用list = { next, prev }
的初始化形式必须在变量定义时进行。大家可以在自己的编译器上尝试一下。
定义链表头节点
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
定义名为head的头节点可以简单实现为:static LIST_HEAD(head);
其宏展开之后为:static list_head head = { &(head), &(head)};
头节点定义中会定义一个单独的list_head
结构的头节点。其不在任何的更大的结构体内部。所以我们在linux内核中使用的双向循环链表的具体表现为:
这里大家可能会有疑问,我们定义链表就是要在某个节点操作其父结构中的属性,这如何做到,我们后面在链表使用相关宏中会详细解释。我们先看基本操作的具体实现。
链表添加节点
添加操作分为添加到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;
}
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);
}
其中,最上面的__list_add
方法用于在prev
和next
之间插入新节点new
,时间复杂度O(1)
,而从头节点之前和之后插入节点的区别仅在于调用__list_add
方法时传入的节点。插入头节点之后,则添加的位置介于头节点head
和head->next
之间。插入头节点之前,则添加的位置介于头节点head
和head->prev
之间。
链表删除节点
链表删除只需要传入entry的list_head指针即可,还可以选择再次初始化被删除节点函数,复杂度O(1)
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = 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->prev, entry->next);
INIT_LIST_HEAD(entry);
}
这里删除节点之后的指针初始化操作是为了DEBUG
,简单理解使用没有初始化的指针时,这个很小的地址会引发page fault
从而识别到具体错误
# define POISON_POINTER_DELTA 0
#define LIST_POISON1 ((void *) 0x00100100 + POISON_POINTER_DELTA)
#define LIST_POISON2 ((void *) 0x00200200 + POISON_POINTER_DELTA)
链表替换节点
使用新节点new替换旧节点old,可选的是否重新初始化旧节点,复杂度O(1)
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;
}
static inline void list_replace_init(struct list_head *old,
struct list_head *new)
{
list_replace(old, new);
INIT_LIST_HEAD(old);
}
链表节点移动
将list节点移动到head链表之中,位置可选head之后和head之前
static inline void list_move(struct list_head *list, struct list_head *head)
{
__list_del(list->prev, list->next);
list_add(list, head);
}
static inline void list_move_tail(struct list_head *list,
struct list_head *head)
{
__list_del(list->prev, list->next);
list_add_tail(list, head);
}
先删除从原始链表中节点再重新添加到新的链表中。由于两个操作都是O(1)复杂度,所以复杂度仍旧是O(1)
链表循环左移一位
链表有效节点循环左移一位,不包括head,时间复杂度依旧O(1)
static inline int list_empty(const struct list_head *head)
{
return head->next == head;
}
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);
}
}
这里需要明白一点,移动前后链表具体位置变化如下图所示:
实际操作中,head
指针始终作为头节点,它是循环双链表的一部分,但它不作为有效节点存在。一般有效节点通常有父结构待操作,内含struct list_head
结构体。first
节点被取出后其两个指针并未变化,图中并未给出。
源码中没有给出链表循环右移一次的函数接口。但不难理解,我们只需要取出last
节点,插入head
之后即可。类似以下内容:
if (!list_empty(head)) {
last= head->prev;
list_move(last, head);// 先从原始链表删除,再添加到head之后
}
举一反一成功,就不给出图了,读者可自行画图验证。
链表合并
将list链表有效节点合并到head链表中,合并位置和是否重新初始化list链表都可选。
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);// 合并位置为head之后。
}
这里我们简单画一下合并过程如下图:
图中,list作为头节点其指针并未改变,图中未给出。简单总结就是取出待添加的链,通过操作新链其头和尾节点的指针实现添加一整条链到head
的效果。
下面给出其余接口:包括合并到head之后,合并后重新初始化list节点。
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);
}
}
这些接口相信大家都能很快理解。
目前为止,所有基于双向循环链表的操作时间复杂度都在O(1)级别,其中一些内容很值得我们学习借鉴。
二、链表使用及相关宏
list_entry宏
list_entry
宏用于通过结构体中list_head
的指针来获取结构体起始地址来访问整个结构体。
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
用法如下:struct student *stu=list_entry(head->next, struct student, list)
其中,container_of宏定义如下:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
我们挨个解析,offsetof
宏用于计算结构体成员MEMBER
距离结构体TYPE
类型起始地址的距离,其中,MEMBER
是TYPE
的成员变量。通过将0地址赋值给TYPE*
并取其MEMBER
的地址即可知道偏移。父类型地址为0,成员的地址也就是成员距离父结构体的偏移。这里我们并未去访问0地址指针的任何内容,只是取其成员地址,所以不会引发问题。
container_of
宏第一行指示作为类型检查用。获取type->member
指针类型并通过临时变量赋值检查是否类型相同。第二行则先将其转为字符指针并偏移成员到结构体初始地址大小的地址即为父结构体的地址。强制类型转换为父类型指针。
最后,({ expr1; expr2; })有返回值,它的返回值就是expr2的值。综上,该宏才可以正确返回父类型的指针。
链表遍历
链表遍历:
#define list_for_each(pos, head) \
for (pos = (head)->next; prefetch(pos->next), pos != (head); \
pos = pos->next)
这里prefetch是编译器的预取优化。可以忽略,简单理解为普通循环会在一次遍历之后重新取值,而预取优化则会增加命中率,增加效率。
用法如下:
struct list_head temp;
list_for_each(temp, head){
// 此时temp为有效链表节点指针
list_...(temp)通过接口操作链表
...
}
通过链表取第一个有效元素,即第一个有效节点父结构体指针
#define list_first_entry(ptr, type, member) \
list_entry((ptr)->next, type, member)
通过链表遍历所有父结构体
#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))
用法如下:
struct student *temp;
list_for_each_entry(temp, head, list){
// temp此时作为遍历变量,指向每一个父结构体
temp->... = ...;
... = temp->...;
...
}
总结
本文详细分析介绍linux内核中双向循环链表的具体实现及操作。后面会介绍kfifo等数据结构以及内核其余内容。
如有问题,欢迎私信批评指正。