当前位置: 首页 > news >正文

Linux内核深入学习(4)——内核常见的数据结构之链表

Linux内核深入学习(3)——内核常见的数据结构1链表

前言

​ 我们的常用的数据结构,一个是我们的list,另一个是笔者打算之后介绍的rb_tree,也就是红黑树。这里我们先从链表开始说起。

内核的链表是双循环链表

​ 双循环链表是一种常见的数据结构,它在计算机科学中扮演着灵活组织数据的角色。想象一条由许多环节连接而成的链条,每个环节不仅知道自己的下一个环节在哪里,还能记住前一个环节的位置。这种链条首尾相接,形成一个闭环,就像游乐场的环形小火车,无论从哪个车厢出发,都能沿着轨道绕回起点。这就是双循环链表最直观的样貌。

​ 每个节点都像一个小型数据仓库,存储着有价值的信息,同时携带着两把钥匙:一把指向它的前驱节点,另一把指向后继节点。这种双向连接的特性让数据访问变得自由灵活。当我们需要查找某个数据时,既可以从头节点顺时针寻找,也能反向逆时针回溯。这种双向性在需要频繁前后移动的场景中尤其有用,比如音乐播放器的播放列表,既支持切到下一首歌,也能随时返回上一曲。

​ 与传统链表不同,双循环链表的首尾节点通过指针紧紧相扣。当链表为空时,头节点的前后指针都指向自己,就像一个孤独的哨兵静静守护着空荡的城堡。随着数据节点的加入,这个闭环会逐渐扩展,但始终保持着循环的特性。这样的设计消除了链表头尾的边界感,使得所有节点都处于平等的位置,操作时无需特别处理首尾边界条件,这在某些算法实现中能显著简化代码逻辑。

​ 插入和删除操作是双循环链表的拿手好戏。要新增一个节点,只需调整相邻节点的指针指向,就像在铁链中间插入新环节时重新焊接两端的连接点。删除节点时同样优雅,断开目标节点与邻居的连接后,前后节点会自然牵起手来,整个过程不需要大规模移动数据。这种高效的重构能力让它在需要频繁修改数据的场景中表现出色,比如实时更新的缓存系统或动态规划的解决方案。

​ 这里,我们的内核采用的就是经典的双循环链表。很快,我们就来看看内核时如何抽象的。

list head是什么

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

​ 我们知道,数据结构本身跟数据自身的属性是没有关系的,换而言之,数据本身的组织不应当被采取的容器所影响。你看,C++的std::list<T>是使用泛型这样做到的,那问题来了,C咋办呢?我们总不能强硬的将每一个我们关心成一条链的数据结构都整出来重复的链表代码吧!那太冗余!

​ 为此,我们的做法是——希望让每一个使用链表组织的数据内部,侵入一个链表的节点结构体,想要找到链表上的存储的数据,只需要反推这个链表所在的位置对应的偏移头,偏移节点所在结构体的偏移量就能拿到原本结构的头

+===========================+ <===== get the struct itself
|						  |
|	int xxx;			   |
|						  |
|---------------------------| <==== Offset of the struct list_head
| 						  |
| 	struct list_head	    |
|---------------------------|
....

​ 为此,只需要将list_head的地址剪掉list_head的offset就能拿到结构体了!就是这个原理!现在,我们就能将数据本身和链表解耦合开来了!

初始化

  • 静态初始化
    使用宏 LIST_HEAD(name) 可在编译期声明并初始化一个空链表头:

    LIST_HEAD(my_list);
    

    等价于:

    struct list_head my_list = { &my_list, &my_list };
    

    我们来看看实际上是如何做的:

    #define LIST_HEAD_INIT(name) { &(name), &(name) }#define LIST_HEAD(name) \struct list_head name = LIST_HEAD_INIT(name)
    

    嗯很,就是静态的初始化结构体嘛!没啥新鲜的!

  • 动态初始化
    对于已声明的 struct list_head head;,可在运行期调用:

    INIT_LIST_HEAD(&head);
    

    将其 nextprev 指向自身,实现空表状态

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

    ​ 这里的write_once是一次赋值大小检查和一次真正的赋值行为封装宏。

核心操作函数

