如何在 Linux 内核中高效使用链表:原理与实践

news2025/7/2 11:29:52

文章目录

  • 前言
  • 一、Linux内核链表源码分析
    • 1.链表的初始化
      • 1. 静态初始化
        • 宏 `LIST_HEAD_INIT(name)`
        • 宏 `LIST_HEAD(name)`
      • 2. 动态初始化
        • 函数 `INIT_LIST_HEAD(struct list_head *list)`
    • 对比总结
    • 2.链表的添加
      • `list_add` 函数的定义
      • 函数参数
      • 内部实现
        • `__list_add` 函数
      • `list_add` 的功能总结
      • 使用场景
      • 小结
    • 3.链表删除
      • `list_del` 函数的定义
      • 函数参数
      • 内部实现
        • `__list_del` 函数
      • 函数的功能总结
      • 调用示例
      • `LIST_POISON1` 和 `LIST_POISON2`
      • 小结
  • 二、Linux内核链表和普通链表对比
      • 1. 内核链表是通用链表
        • 通用性
        • 示例对比
      • 2. 内核链表是双向链表
        • 结构设计
        • 操作简便性
      • 3. 内核链表的灵活性和复用性
        • 灵活性
      • 总结


前言

一、Linux内核链表源码分析

1.链表的初始化

在 Linux 内核开发中,链表的初始化非常常见,链表初始化通常有两种方式:静态初始化和动态初始化。你给出的代码片段展示了这两种初始化方式。让我们来详细讲解一下这些宏和函数是如何工作的。

1. 静态初始化

静态初始化是在编译时就完成的初始化,它不需要在运行时调用函数。静态初始化主要通过宏 LIST_HEAD_INITLIST_HEAD 来完成。

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

LIST_HEAD_INIT(name) 是一个宏,用于静态地初始化一个 list_head 结构体变量。它将链表头的 nextprev 指针都指向链表头本身,也就是 &(name)。这表示链表是空的,但已经初始化好了。

  • &(name) 获取了链表头 name 的地址。
  • { &(name), &(name) } 是一个结构体初始化列表,将 nextprev 都指向 name
LIST_HEAD(name)
#define LIST_HEAD(name) \
	struct list_head name = LIST_HEAD_INIT(name)

LIST_HEAD(name) 是一个宏,用于定义并初始化一个名为 namelist_head 结构体变量。

  • struct list_head name 定义了一个 list_head 结构体。
  • = LIST_HEAD_INIT(name) 通过前面的宏 LIST_HEAD_INIT 来初始化这个结构体,使 namenextprev 都指向 name 本身。

总结:

  • 静态初始化是通过 LIST_HEAD_INITLIST_HEAD 在编译时完成的,它们使链表头部的 nextprev 都指向链表头本身,这样就表示链表是空的。

2. 动态初始化

动态初始化是在程序运行时调用函数来完成的。在链表的动态初始化中,通常通过调用 INIT_LIST_HEAD 函数来完成。

函数 INIT_LIST_HEAD(struct list_head *list)
static inline void INIT_LIST_HEAD(struct list_head *list)
{
    WRITE_ONCE(list->next, list);
    list->prev = list;
}

INIT_LIST_HEAD 是一个内联函数,用于动态地初始化一个 list_head 结构体。

  • WRITE_ONCE(list->next, list);listnext 指针指向 list 本身。
  • list->prev = list;listprev 指针指向 list 本身。

总结:

  • 动态初始化是通过调用 INIT_LIST_HEAD 函数在程序运行时完成的。它的效果与静态初始化相同,即将链表头的 nextprev 指针都指向链表头本身,表示链表为空。

对比总结

  • 静态初始化:在编译时就完成了初始化,通过 LIST_HEAD_INITLIST_HEAD 宏来实现。适用于编译时即可确定链表头的情况。

  • 动态初始化:在运行时通过调用 INIT_LIST_HEAD 函数来完成初始化。适用于运行时需要初始化链表头的情况。

无论使用静态还是动态初始化,最终的效果都是将链表头的 nextprev 指针都指向链表头本身,表示链表为空并已初始化。

2.链表的添加

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
list_add 是 Linux 内核中用于在双向链表中插入新节点的函数。双向链表是一个常用的数据结构,在 Linux 内核中被广泛应用于各种任务,如管理进程、设备列表、文件系统结构等。

list_add 函数的定义

