IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> Linux 链表 list_head -> 正文阅读

[数据结构与算法]Linux 链表 list_head

参考文章

list_head

内核关于链表的代码定义在:include/linux/list.h
list_head 结构体,它分别有一个next指针和prev指针 都是struct list_head 类型。

struct list_head {
    struct list_head *next, *prev;
};

内核链表的设计思想:既然链表不能包含万事万物,那么就让万事万物来包含链表。
一个链表节点包含数据域:person.age,链表域:list。

//用自定义类型的struct 包含list_head,即用万物包含链表。
struct person 
{
     struct list_head list;
     int age;
};

定义一个head

定义一个head结构体,head 通常不会用来存储数据。
它的作用就是方便找到其它节点,牵着头节点就可以找到这条链表中的任何一个节点。

struct person person_head;		
INIT_LIST_HEAD(&head.list)	

//第一步:根据自己的数据类型定义一个person_head结构体
//第二步:对head 做初始化,所谓的初始化就是让person_head中的list头指针和尾指针都指向自己
在这里插入图片描述
INIT_LIST_HEAD 的原型如下
内核中有两种方法可以初始化一个链表头节点:INIT_LIST_HEAD、LIST_HEAD
它们的不同点在于INIT_LIST_HEAD 需要自己定义一个list_head,而LIST_HEAD只需要传入一个变量名。

INIT_LIST_HEAD(struct list_head *list)
{
    list->next = list->prev = list;
}


#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) \
	struct list_head name = LIST_HEAD_INIT(name)

添加一个新节点

向一条链表添加一个新的节点,我们首先要了解链表的结构。如图,是一条完整的链表:
在这里插入图片描述
它拥有head 头节点和,list_head1~list_headn 个数据节点。注意:头不存放数据,只是为了能找到其它节点;list_head1-n 可以存放数据。

头插法:
添加新节点有头插法和尾插法,我们先来看一下头插法,对应内核中的函数是list_add:

static inline void list_add(struct list_head *new, struct list_head *head)
{
    __list_add(new, head, head->next);
}

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;
}

首先,我们假设现在有一个已经初始化号的头节点,它的prev 和next 分别指向自己。
在这里插入图片描述
要安插一个new节点,调用list_add->__list_add
next->prev = new; //next在list_add 中传入的是head->next,此时的head->next 指向的是head(head->next = head)
//那么这里的next 就是head,得出结论 head->prev = new head 的前一个指向的是new。
new->next = next; //new 的后一个指向的是 head。
new->prev = prev; //new 的前一个指向的是 head。
prev->next = new; //head 的后一个指向的是 new。

得到的结果就是两个结构体的next、prev 互相指。
在这里插入图片描述
添加 第二个节点new2,假设上一步添加的节点为new1
next->prev = new; //在list_add 传入参数head->next 此时已经指向了new1,这里的next 就是new1
//那么就是new1->prev =new2,new1的prev 指向了new2
new->next = next; //new2->next 指向new1
new->prev = prev; //new2->prev 指向head
prev->next = new; //head->next 指向new2

最终结果就是如下图:
(头插法:新插入的节点永远会再上一个插入的前面,例如new2在new1的前面)
在这里插入图片描述
以此类推再插入一个新的节点,new3 又会再new2 的前面。
最后插入的节点它的prev 永远指向head,head的prev 永远指向第一个节点。next 刚好与prev 相反。
后面添加的节点会向左边new4、new5 … 依次增长。
在这里插入图片描述

尾插法:

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;
}

尾插法对应内核函数list_add_tail,它也是调用的__list_add。
添加节点过程:添加第一个节点时,最终的结果是new1 和 head的 prev 和next互相指。添加第二个节点时这里把new 看成new2,head->prev 指向的是new1 所以看成new1,head还是head 依次代入到 __list_add 函数里面。以此类推添加其它节点。
__list_add(new, head->prev, head);
得到的结果如图,和头插法相反 新节点会像右不断的增长。
在这里插入图片描述

遍历链表

构建好一个完整的链表后,我们需要通过遍历链表来查找节点,访问其中的数据域。
遍历链表使用函数:list_for_each

//include/linux/list.h
#define list_for_each(pos, head) \
	for (pos = (head)->next; pos != (head); pos = pos->next)

pos 是一个指针,目的是偏移,指向某个节点;head 就是链表头,与上一步的head 相同。
list_for_each 是一个宏,它本身的内容其实就是一个for 循环:
① pos = (head)->next,pos 偏移到链表的头节点或尾节点 (原因是头插 和 尾插的不同)。
② pos != (head),判断pos 指向的节点是不是head,是的话就说明一整个链表遍历完了,退出for 循环。
③ pos = pos->next,pos 再次偏移向下一个节点。如果①中 pos 是头节点,那么就会不断的向后(右,参考尾插法图)偏移,如果是尾节点那么就不断的向前(右,参考头插法图)偏移。每偏移一次都会判断一下②,直到遍历完所有的节点回到head。