核心操作都在 include/linux/list.hlib/list.c(旧版中为 lib/list.c,新版本都为宏实现)中定义,主要包括:

  • 添加节点

    • list_add(new, head):将 new 添加到 head 之后,常用于栈(LIFO)场景。
    • list_add_tail(new, head):将 new 添加到 head 之前,实现队列(FIFO)效果
    /** Insert a new entry between two known consecutive entries.** This is only for internal list manipulation where we know* the prev/next entries already!*/
    static inline void __list_add(struct list_head *new,struct list_head *prev,struct list_head *next)
    {if (!__list_add_valid(new, prev, next))return;next->prev = new;new->next = next;new->prev = prev;WRITE_ONCE(prev->next, new);
    }/*** list_add - add a new entry* @new: new entry to be added* @head: list head to add it after** Insert a new entry after the specified head.* This is good for implementing stacks.*/
    static inline void list_add(struct list_head *new, struct list_head *head)
    {__list_add(new, head, head->next);
    }
    

    ​ 我们的内核喜欢使用__TT隔离实现,这样的方式来保证API的稳定,内部函数__list_add如同精密的手术刀,负责在已知的相邻节点prevnext之间嵌入新节点new。它先通过__list_add_valid进行安全性校验,随后像编织绳索般调整指针:先将新节点的nextnext节点挂钩,再将新节点的prevprev节点相连,最后用WRITE_ONCE确保前驱节点的next指针原子性地指向新节点,防止多线程环境下的数据竞争。

    外层函数list_add则是面向开发者的友好接口,它将新节点直接插入到链表头节点head之后。

  • 删除节点

    • list_del(entry):将 entry 从所属链表中移除,内部调用 __list_del(entry->prev, entry->next) 并将 entry 指针置为 UNDEFINED。
    • list_del_init(entry):移除后重新初始化该节点,使其成为单节点空链表。
    /** Delete a list entry by making the prev/next entries* point to each other.** This is only for internal list manipulation where we know* the prev/next entries already!*/
    static inline void __list_del(struct list_head * prev, struct list_head * next)
    {next->prev = prev;WRITE_ONCE(prev->next, next);
    }/** Delete a list entry and clear the 'prev' pointer.** This is a special-purpose list clearing method used in the networking code* for lists allocated as per-cpu, where we don't want to incur the extra* WRITE_ONCE() overhead of a regular list_del_init(). The code that uses this* needs to check the node 'prev' pointer instead of calling list_empty().*/
    static inline void __list_del_clearprev(struct list_head *entry)
    {__list_del(entry->prev, entry->next);entry->prev = NULL;
    }static inline void __list_del_entry(struct list_head *entry)
    {if (!__list_del_entry_valid(entry))return;__list_del(entry->prev, entry->next);
    }/*** list_del - deletes entry from list.* @entry: the element to delete from the list.* Note: list_empty() on entry does not return true after this, the entry is* in an undefined state.*/
    static inline void list_del(struct list_head *entry)
    {__list_del_entry(entry);entry->next = LIST_POISON1;entry->prev = LIST_POISON2;
    }
    

    基础指针调整 (__list_del)
    内部函数,通过直接操作相邻节点的指针完成删除:

    • 将后驱节点 nextprev 指向当前节点的前驱 prev
    • 使用 WRITE_ONCE 宏原子性地更新前驱节点 prevnext 指向 next
      此操作仅解除目标节点与链表的连接,不修改目标节点自身指针,适用于已知前后节点的场景。

    清除前向指针 (__list_del_clearprev)
    在基础删除后,显式将目标节点的 prev 置为 NULL。该设计针对网络模块的 per-cpu 链表优化,牺牲 list_empty 的可用性来减少一次写操作,调用方需通过检测 prev 是否为空判断节点状态。

    带校验的删除 (__list_del_entry)
    对外部暴露的安全接口:

    • 先通过 __list_del_entry_valid 验证节点合法性(防止无效操作)
    • 再调用基础 __list_del 执行指针调整
      提供基本的防错机制,确保节点确实存在于链表中。

    完全节点隔离 (list_del)
    标准删除接口:

    • 调用 __list_del_entry 解除节点与链表的关联
    • 将节点的 next/prev 赋值为 LIST_POISON(特殊标记值,通常为非法地址)
    • 使被删节点进入"毒化"状态:既无法通过常规方法访问链表,又能在调试时触发错误显式暴露悬空指针访问。
  • 移动与拼接

    • list_move(entry, head)list_move_tail(entry, head):分别将单节点移到另一链表的 head 之后或之前。

    • list_splice(list, head)list_splice_tail(list, head):将整个链表拼接到目标链表中,原链表保留或变为空链表(视 API 而定)

      /*** list_move - delete from one list and add as another's head* @list: the entry to move* @head: the head that will precede our entry*/
      static inline void list_move(struct list_head *list, struct list_head *head)
      {__list_del_entry(list);list_add(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*/
      static inline void list_move_tail(struct list_head *list,struct list_head *head)
      {__list_del_entry(list);list_add_tail(list, head);
      }
      

      list_move
      将节点从原链表删除,并插入到目标头节点之后(成为新链表的第一个节点)。
      内部调用 __list_del_entry 删除节点,再通过 list_add 将其添加到目标位置。

      list_move_tail
      将节点从原链表删除,并插入到目标头节点之前(成为新链表的最后一个节点)。
      同样先删除节点,再通过 list_add_tail 将其添加到目标链表的末尾。

      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_splice - join two lists, this is designed for stacks* @list: the new list to add.* @head: the place to add it in the first 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);
      }/*** list_splice_tail - join two lists, each list being a queue* @list: the new list to add.* @head: the place to add it in the first 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);
      }

      __list_splice (内部函数)
      核心功能是将源链表 list 的全部节点插入到目标位置 prevnext 之间。

      • 获取源链表的首节点 first (list->next) 和尾节点 last (list->prev)
      • firstprev 指向目标前驱节点 prev,同时 prevnext 指向 first
      • lastnext 指向目标后继节点 next,同时 nextprev 指向 last
        通过 4 次指针操作完成两个链表的无缝衔接。

      list_splice
      将源链表 list 拼接到目标链表 head 之后(插入到 headhead->next 之间)。

      • 先检查源链表是否非空 (list_empty 判断)
      • 调用 __list_splice(list, head, head->next) 实现拼接
        典型应用场景是栈的合并,新链表节点会出现在目标链表头部之后。

      list_splice_tail
      将源链表 list 拼接到目标链表 head 之前(插入到 head->prevhead 之间)。

      • 同样先检查源链表非空
      • 调用 __list_splice(list, head->prev, head) 实现拼接
        适用于队列操作,新链表节点会出现在目标链表尾部。
  • 遍历宏

    • list_for_each(pos, head):以 struct list_head *pos 形式遍历所有节点。
    • list_for_each_entry(entry, head, member):直接以外层结构指针形式遍历,内置 container_of 安全转换,推荐使用
    /*** list_for_each	-	iterate over a list* @pos:	the &struct list_head to use as a loop cursor.* @head:	the head for your list.*/
    #define list_for_each(pos, head) \for (pos = (head)->next; !list_is_head(pos, (head)); pos = pos->next)/*** list_for_each_reverse - iterate backwards over a list* @pos:	the &struct list_head to use as a loop cursor.* @head:	the head for your list.*/
    #define list_for_each_reverse(pos, head) \for (pos = (head)->prev; pos != (head); pos = pos->prev)
    /*** list_for_each_entry	-	iterate over list of given type* @pos:	the type * to use as a loop cursor.* @head:	the head for your list.* @member:	the name of the list_head within the struct.*/
    #define list_for_each_entry(pos, head, member)				\for (pos = list_first_entry(head, typeof(*pos), member);	\!list_entry_is_head(pos, head, member);			\pos = list_next_entry(pos, member))/*** list_for_each_entry_reverse - iterate backwards over list of given type.* @pos:	the type * to use as a loop cursor.* @head:	the head for your list.* @member:	the name of the list_head within the struct.*/
    #define list_for_each_entry_reverse(pos, head, member)			\for (pos = list_last_entry(head, typeof(*pos), member);		\!list_entry_is_head(pos, head, member); 			\pos = list_prev_entry(pos, member))
    

    ​ 这些就是典型的使用for爬取链表,当然,还是很简单的,这里不做讲解