在 Linux 内核中,list_add 函数的原型通常定义在 include/linux/list.h 头文件中:

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

list_add 的主要功能是将一个新节点插入到链表的头部。具体来说,它将节点插入到链表的某个节点(通常是头节点 head)之后,使得新节点成为链表的第一个节点。

函数参数

  • new: 指向将要插入的新节点的指针,类型为 struct list_head *
  • head: 指向链表头节点的指针,类型为 struct list_head *

内部实现

list_add 实际上调用了一个内部函数 __list_add 来完成插入操作。__list_add 函数负责处理链表中节点的插入操作。

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

__list_add 函数执行的步骤如下:

  1. next->prev = new;
    next 节点的 prev 指针指向新节点 new

  2. new->next = next;
    将新节点 newnext 指针指向原本的 next 节点。

  3. new->prev = prev;
    将新节点 newprev 指针指向原本的 prev 节点。

  4. prev->next = new;
    prev 节点的 next 指针指向新节点 new

通过这四个步骤,新节点 new 就被正确地插入到了 prevnext 之间,维护了链表的双向链接结构。

list_add 的功能总结

list_add 的功能是在链表中插入一个新节点,使得新节点成为链表中的第一个节点(紧跟在头节点 head 之后)。在调用 list_add 之后,链表的结构会被调整,以包含这个新的节点。

例如,假设我们有以下链表结构:

head <-> A <-> B <-> C

我们希望将一个新节点 new 插入到 head 之后(即链表的头部)。调用 list_add(new, &head); 后,链表的结构变为:

head <-> new <-> A <-> B <-> C

使用场景

list_add 通常用于将新节点插入到链表头部,用于维护栈结构或优先级较高的元素。在 Linux 内核中,它被广泛用于各类内核数据结构的管理,如设备列表、文件系统结构、进程链表等。

小结

  • 功能: list_add 在双向链表的头部插入一个新节点。
  • 操作: 调用 __list_add 函数,调整前后节点的 nextprev 指针,将新节点插入到链表中。
  • 应用: 常用于需要在链表头部插入新元素的场景,广泛应用于 Linux 内核的各种链表管理。

3.链表删除

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
list_del 是 Linux 内核中用于从双向链表中删除节点的函数。这个函数在内核链表操作中非常重要,因为它提供了一种安全且高效的方式来从链表中移除节点。

list_del 函数的定义

在 Linux 内核的链表实现中,list_del 函数通常定义在 include/linux/list.h 头文件中:

static inline void list_del(struct list_head *entry)
{
    __list_del(entry->prev, entry->next);
    entry->next = LIST_POISON1;
    entry->prev = LIST_POISON2;
}

函数参数

  • entry: 指向要从链表中删除的节点,类型为 struct list_head *

内部实现

list_del 函数通过以下步骤来删除链表中的节点:

  1. 调用 __list_del(entry->prev, entry->next);
    这一行代码调用了 __list_del 函数,将当前节点 entry 的前驱节点和后继节点连接起来,从而从链表中移除了 entry

  2. entry->next 设置为 LIST_POISON1
    将被删除节点的 next 指针设置为 LIST_POISON1。这是一个特殊的宏定义,通常用于在调试中检测非法的指针操作。

  3. entry->prev 设置为 LIST_POISON2
    将被删除节点的 prev 指针设置为 LIST_POISON2,同样是为了调试和防止使用已经删除的节点。

__list_del 函数
static inline void __list_del(struct list_head *prev, struct list_head *next)
{
    next->prev = prev;
    prev->next = next;
}

__list_del 函数负责断开链表中当前节点 entry 的前驱节点 prev 和后继节点 next,并将它们直接连接起来。

  • next->prev = prev;
    entry 的后继节点的 prev 指针指向 entry 的前驱节点。

  • prev->next = next;
    entry 的前驱节点的 next 指针指向 entry 的后继节点。

通过这两个步骤,链表中的 entry 节点被移除,链表结构保持完整。

函数的功能总结

  • 删除节点: list_del 函数从双向链表中移除指定的节点,并将它的前驱节点和后继节点连接起来。
  • 安全性: 被删除节点的 nextprev 指针被设置为特殊的值 (LIST_POISON1LIST_POISON2),防止后续的非法访问和使用被删除的节点。

调用示例

假设我们有一个链表:

A <-> B <-> C

