00引言
1.1 普通链表弊端
普通链表概念简单,操作方便,但存在有致命的缺陷,即:每一条链表都是特殊的,不具有通用性。因为对每一种不同的数据,所构建出来的链表都是跟这些数据相关的,所有的操作函数也都是数据密切相关的,换一种数据节点,则所有的操作函数都需要一一重写编写,这种缺陷对于一个具有成千上万种数据节点的工程来说是灾难性的。
1.2 问题分析
比如下面的操作函数,函数只能操作指定的参数类型:
// 普通链表的插入函数,与数据节点node密切相关
// 换一种数据节点,该函数就无法使用了
void insert(node *head, node *new)
{
// ...
}
// 普通链表的删除函数,与数据节点node密切相关
// 换一种数据节点,该函数就无法使用了
node * remove(node *head)
形态各异的普通链表
在普通链表的节点设计中,不同的链表所使用的指针不同,就直接导致操作函数的参数不同,在C语言的环境下,无法统一这些所有的操作,这给编程开发带来了很大的麻烦,尤其在节点种类众多的场合。
1.3 原因分析
分析上述问题,其产生的根本原因是链表节点的设计,没有把数据和逻辑分开,也就是将具体的数据与组织这些数据的链表揉在一起,导致链表的操作不得已绑定了某个固定类型的数据。
节点中的数据和逻辑
1.4 解决思路
既然是因为数据和链表指针混在一起导致了通用性问题,那么解决的思路就是将它们分开。将链表逻辑单独抽出来,去掉节点内的具体数据,让节点只包含双向指针。这样的节点连接起来形成一条单纯的链表如下所示:
//不包含数据的结构体,结构体有两个成员
struct list
{
struct list *prev; //存放前缀节点的指针
struct list *next; //存放后缀节点的指针
};
接着,将这样的不含任何数据的链表,镶嵌在具体要用串起来的数据节点之中,这样一来,就可以将任何节点的链表操作完全统一了。
镶嵌了标准链表的用户节点
1.5节点分析:
如上图所示,不管用户节点是什么类型的节点,也不管它里面包含什么数据,都跟链表本身没有关系。如下图所示,在 A 和 C 中插入 B 节点,和在 X 与 Z 中插入 Y 节点,完全可以用相同的函数来达到。此时,就已经成功地将数据与组织这些数据的逻辑分开了。这就是内核链表的基本思路。
//不包含数据的结构体,结构体有两个成员
//小结构体
struct list
{
struct list *prev; //存放前缀节点的指针
struct list *next; //存放后缀节点的指针
};
//大结构体
struct node
{
//七个数据
int a;
int b;
int c;
int d;
int e;
int f;
int g;
struct list list; //小结构--里面有两个成员
}
01. 内核链表概念
1.1内核介绍:
如前所述,内核链表解决通用性问题,大概分两步:
- 设计标准节点
- 针对标准节点,设计由标准节点构成的标准链表的所有操作
内核链表的标准节点及其所有操作,都被封装在内核源码中,具体来讲都被封装在一个名为 list.h 的文件中,该文件在内核中的位置是:
/usr/src/linux-hwe-5.15-headers-5.15.0-43/include/linux/list.h
内核中的源码文件 list.h 实际上包含了两部分内容,一是内核链表,二是哈希链表。经过整理的、仅包含内核链表的文件:kernel_list.h
Linux内核链表(Linux系统链表:双向循环链表,哈希表....)
下载系统源码的方法常见的有两种:
第一种访问网站下载: kernel.org
第二种输入Linux命令下载:sudo apt install linux-source-5.15.0 (一般这种下载的是当前系统所用到的系统源码版本) 下载完之后在/usr/src中可找到系统源码的压缩包,可以解压到共享路径
1.2 kernel_list.h:(个人的内核链表翻译版)
文章顶部下载:
02.内核链表操作
内核链表函数接口
2.1 节点设计
标准节点就是不包含任何数据的双向链表节点,如下所示:
这是内核链表当中的小结构体
//小结构体
struct list_head
{
struct list_head *prev;
struct list_head *next;
};
这个标准节点的用法,就是在实际用户数据节点中,镶嵌进去。为了表述简单,一般将用户的数据节点称为大结构体,将标准节点称为小结构体,例如:
struct node //大结体
{
int data; //数据
struct list_head list; //定义小结构体变量,小结构体由内核进行提供
};
2.2节点初始化
内核链表对节点的初始化与普通的双向链表节点无异,就是简单地让节点的前后向指针指向自身,其内核中的代码如下:
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
初始化了的标准节点
实际应用中,采用标准节点的内核链表都是带头结点的,其初始化代码如下所示:
//结构体指针函数 生成新的一个节点
//节点初始化 -- 在堆空间申请一个struct node结构体大小,并对成员进行初始化
struct node *init_node(void)
{
//在堆空间开辟sizeof(struct node)大小空间--大结构体
struct node *new = (struct node *)malloc(sizeof(struct node));
if(new == NULL)
{
printf("malloc fail\n");
return NULL;
}
new->data = 0;
//小结构体里面是两个指针,两个指针指向自己,形成双向循环循环
//手写初始化代码
// new->list.prev = &new->list;
// new->list.next = &new->list;
//使用内核链表API进行初始化小结构体
//传入小结构体的地址
INIT_LIST_HEAD(&new->list);
return new;
}
2.3插入节点
内核练表的标准操作中,提供了头插法和尾插法,内核源码代码如下:
// 内部函数
// 将节点new插入到prev与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;
}
// 将新节点new插入到链表head的首部
// 即:插入到head的后面,实现数据头插
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
// 将新节点new插入到链表head的尾部
// 即:插入到head的前面,实现数据后插
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}
在实际应用中,用户针对大结构体中的标准节点去操作,比如:
// 将该节点插入已有链表的首部
//struct list_head *new:新节点里面小结体的地址
//struct list_head *head:头节点里面小结体的地址
list_add(struct list_head *new, struct list_head *head);
// 将该节点插入已有链表的首部
list_add(&new->list, &head->list);
// 将该节点插入已有链表的尾部
list_add_tail(&new->list, &head->list);
2.4 遍历链表
(遍历里是通过小结构体list进行遍历)
内核练表提供了向前、向后遍历链表的标准算法,源码如下:
/**
* list_entry – get the struct for this entry
* @ptr: the &struct list_head pointer. 移动的小结构体对应的地址
* @type: the type of the struct this is embedded in. 大结构体类型 (struct node)
* @member: the name of the list_struct within the struct. 小结构体在大结构体里面的成员名list
*///返回值为大结构体地址
#define list_entry(ptr, type, member) \
((type *)((char *)(ptr)-(size_t)(&((type *)0)->member)))
//向后遍历,里面是小结构体遍历
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); \
pos = pos->next)
/**
* list_for_each_prev - iterate over a list backwards
* @pos: the &struct list_head to use as a loop counter.
* @head: the head for your list.
*/
//向前遍历
#define list_for_each_prev(pos, head) \
for (pos = (head)->prev; pos != (head); \
pos = pos->prev)
//往后遍历时,保留pos后缀节点
#define list_for_each_safe(pos, n, head) \
for (pos = (head)->next, n = pos->next; pos != (head); \
pos = n, n = pos->next)
/**
* list_for_each_entry - iterate over list of given type
* @pos: the type * to use as a loop counter. 大结构体指针
* @head: the head for your list. 小结构体指针
* @member: the name of the list_struct within the struct. 小结构体在大结构体里面的成员名list
*/
//向后直接遍历得到大结构体
#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))
void display(struct node *head)
{
struct node *tmp = NULL;
struct list_head *pos = NULL;
#if 0
//向后遍历显示数据
//遍布小结构体
list_for_each(pos, &head->list)
{
//通过小结构体得到大结构体地址
tmp = list_entry(pos, struct node, list);
printf("%d\t", tmp->data);
}
#elif 0
//向前遍历显示数据
//遍布小结构体
list_for_each_prev(pos, &head->list)
{
//通过小结构体得到大结构体地址
tmp = list_entry(pos, struct node, list);
printf("%d\t", tmp->data);
}
#elif 1
//向后遍历显示数据
//遍布小结构体
list_for_each_entry(tmp, &head->list, list)
{
printf("%d\t", tmp->data);
}
#endif
printf("\n");
}
2.5剔除节点
与普通链表一致,剔除内核链表节点只是意味着将某指定节点脱离链表,并不意味着释放其内存。剔除节点的内核源码是:
// 内部函数
static inline void __list_del(struct list_head *prev, struct list_head *next)
{
next->prev = prev;
prev->next = next;
}
// 将指定节点 entry 从链表结构中剔除(并没有free)
// 并将其前后指针置空
static inline void list_del(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
entry->next = (void *) 0; //(void *) 0 == NULL
entry->prev = (void *) 0;
}
// 将指定节点 entry 从链表结构中剔除
// 并将其前后指针指向自身
static inline void list_del_init(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
INIT_LIST_HEAD(entry);
}
2.6 移动节点
移动节点的操作,实际上是先将节点剔除出链表,然后再插入某指定位置,内核源码如下:
// 将节点list,移动到指定位置head的后面
static inline void list_move(struct list_head *list,
struct list_head *head)
{
__list_del(list->prev, list->next);
list_add(list, head);
}
// 将节点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);
}
2.7.链表合并
/**
* list_splice – join two lists
* @list: the new list to add.
* @head: the place to add it in the first list.
*/
//将list链表合并到head链表当中
static inline void list_splice(struct list_head *list, struct list_head *head)
{
if (!list_empty(list))
__list_splice(list, head);
}
/**
* list_splice_init – join two lists and reinitialise the emptied list.
* @list: the new list to add.
* @head: the place to add it in the first list.
*
* The list at @list is reinitialised
*/
//将list链表合并到head链表当中 再将链表头节点进行初始化
static inline void list_splice_init(struct list_head *list,
struct list_head *head)
{
if (!list_empty(list)) {
__list_splice(list, head);
INIT_LIST_HEAD(list);
}
}
2.8.链表拆分
/**
* list_cut_position - cut a list into two
* @list: a new list to add all removed entries
* @head: a list with entries
* @entry: an entry within head, could be the head itself
* and if so we won't cut the list
*
* This helper moves the initial part of @head, up to and
* including @entry, from @head to @list. You should
* pass on @entry an element you know is on @head. @list
* should be an empty list or a list you do not care about
* losing its data.
*
*/
// struct list_head *head原先的链表
//struct list_head *list:新链表的头节点
//struct 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))
03.结语:
内核链表是 Linux 内核中不可或缺的数据结构,提供了高效的动态内存管理与灵活的数据操作。它的优点在于简化插入、删除和遍历等操作,极大地提升了内核模块的开发效率。
本文介绍了内核链表的基本概念和使用方法,但还有许多细节未涉及,例如链表的嵌套使用、与其他数据结构的组合、以及在高并发环境下的线程安全问题。了解这些内容可以帮助开发者更加高效地进行系统级编程并避免常见的误用。
总之,掌握内核链表对于深入理解 Linux 内核的运行机制和优化性能是非常重要的。希望本文为你在学习内核链表的道路上提供了一些指导,并期待未来的讨论与探索