相关文章:

  • WebRTC技术EasyRTC嵌入式音视频通信SDK助力智能电视搭建沉浸式实时音视频交互
  • 正点原子STM32新建工程
  • 容器化-K8s-镜像仓库使用和应用
  • 【Linux学习】Ubuntu对用户进行管理
  • 云原生安全:错误策略S3存储桶ACL设置为Everyone:FullControl
  • CSS新增表单属性及结构伪类选择器(nth-child)
  • 深入探究AKS Workload Identity
  • AT 指令详解:基于 MCU 的通信控制实战指南AT 指令详解
  • Linux多进程 写时拷贝 物理地址和逻辑地址
  • MATLAB中NLP工具箱支持聚类算法
  • 第 1 章:数字 I/O 与串口通信(GPIO UART)
  • 音视频之H.265/HEVC率失真优化
  • 7-2 银行业务队列简单模拟
  • HarmonyOS:应用及文件系统空间统计
  • 山东大学计算机图形学期末复习13——CG14上
  • 智能投影仪行业2025数据分析报告
  • 吉林省建筑工程专业技术人员职称评审实施办法
  • 牛客网 NC274692 题解:素世喝茶
  • 基于亚博K210开发板——安全散列算法加速器测试
  • Windows逆向工程提升之PE文件的基本概念
  • 确诊前列腺癌后,拜登首次发声
  • 半年不到再换岗:伊春市委常委、政法委书记方春彪任伊春森工集团党委书记
  • 新冠阳性率升高,专家:新冠变异株致病力没太多变化
  • 浙江一教师被指殴打并威胁小学生,教育局通报涉事人被行拘
  • 哈马斯与以色列在多哈举行新一轮加沙停火谈判
  • “80后”萍乡市安源区区长邱伟,拟任县(区)委书记