如果我们希望删除节点 B,可以调用:

list_del(&B);

调用后,链表变为:

A <-> C

B 的前驱节点 A 和后继节点 C 被直接连接起来,而 B 的指针被设置为毒性值,以防止后续误用。

LIST_POISON1LIST_POISON2

这两个宏定义通常是在调试模式下使用的。它们的值通常被定义为不可能是有效指针的值,以便在使用非法指针时快速发现错误。

#define LIST_POISON1  ((void *) 0x00100100)
#define LIST_POISON2  ((void *) 0x00200200)

这些值帮助开发人员检测到链表节点在删除后被错误地访问。

小结

  • 功能: list_del 用于从双向链表中删除节点,确保链表的前后节点正确链接。
  • 实现: 通过调用 __list_del 函数实现节点删除,并将删除节点的指针设置为毒性值,防止非法访问。
  • 应用: 在需要从链表中删除节点时使用,是 Linux 内核链表操作中的基础函数。

二、Linux内核链表和普通链表对比

Linux 内核链表是一种通用的双向链表实现,它在设计上与普通链表有一些显著的区别。以下是对 Linux 内核链表和普通链表的对比分析,重点讲解为什么内核链表更通用,而普通链表往往需要重新实现。

1. 内核链表是通用链表

通用性
  • Linux 内核链表:

    • 设计: Linux 内核链表被设计为一个通用的、可重用的双向链表实现,可以用于各种不同类型的数据结构。它的核心在于 struct list_head,这个结构体仅包含两个指针,分别指向链表的前一个节点和后一个节点。
    • 使用方式: 使用者只需要将 struct list_head 嵌入到自己的数据结构中,然后利用链表操作函数(如 list_add, list_del 等)进行操作,无需每次重新实现链表逻辑。
  • 普通链表:

    • 设计: 普通链表通常是在应用程序或驱动中为特定数据结构而编写的。每次需要使用链表时,开发者需要从头实现链表的数据结构和操作方法。
    • 使用方式: 由于链表的节点结构往往包含具体的数据,链表操作函数(如添加、删除、遍历等)都需要针对具体的数据类型进行重新编写,缺乏通用性。
示例对比
  • Linux 内核链表示例:

    struct my_data {
        int value;
        struct list_head list; // 内核链表结构
    };
    
    struct my_data item1, item2;
    INIT_LIST_HEAD(&item1.list); // 初始化链表头
    list_add(&item2.list, &item1.list); // 将 item2 添加到链表中
    
  • 普通链表示例:

    struct node {
        int value;
        struct node *next;
    };
    
    struct node *head = NULL;
    struct node *item1 = malloc(sizeof(struct node));
    struct node *item2 = malloc(sizeof(struct node));
    
    // 添加 item2 到链表
    item2->next = head;
    head = item2;
    

2. 内核链表是双向链表

结构设计
  • Linux 内核链表:

    • 双向链表: Linux 内核链表是双向链表,每个节点都包含两个指针,一个指向前一个节点 (prev),另一个指向后一个节点 (next)。双向链表的优势在于可以轻松地进行向前和向后的遍历以及高效地进行节点删除操作。
  • 普通链表:

    • 单向链表: 普通链表在大多数情况下是单向链表,即每个节点只包含一个指向下一个节点的指针。单向链表的节点删除操作通常较复杂,因为在删除一个节点时,必须要知道前一个节点的指针。
    • 双向链表: 虽然普通链表也可以实现为双向链表,但这种实现通常需要开发者手动设计双向指针结构,并为插入、删除、遍历等操作编写相应的代码。
操作简便性
  • Linux 内核链表:

    • 删除操作: 由于是双向链表,删除操作只需调整前后两个节点的指针,而无需查找前驱节点。例如,在内核链表中,list_del 函数能够直接删除当前节点而不影响其他节点的链接。
    • 插入操作: 在双向链表中插入节点相对简单,只需调整几个指针,即可将新节点插入到任意位置。
  • 普通链表:

    • 删除操作: 在单向链表中删除节点往往需要先遍历到要删除节点的前一个节点,然后才能修改其 next 指针。这使得操作更复杂且容易出错。
    • 插入操作: 插入操作也较为复杂,尤其是在指定位置插入时,单向链表需要遍历到目标位置。

3. 内核链表的灵活性和复用性

