Linux 链表简介
链表简述
链表初识
链表是什么
链表是一种存放和操作可变数量的元素的一种常见的数据结构,即节点可变动。链表将一些数据元素通过“链”连接在一起,是线性表的一种重要实现方式。
链表结构主要分为两个区域:数据域和指针域。顾名思义,数据域用与存放相关的数据。而指针域用来保证整条链表的联系。
链表和数组的对比
相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。
数组长度要事先声明、使用的内存连续、不能动态缩减大小。
优点 | 缺点 | 使用场景 | |
---|---|---|---|
数组 | 因为数据是连续存储的,内存地址连续,所以在查找数据的时候效 率比较高 | 在存储之前,我们需要申请一块连续的内存空间,并且在编译的时候就必须确定好它的空间的大小。在运行的时候空间的大小是无法随着你的需要进行增加和减少而改变的,当数据两比较大的时候,有可能会出现越界的情况,数据比较小的时候,又有可能会浪费掉内存空间。在改变数据个数时,增加、插入、删除数据效率比较低 | 数据比较少;经常做的运算是按序号访问数据元素;数组更容易实现,任何高级语言都支持;构建的线性表较稳定 |
链表 | 动态申请或删除内存空间,对于数据增加和删除以及插入比数组灵活、操作效率高,内存可以不连续 | 相关声明比较繁琐 | 对线性表的长度或者规模难以估计;频繁做插入删除操作;构建动态性比较强的线性表 |
链表的类型
单链表
可以用一种最简单的数据结构来表示这样一个链表:
/* 一个链表中的一个元素 */
struct list_Node{
void *data; /* 数据 */
struct list_Node *next; /* 指向下一个元素的指针 */
}
单链表是最简单的一类链表,它的特点是仅有一个指针域指向后继节点(next),因此,对单链表的遍历只能从头至尾(通常是NULL空指针)顺序进行
双向链表
链表元素中存在两个指针,分别指向前一个元素和后一个元素。
struct list_Node{
void *data; /* 有效数据 */
struct list_Node *next; /* 指向下一个元素的指针 */
struct list_Node *prev; /* 指向前一个元素的指针 */
}
环形链表
为链表中最后一个元素不再有下一个元素,所以将链表尾元素中的向后指针设置为NULL,以此表明它是链表中的最后一个元素。但在有些链表中,末尾元素并不指向特殊值,相反,它指回链表的首元素。这种链表因为首尾相连,所以被称为是环形链表。环形链表也存在双向链表和单向链表两种形式。在环形双向链表中,首节点的向前指针指向尾节点。下图1和图2分别表示单向和双向环形链表。
Linux链表和普通链表
普通链表
普通的链表有上面的基本介绍可以看出,在链表的结构体中存在有普通的数据,除此之外,还有指向结构体本身的指针域。例如:
struct person{
char name[20];
int age;
struct person*pre;
struct person*Next;
}
这种链表是将数据添加到链表中,整个链表中的数据只能完全一致,假设向上述的链表中增加一个动物的数据,很明显是无法实现的。
Linux内核链表
Linux内核使用的链表则与正常的链表完全不一样,linux的链表的实现是将链表的节点塞入数据结构中
。linux内核链表一般就是在一个结构体放一个链表节点的结构体成员变量,该链表节点结构体变量仅有next 和prev两个指针,分别指向下一个结点和上一个节点,通过这样的方式将所有的结构体串在一起。这样做可以用内核链表将不同类型的结构体串起。就像冰糖葫芦串一样,不仅可以串正宗的山楂,也可以将葡萄、草莓等串起来。
linux 链表的实现
linux链表结构的初始化
struct list_head {
struct list_head *next, *prev;
};
上述的代码仅是一个链表节点。对其进行初始化可以得到一个双向链表,并且是一个环形的链表。这种方式和C++模板中的list是一样。
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
在linux中存在这样的宏定义
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
将两者结合就会出现下面的样子
struct list_head name = { &(name), &(name) }
linux链表的数据访问
linux的内核链表不同于普通的链表,它是嵌套在相关的数据之中的,但是如何通过链表来获取到数据就是其最大的问题。
#define list_entry(ptr, type, member) container_of(ptr, type, member)
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member)*__mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member));})
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
在上述的代码中,offsetof(Type,MEMBER)将0x0地址强制转换为TYPE*类型,然后取TYPE中的成员MEMBER地址,因为起始地址为0,得到的MEMBER的地址就直接是该成员相对于TYPE对象的偏移地址了。所以offsetof()就获得了MEMBER的相对偏移地址了。
((type *)0)->member)是将0强制转换成TYPE类型的指针,然后在调用其内部成员变量。typeof(x)返回变量X的数据类型,typeof(((type *)0)->member)就是type中member成员的数据类型,typeof(((type\ *)0)->member)*__mptr 是将mptr强制转换成member的数据类型,然后将ptr指针赋值给他。
(type *)((char *)__mptr - offsetof(type, member));将mptr强制转换成char*类型,这样操作是因为char所占的内存空间位1,指针加减就能够以1个字节进行。就能够实现地址的精确定位。因此整个语句的意思就是获取type类型的指针,然后减去偏移量就会得到相关数据的地址。
linux链表的遍历
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)
这种方式使用的都是指向内核结构体的,这种方式在理解上比较简单,但是在设计上存在缺陷,其封装性不是很强。
下面介绍的是指向内核链表结构体所在的结构体的方法。上面一节中list_entry(ptr, type, member)传入的ptr是将各个type对象串起来的链表的头指针,ptr->next 就是该链表的第一个节点。上面返回值就是挂载在第一个节点上的对象地址。
对象结构就是像下面这样挂载在链表上
正向遍历
#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))
/*
实际上就是一个for循环,循环内部由用户自己根据需要定义
初始条件:pos = list_entry((head)->next, typeof(*pos), member); pos为第一个对象地址
终止条件:&pos->member != (head);pos的链表成员不为头指针,环状指针的遍历终止判断
递增状态:pos = list_entry(pos->member.next, typeof(*pos), member) pos为下一个对象地址
所以pos就把挂载在链表上的所有对象都给遍历完了。至于访问对象内的数据成员,有多少个,用来干嘛用,则由用户
根据自己需求来定义了。
*/
反向遍历
#define list_for_each_entry_reverse(pos, head, member) \
for (pos = list_entry((head)->prev, typeof(*pos), member); \
&pos->member != (head); \
pos = list_entry(pos->member.prev, typeof(*pos), member))
链表的插入
链表的插入可以用这样的一个公式实现
prev <<=>> new <<=>> next
new是要新增的节点,
pre和next是相邻的节点
A <<=>> B 表示A的后继指向B,B的前驱指向A,理解上述公司,后面的函数操作就很好理解了。
也可以直接看后面的list_add()函数,把结构体带入函数中也会好理解些
在内核中有很多前面带有两个_的函数(即:__记住是两杠),这种类型的函数基本上都是不能直接调用的,需要重新封装后才能调用。这是个原始函数
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;
}
new 元素将添加在prev的后面,next的前面。
//head是头指针
//头节点后添加元素,相当于添加头节点,可实现栈的添加元素
//new添加到head之后,head->next之前 head <<=>> new <<=>> head->next
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
//头节点前添加元素,因为是环状链表所以相当于添加尾节点,可实现队列的添加元素
//new添加到head->prev之后,head之前 head-prev <<=>> new <<=>> head
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}
针对链表的插入还有一种简单的方式如:
#define ListAdd(head, list) \
do{\
(list)->prev = head; \
(list)->next = (head)->next;\
(head)->next->prev = list;\
(head)->next = list;\
}while(0)
链表元素的删除
链表节点的删除可以使用公式prev <<=>> next
表示
//prev和next两个链表指针构成双向链表关系
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
//删除指定元素,这是一种安全删除方法,最后添加了初始化
static inline void list_del_init(struct list_head *entry)
{
//删除entry节点
__list_del(entry->prev, entry->next);
//entry节点初始化,是该节点形成一个只有entry元素的双向链表
INIT_LIST_HEAD(entry);
}
链表元素的删除还可以使用另外一种方式:
#define ListDel(list) \
do{\
(list)->prev->next = (list)->next;\
(list)->next->prev = (list)->prev;\
}while(0)
链表中元素的替换
链表的替换就是让new的节点替换为old节点。但是old节点指针的前驱和后驱没有变化,old还是在原节点位置上。
//调整指针,使new的前后指针指向old的前后,反过来old的前后指针也指向new
//就是双向链表的指针调整
//A<==>B<==>C
//A<==>D<==>C
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;
}
//替换元素,最后将old初始化一下,不然old还是指向原来的两个前后节点(虽然人家不指向他)
static inline void list_replace_init(struct list_head *old,
struct list_head *new)
{
list_replace(old, new);
INIT_LIST_HEAD(old); //old变为空节点
}
内核链表的移动
链表的移动就是将一个节点先从链表节点上卸载下来,然后在添加到某个位置。
//移动指定元素list到链表的头部
static inline void list_move(struct list_head *list, struct list_head *head)
{
__list_del(list->prev, list->next);//删除list元素
list_add(list, head);//将list节点添加到head头节点
}
/**
* list_move_tail - delete from one list and add as another's tail
* @list: the entry to move
* @head: the head that will follow our entry
*/
//移动指定元素list到链表的尾部
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);
}
链表的判断
判断list节点是否是该链表中最后的一个节点。
// 因为是环链表,所以若是最后一个节点。则该节点的后继为头节点:list->next = head
static inline int list_is_last(const struct list_head *list,
const struct list_head *head)
{
return list->next == head;
}
// 判断该链表是否是空链表,只有一个head节点
static inline int list_empty(const struct list_head *head)
{
return head->next == head;
}
//链表中只有一个节点
static inline int list_is_singular(const struct list_head *head)
{
return !list_empty(head) && (head->next == head->prev);
}
链表的拼接
// 先说下前提:list是个单独的链表;prev和next是个链表中相邻的2个节点
// 而这个函数实现的是把list和prev这个链表相整合成一个链表。prev和next中断开连接list前后2个节点
// 但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;
}
// 这个函数是先考虑list是否为空表,然后调用上面的整合函数,从头部整合进去。
// 但这个list的前驱和后继都没有更改
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);
}
// 这是解决 list_splice()函数中list的前驱和后继没有修改的问题。
// 该函数调用INIT_LIST_HEAD(list)来是list为空节点
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_splice_tail()这个函数功能是一样的,只是这个函数对list进行了处理。
// 让list变成了空节点。其实有点不理解的是list_splice_tail()函数为什么不对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);
}
}
链表的分割
// 单独分析这个函数是比较难看出怎么分割的。这函数有个前提就是head 和 entry 是在同一个链表上的节点
// 第一步:....<<=>> head <<=>>......<<=>> entry <<=>>.....
// 第二步:设head的next为head_next,entry的next为entry_next
// 第三步:....<<=>> head <<=>> head_next <<=>>.....<<=>> entry <<=>> entry_next <<=>>....
// 第四步:经过函数分割后得两条链表:...<<=>> head <<=>> entry_next <<=>> .....
// 和 ....<<=>> entry <<=>> list <<=>> head_next <<=>> ....
// 函数功能:函数把head....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;
}
// 这是个分割函数,与上面这个函数不同的是,
// 这个函数考虑到了空链表和一个节点的链表情况
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);
}
// 这个函数考虑到了空链表和一个节点的链表情况
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);
}
参考文章:
[添加链接描述](https://blog.csdn.net/caihaitao2000/article/details/80556562?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522159833747919195162537112%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=159833747919195162537112&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_v1~rank_blog_v1-1-80556562.pc_v1_rank_blog_v1&utm_term=Linux%20%E9%93%BE%E8%A1%A8&spm=1018.2118.3001.4187)