再for 循环中可以访问节点的数据域,如下:

list_for_each(pos, head){
	strcut person* person1 = container_of(pos,struct person,list)
	printk("age = %d\n", person1->age)
}

因为 list_head 中并没有数据域,所以我们要获取包含这个list 的person 结构体。这就会用到container_ofoffsetof 宏,它可以通过list 的地址获取到包含这个list 的结构体,person 也可以替换成其它类型。
container_of 和offsetof参考
我们后面把包含list 成员的结构体称为容器结构体 ,例如person。
内核又对container_of 宏做了封装,换了个名字 list_entry。用这两个宏的效果是一样的。

#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)

list_for_each_entry
在上面我们遍历链表,然后用list_entry 获取到容器结构体,再访问容器结构体中的内容,这样的步骤太麻烦。所以内核为我们想到了解决方法:宏list_for_each_entry。
list_next_entry 就是在list_entry 上再做出扩展。可以把pos 偏移到下一个list,然后返回list 所在的容器结构体。
list_for_each_entry 遍历整个链表所有的list,然后找到它所在的容器,在for循环里就可以直接访问容器结构体了。
注意: list_for_each_entry 传入的参数pos 是容器类型的指针,而不是 struct list_head* 。

#define list_next_entry(pos, member) \
	list_entry((pos)->member.next, typeof(*(pos)), member)

#define list_for_each_entry(pos, head, member)				\
	for (pos = list_first_entry(head, typeof(*pos), member);	\
	     &pos->member != (head);					\
	     pos = list_next_entry(pos, member))

删除节点

对于删除链表节点,内核定义了函数list_del。

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)
{
	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;
}

首先判断entry 是否有效,如果entry == NULL 则无效。
接着调用到了__list_del, 是关于指针的操作。可以想象一个节点n,它的前一个节点是n-1,后一个节点是n+1,把n 带入到代码中的entry:
__list_del(entry->prev, entry->next); entry->prev 就是n-1,entry->next 是n+1。
next->prev = prev; n+1 的前一个是本来是n,现在改成n-1。
prev->next = next; n-1 的后一个本来是n,现在改成了n+1。 如此n 就在n-1 和n+1 中间删除了,链表中也就没有了n节点。
如果一条链表中只有两个节点:head 和 n。他们两个的关系是next 和perv 都指向对方,删除n 后又变成了head 自己指自己。
最后将entry 的next、prev 指向LIST_POISON1 和 LIST_POISON2,这个是内核设置的一个区域。

使用list_del 可以从链表中删除一个节点,但是如果你想在遍历链表时删除一个节点,那么删除它后必须执行break 跳出for 循环,否则就会发生段错误,如图:
在这里插入图片描述
为什么会这样,我们来看list_for_each_entry 的实现,假设我们遍历找到了节点(pos)->member ,删除之后for 循环还会继续利用(pos)->member.next 来找到下一个节点,但是此时它指向LIST_POISON1,所以这样时不对的。

#define list_next_entry(pos, member) \
	list_entry((pos)->member.next, typeof(*(pos)), member)

#define list_for_each_entry(pos, head, member)				\
	for (pos = list_first_entry(head, typeof(*pos), member);	\
	     &pos->member != (head);					\
	     pos = list_next_entry(pos, member))

为了应付这个问题,内核开发者又想出了新的宏list_for_each_entry_safe,它与list_for_each_entry 的区别就是它提供了一个新的指针n,在遍历的时候提前把pos 的下一个节点n获取到了,所以即使pos 被删除也没关系,因为它用的是n 来寻找下一个。

#define list_for_each_entry_safe(pos, n, head, member)			\
	for (pos = list_first_entry(head, typeof(*pos), member),	\
		n = list_next_entry(pos, member);			\
	     &pos->member != (head); 					\
	     pos = n, n = list_next_entry(n, member))

替换节点

使用一个单独的节点替换链表中的某一个节点,可以用 list_replace:

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没有将old的前驱和后继断开,所以内核又提供了:list_replace_init

static inline void list_replace_init(struct list_head *old,
					struct list_head *new)
{
	list_replace(old, new);
	INIT_LIST_HEAD(old);
}

移动节点

内核链表还提供给我们:list_move
list_move就是删除list指针所处的容器结构节点,然后将其重新以头插法添加到另一个头结点中去,head可以是该链表自身,也可以是其他链表的头指针。

static inline void list_move(struct list_head *list, struct list_head *head)
{
	__list_del_entry(list);
	list_add(list, head);
}

既然有头插法的list_move,那么也同样有尾插法的list_move_tail:

static inline void list_move_tail(struct list_head *list,
				  struct list_head *head)
{
	__list_del_entry(list);
	list_add_tail(list, head);
}

判断链表是否空

如果head->next == head 的话,那么这是个空链表

static inline int list_empty(const struct list_head *head)
{
	return READ_ONCE(head->next) == head;
}
  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-09-13 11:43:26  更:2022-09-13 11:47:31 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 14:05:42-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码