灵活性
  • Linux 内核链表:

    • 复用性强: 由于内核链表的设计与具体的数据结构解耦,因此可以在内核中的各种数据结构之间轻松复用。同一个链表操作函数可以处理不同的数据结构,只需确保数据结构中嵌入了 struct list_head
    • 内核链表工具函数: Linux 内核提供了大量的工具函数来简化链表操作,如 list_for_each(遍历)、list_empty(检查链表是否为空)、list_move(移动节点)等,这些函数进一步增强了链表的复用性和简便性。
  • 普通链表:

    • 特定性强: 普通链表往往是针对具体的应用场景而设计的,其复用性较差。如果另一个模块或应用需要使用链表,通常需要重新设计和实现。
    • 缺乏工具函数: 普通链表通常没有像内核链表那样丰富的工具函数,这意味着开发者需要手动实现各种常见操作,增加了开发复杂性。

总结

  • Linux 内核链表 是一个高度通用的双向链表实现,提供了丰富的工具函数,能够在内核中的各种数据结构之间复用,极大地提高了代码的灵活性和可维护性。
  • 普通链表 通常是单向链表,针对特定的数据结构和应用场景设计,虽然可以根据需要扩展为双向链表,但复用性和灵活性较差,每次使用时都可能需要重新实现链表操作逻辑。

通过内核链表的设计,Linux 内核能够在不牺牲性能的情况下,实现复杂数据结构的高效管理,而普通链表的实现则更适合于简单和特定的应用场景。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2040599.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java Spring|day4.SpringCloud

SpringCloud 定义 springcloud是分布式微服务架构的一站式解决方案&#xff0c;是多种微服务架构落地技术的集合体&#xff0c;俗称微服务全家桶。实现的功能有服务注册与发现&#xff0c;服务调用&#xff0c;服务熔断&#xff0c;负载均衡&#xff0c;服务降级&#xff0c;…

Excel公式合并同类项

Excel公式合并同类项 1、新建表&#xff0c;用公式引用要处理的数据&#xff0c;快速选中表格复制公式2、 合并同类项&#xff0c;复制数据&#xff0c;删除重复项3、 sumif()合并同类项4、vlookup()复制同类项 1、新建表&#xff0c;用公式引用要处理的数据&#xff0c;快速选…

docker数据卷:

docker数据卷&#xff1a; 容器和宿主机之间数据共享 容器和宿主机之间数据共享——————挂载卷————容器内的目录和宿主机的目录进行挂载&#xff0c;实现数据文件共享 容器的生命周期有限&#xff0c;一旦重启所有对容器内部文件数据的修改以及保存的数据都会被初始…

深入理解 iOS 中的 AutoLayout(二)

目录 前言 一、UIStackView自动布局 1.简单的UIStackView 2.嵌套的UIStackView 二、AutoLayout高级用法 1.以编程方式创建约束 1.布局锚点 1.主要特点 2.常见子类 1.NSLayoutXAxisAnchor 2.NSLayoutYAxisAnchor 3.NSLayoutDimension 3.常用方法 4.…

SQL server数据库备份和还原

新手小白都懂的sql server数据库备份和还原 一、备份 1.打开sql server数据库找到 2.展开找到对应的数据库文件 鼠标右击—任务–备份 3.复制名称 4.复制完点击添加 5.点击添加完之后再次点击查找路径 6.分别两个路径 原路径和新路径 &#xff08;新路径是找到原路径新建了一…

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. 问题解决

问题描述 原来我的服务器docker服务运行正常&#xff0c;但在某次尝试用时, 根据系统的错误提示执行了snap install docker指令之后&#xff0c; 再执行docker ps命令则提示Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running…

Arm Linux 串口 open 标志引起的问题

一、测试环境 硬件&#xff1a;nuc980 开发版 系统&#xff1a;Linux 4.4 二、open 函数描述 函数 open 的介绍 头文件 #include <fcntl.h>原型 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>int open(const char *pathname,…

LabVIEW开发HVAC总成真空检测及监控系统

在HVAC&#xff08;Heating, Ventilation, and Air Conditioning&#xff0c;供暖、通风与空气调节&#xff09;总成制造过程中&#xff0c;真空检测是确保产品质量的重要环节。真空度是判断HVAC总成密封性能和气密性的关键指标&#xff0c;因此需要一个自动化、精准且可追溯的…

上海泌尿专家来黄山新晨医院义诊,解决患者前列腺等疑难疾病

为满足广大男性对自身健康的关爱、让男性患者不出远门&#xff0c;就能享受到高质量的上海男科诊疗服务&#xff0c;7月28日黄山新晨医院特邀请上海第四人民医院泌尿外科周铁主任团队到院&#xff0c;开展男科疾病义诊活动。 周铁主任为中华医学会泌尿外科分会男科学组委员&…

【docker系列】docker删除指定容器

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

超详细!!!electron-vite-vue开发桌面应用之Electron Forge打包项目(三)

云风网 云风笔记 云风知识库 electronforge可将前端静态页面打包成.exe、.deb和.rpm等&#xff0c;能适配各种平台 一、安装依赖 cd my-app npm install --save-dev electron-forge/cli npm exec --packageelectron-forge/cli -c "electron-forge import"安装后pack…

Prostgresql的Timescaledb插件/扩展部署

背景&#xff1a;研发需求&#xff0c;需要把docker部署得postgresql迁移到新的节点并要求再本地部署&#xff0c;提前查看数据库需要那些插件&#xff0c;并进行安装&#xff0c;docker部署的默认有插件。 版本对比&#xff1a;postgresql版本对应某个Timescaledb版本 我得p…

登录过程记录

过程&#xff1a; 未登录状态打开我的消息页-》调用后端接口查询登录状态->后端接口从cookie里拿lt,判断是否登录-》未登录&#xff0c;携带页面链接(我的消息)跳转passport【单点登录服务】 登录页-》输入验证码提交后-》验证成功-》根据用户信息生成票据-》携带票据和我的…

下载 MC Minecraft Launcher 我的世界 启动器下载

下载地址&#xff1a; https://mc-launcher.com/wp/minecraft/ 我们下期见&#xff0c;拜拜&#xff01;

超详细排序汇总--插入排序类,选择排序类,交换排序类,归并排序,非比较排序

博客中所有代码均在leetcode912. 排序数组中执行 &#xff08;一&#xff09;插入排序类 1、直接插入排序 1&#xff09;思路 当插入第i(i>1)个元素时&#xff0c;前面的array[0],array[1],…,array[i-1]已经排好序&#xff0c;此时用array[i]的排序码与array[i-1],array[…

“论软件体系结构的演化”写作框架,软考高级,系统架构设计师

论文真题 软件体系结构的演化是在构件开发过程中或软件开发完毕投入运行后&#xff0c;由于用户需求发生变化&#xff0c;就必须相应地修改原有软件体系结构&#xff0c;以满足新的变化了的软件需求的过程。体系结构的演化是一个复杂的、难以管理的问题。 请围绕“论软件体系…

【go语言】go-webview2用法(持续更新)

文章目录 背景核心接口和方法扩展接口遗憾的是 背景 目前为止&#xff0c;已经有很多优秀的electron应用。但其特点也很明显&#xff1a;使用htmlcssjs构建的布局很精致&#xff0c;但是体积不容小觑&#xff08;最新版electron-egg打包出来的程序已经300MB&#xff09;。 vs…

共享经济背景下校园、办公闲置物品交易平台-计算机毕设Java|springboot实战项目

&#x1f34a;作者&#xff1a;计算机毕设残哥 &#x1f34a;简介&#xff1a;毕业后就一直专业从事计算机软件程序开发&#xff0c;至今也有8年工作经验。擅长Java、Python、微信小程序、安卓、大数据、PHP、.NET|C#、Golang等。 擅长&#xff1a;按照需求定制化开发项目、 源…

【JAVA入门】Day20 - 正则表达式

【JAVA入门】Day20 - 正则表达式 文章目录 【JAVA入门】Day20 - 正则表达式一、正则表达式使用的注意事项1.1 一个 [ ] 匹配一个字符1.2 表示“或者”的表达式可以再用一个 [ ] 括起来1.3 &&表示“而且”1.4 ^表示“非” 二、预定义字符&#xff08;只能匹配一个字符&a…

PCB结构

覆铜板&#xff08;Copper Clad Laminate&#xff0c;CCL&#xff09;是PCB&#xff08;Printed Circuit Board&#xff0c;印制电路板&#xff09;的主体&#xff0c;由基材和覆在其表面上的一层铜箔组成&#xff0c;基材通常是由增强材料&#xff08;如玻璃纤维织物&#xff…