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

解码数据结构内核链表

普通链表的局限性

普通链表是数据结构中的基础结构,其核心是 “数据 + 指针” 的节点设计,虽概念简单、操作直观,但在工程化应用(尤其是多数据类型场景)中存在通用性缺失的致命缺陷,无法满足复杂开发需求。

核心问题:操作与数据强绑定

普通链表的节点设计将 “具体数据” 与 “链表逻辑(指针操作)” 硬编码在一起,导致针对一种数据类型编写的链表操作函数(如插入、删除),完全无法复用给其他数据类型。

问题示例:普通链表的操作函数局限性

假设定义存储整数的节点node_int和存储字符串的节点node_str,二者的操作函数完全独立,无法共享:

// 存储整数的节点
typedef struct node_int {int data;               // 具体数据(整数)struct node_int *next;  // 链表指针(绑定node_int类型)
} node_int;// 仅支持node_int的插入函数
void insert_int(node_int *head, node_int *new_node) {new_node->next = head->next;head->next = new_node;
}// 存储字符串的节点
typedef struct node_str {char *data;             // 具体数据(字符串)struct node_str *next;  // 链表指针(绑定node_str类型)
} node_str;// 仅支持node_str的插入函数(需重新编写,无法复用insert_int)
void insert_str(node_str *head, node_str *new_node) {new_node->next = head->next;head->next = new_node;
}

问题本质:C 语言缺乏原生泛型机制,无法定义 “通用数据类型” 的链表节点,导致链表操作与数据类型强耦合 —— 每新增一种数据类型,就需重写全套插入、删除、遍历函数,对包含上千种数据类型的工程(如操作系统内核)而言,会造成代码冗余、维护成本激增。

根源分析:数据与逻辑未分离

普通链表的节点结构中,“数据域”(如int data)和 “逻辑域”(如next指针)是不可拆分的整体

  • 逻辑域(指针)的类型由数据域所属的节点类型决定(如node_int*对应node_int节点);
  • 操作函数(如insert)的参数类型与节点类型强绑定,无法兼容其他数据类型的节点。

简言之:普通链表是 “为特定数据定制的链表”,而非 “可适配任意数据的通用链表”。

解决思路:数据与逻辑分离(内核链表的设计思想)

要实现链表的通用性,核心是将 “链表逻辑” 与 “具体数据” 彻底拆分,步骤如下:

  • 抽离通用逻辑:设计一个 “无数据的标准节点”,仅包含链表操作所需的指针(逻辑域),该节点与任何具体数据无关;
  • 嵌入用户数据:用户定义数据节点(大结构体)时,将 “标准节点”(小结构体)作为其一个成员,实现 “通用逻辑嵌入具体数据”;
  • 统一操作接口:基于 “标准节点” 编写全套通用操作(插入、删除、遍历),由于所有用户节点都包含标准节点,这些操作可适配任意用户数据类型。

结构对比示意图

普通链表节点(数据 + 逻辑耦合)内核链表用户节点(数据 + 逻辑分离)
`c struct node { int data; // 数据域
struct node *next; // 逻辑域(绑定node) };`c struct user_node { int data; // 用户数据域(可任意定义) struct list_head list; // 标准逻辑域(通用) };

内核链表的实现(基于 Linux 内核list.h

Linux 内核为解决普通链表的通用性问题,设计了内核链表,其核心代码封装在linux/include/list.h中(该文件同时包含哈希链表,实际开发中可提取内核链表部分为独立头文件,如kernel_list.h)。

内核链表的实现围绕 “标准节点” 展开,所有操作均基于标准节点,完全脱离具体用户数据。

标准节点设计:无数据的双向循环节点

内核链表的核心是struct list_head结构体,它是一个无数据域、仅含双向指针的标准节点,用于承载链表的通用逻辑:

// 来自linux/include/list.h,标准节点(小结构体)
struct list_head {struct list_head *next;  // 后继指针(指向另一个list_head)struct list_head *prev;  // 前驱指针(指向另一个list_head)
};
  • 设计优势
    • 双向指针:支持前后双向遍历,删除节点时无需遍历前驱(仅需修改前后节点指针),效率高于单向链表;
    • 循环结构:初始化后节点的nextprev指向自身,便于构建 “带头节点的循环链表”—— 首尾操作统一,无需判断空链表(空链表时头节点的nextprev仍指向自身)。

用户节点设计:标准节点嵌入数据

用户定义具体数据节点(大结构体)时,需将struct list_head作为成员嵌入,示例如下:

// 示例1:存储学生信息的用户节点
typedef struct student {// 用户数据域(可任意定义,如学号、姓名、成绩)int id;char name[20];float score;// 标准节点域(嵌入的list_head,用于链表操作)struct list_head list;  // 命名为list
} student_t;// 示例2:存储文件信息的用户节点
typedef struct file_info {char filename[50];long size;struct list_head list;  // 同样嵌入标准节点
} file_info_t;

关键:无论用户数据域如何变化,只要包含struct list_head成员,就能使用内核链表的通用操作。

初始化:构建循环链表

内核链表需初始化 “标准节点”,使其nextprev指向自身,形成循环结构。内核提供INIT_LIST_HEAD宏实现初始化。

  • 初始化宏定义

    // 初始化list_head节点:让prev和next均指向自身
    #define INIT_LIST_HEAD(ptr) do { \(ptr)->next = (ptr);          \(ptr)->prev = (ptr);          \
    } while (0)  
    
    • do-while(0)的作用:确保宏在任何场景下(如if语句后)都能被当作单个语句执行,避免语法错误。
  • 头节点初始化(带头节点的链表)

    内核链表通常采用 “带头节点的循环链表”(头节点不存储用户数据,仅用于统一操作),初始化示例:

    // 初始化学生链表的头节点(返回头节点指针,失败返回NULL)
    student_t *student_list_init() {// 为头节点分配内存(头节点也是user_node类型,含list成员)student_t *head = (student_t *)malloc(sizeof(student_t));if (head == NULL) {perror("malloc head failed");return NULL;}// 初始化头节点中的标准节点(list成员)INIT_LIST_HEAD(&head->list);return head;
    }
    
    • 空链表状态:头节点的list.nextlist.prev均指向&head->list

核心能力:大小结构体指针转换(list_entry宏)

内核链表的所有操作(插入、遍历)均针对struct list_head(小结构体),但用户需要访问的是包含数据的 “用户节点”(大结构体)。因此,必须通过小结构体指针反向计算大结构体指针,这一功能由list_entry宏实现。

  • 原理:偏移量计算

    假设:

    • 已知小结构体指针为ptr(如&student->list
    • 大结构体类型为type(如student_t
    • 小结构体在大结构体中的成员名为member(如list

    则大结构体指针 = 小结构体指针 - 小结构体在大结构体中的偏移量(偏移量是固定值,编译时可计算)。

  • 宏定义与解释

    // 从list_head指针(ptr)获取用户节点(type类型)的指针
    // 参数:ptr - list_head指针;type - 用户节点类型;member - list_head在用户节点中的成员名
    #define list_entry(ptr, type, member) \((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))
    
    • (type *)0:假设大结构体的起始地址为 0(仅用于计算偏移量,不访问实际内存);
    • &((type *)0)->member:计算member(小结构体)在大结构体中的偏移量(以字节为单位);
    • (char *)(ptr):将小结构体指针转为char*(确保按字节计算);
    • 最终结果:小结构体指针减去偏移量,得到大结构体的起始地址(即用户节点指针)。
  • 使用示例

    // 已知小结构体指针ptr(如遍历中得到的pos),获取学生节点指针
    struct list_head *ptr = &some_student->list;
    student_t *stu = list_entry(ptr, student_t, list);// 此时可访问用户数据
    printf("学生ID:%d,姓名:%s\n", stu->id, stu->name);
    

节点插入:头插与尾插(基于__list_add

内核链表的插入操作基于内部函数__list_add(实现核心指针操作),对外提供list_add(头插)和list_add_tail(尾插)两个接口,均针对struct list_head操作。

  • 内部核心函数:__list_add

    用于将新节点new插入到prevnext两个节点之间(通用指针操作,与用户数据无关):

    // 静态内部函数(仅在list.h内部使用),不对外暴露
    static inline void __list_add(struct list_head *new,struct list_head *prev,struct list_head *next) {next->prev = new;  // 1. next的前驱指向newnew->next = next;  // 2. new的后继指向nextnew->prev = prev;  // 3. new的前驱指向prevprev->next = new;  // 4. prev的后继指向new
    }
    
  • 头插法:list_add

    将新节点插入到头节点之后(链表首部),适合实现 “栈”(先进后出):

    // 头插:new插入到head和head->next之间
    static inline void list_add(struct list_head *new, struct list_head *head) {__list_add(new, head, head->next);
    }
    
  • 尾插法:list_add_tail

    将新节点插入到头节点之前(链表尾部),适合实现 “队列”(先进先出):

    // 尾插:new插入到head->prev和head之间
    static inline void list_add_tail(struct list_head *new, struct list_head *head) {__list_add(new, head->prev, head);
    }
    
  • 插入示例(学生节点)

    // 创建一个新学生节点
    student_t *create_student(int id, const char *name, float score) {student_t *new_stu = (student_t *)malloc(sizeof(student_t));if (new_stu == NULL) return NULL;// 初始化用户数据new_stu->id = id;strncpy(new_stu->name, name, sizeof(new_stu->name)-1);new_stu->score = score;// 初始化新节点中的标准节点(必须初始化,否则指针混乱)INIT_LIST_HEAD(&new_stu->list);return new_stu;
    }// 插入节点到链表
    int main() {student_t *head = student_list_init();if (head == NULL) return -1;// 头插:插入学生(101, "Alice", 95.5)student_t *stu1 = create_student(101, "Alice", 95.5);list_add(&stu1->list, &head->list);  // 传入标准节点指针// 尾插:插入学生(102, "Bob", 88.0)student_t *stu2 = create_student(102, "Bob", 88.0);list_add_tail(&stu2->list, &head->list);  // 传入标准节点指针return 0;
    }
    

节点删除:安全删除

内核链表提供删除操作,核心是__list_del(断开节点连接),对外暴露list_del(删除节点)和list_del_init(删除后初始化节点,便于复用)。

  • 核心函数定义

    // 内部函数:断开prev和next的连接(不处理被删除节点的指针)
    static inline void __list_del(struct list_head *prev, struct list_head *next) {next->prev = prev;prev->next = next;
    }// 对外接口:删除节点entry(删除后entry的指针变为"毒值",防止野指针访问)
    static inline void list_del(struct list_head *entry) {__list_del(entry->prev, entry->next);// LIST_POISON1/2是内核定义的非法地址,访问会触发错误entry->next = (struct list_head *)LIST_POISON1;entry->prev = (struct list_head *)LIST_POISON2;
    }// 对外接口:删除节点后重新初始化(便于后续复用该节点)
    static inline void list_del_init(struct list_head *entry) {__list_del(entry->prev, entry->next);INIT_LIST_HEAD(entry);  // 重新初始化为循环节点
    }
    
  • 删除示例(结合遍历)

    // 删除ID为101的学生节点
    void delete_student(student_t *head, int target_id) {student_t *pos;  // 遍历用的用户节点指针student_t *n;    // 安全遍历用的临时指针(保存下一个节点)// 安全遍历:支持边遍历边删除(list_for_each_entry_safe)list_for_each_entry_safe(pos, n, &head->list, list) {if (pos->id == target_id) {list_del(&pos->list);  // 删除标准节点free(pos);             // 释放用户节点内存(避免内存泄漏)printf("删除学生ID:%d\n", target_id);return;}}printf("未找到学生ID:%d\n", target_id);
    }
    

链表遍历:四种常用遍历宏

内核链表提供多种遍历宏,覆盖 “正向 / 反向”“安全 / 非安全” 场景,核心是通过list_entry自动转换为用户节点指针。

  • list_for_each:正向遍历标准节点(非安全)

    • 宏定义
    #define list_for_each(pos, head) \for (pos = (head)->next; pos != (head); pos = pos->next)
    
  • 展开逻辑

    for循环为框架,分三步:

    • 初始化pos指向头节点head的下一个标准节点(即链表第一个有效节点);
    • 循环条件pos未回到头节点head(因链表是循环结构,回到头节点即遍历完成);
    • 迭代pos通过next指针移动到下一个标准节点。
  • 核心特性

    • 遍历对象struct list_head类型的标准节点(小结构体),与用户数据无关;
    • 遍历方向:正向(按next指针顺序,从链表首到尾);
    • 安全性:非安全。若遍历中删除当前pos节点,pos->next会被list_del设置为非法地址(如LIST_POISON1),导致下一次迭代pos = pos->next访问非法内存,触发崩溃;
    • 适用场景:仅需读取标准节点信息,不涉及节点删除或修改。
  • list_for_each_safe:正向遍历标准节点(安全)

    • 宏定义
    #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基础上引入临时指针n,分三步:

    • 初始化pos指向头节点下一个标准节点,n提前保存pos的下一个节点(pos->next);
    • 循环条件:同list_for_eachpos未回到头节点;
    • 迭代pos先移动到n(已保存的下一个节点),再更新n为新pos的下一个节点(pos->next)。
  • 核心特性

    • 遍历对象:同list_for_each,仍为标准节点;
    • 遍历方向:正向;
    • 安全性:安全。通过n提前缓存下一个节点地址,即使当前pos节点被删除(pos->next变为非法地址),n仍保存有效地址,确保迭代不中断;
    • 适用场景:遍历过程中需要删除当前标准节点(如清理无效节点)。
  • list_for_each_entry:正向遍历用户节点(非安全)

    • 宏定义
    #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))
    
  • 展开逻辑

    通过list_entry自动将标准节点转换为用户节点,分三步:

    • 初始化pos通过list_entry将头节点下一个标准节点(head->next)转换为用户节点指针;
    • 循环条件:当前用户节点中标准节点的地址(&pos->member)未回到头节点head
    • 迭代pos通过list_entry将当前用户节点中标准节点的下一个节点(pos->member.next)转换为下一个用户节点指针。
  • 核心特性

    • 遍历对象:用户节点(大结构体,如student_t),直接关联用户数据;
    • 遍历方向:正向;
    • 安全性:非安全。若删除当前pos节点,其内部标准节点的next指针(pos->member.next)会变为非法地址,导致下一次list_entry转换时基于非法地址计算用户节点指针,触发崩溃;
    • 适用场景:仅需读取用户节点数据(如统计、打印),不涉及节点删除或修改。
  • list_for_each_entry_safe:正向遍历用户节点(安全)

    • 宏定义
    #define list_for_each_entry_safe(pos, n, head, member)            \for (pos = list_entry((head)->next, typeof(*pos), member),    \  n = list_entry(pos->member.next, typeof(*n), member);    \  &pos->member != (head);                     \  pos = n, n = list_entry(n->member.next, typeof(*n), member))
    
  • 展开逻辑

    list_for_each_entry基础上引入临时用户节点指针n,分三步:

    • 初始化pos转换为第一个用户节点,n提前通过list_entry转换为pos的下一个用户节点;
    • 循环条件:同list_for_each_entry&pos->member未回到头节点;
    • 迭代pos先移动到n(已保存的下一个用户节点),再更新n为新pos的下一个用户节点(通过n->member.next转换)。
  • 核心特性

    • 遍历对象:用户节点;
    • 遍历方向:正向;
    • 安全性:安全。n提前缓存下一个用户节点地址,即使当前pos节点被删除,n仍基于有效地址转换,确保迭代不中断;
    • 适用场景:遍历过程中需要删除当前用户节点(如筛选并移除无效数据)。
  • list_for_each_prev:反向遍历标准节点(非安全)

    • 宏定义
    #define list_for_each_prev(pos, head) \for (pos = (head)->prev; pos != (head); pos = pos->prev)
    
  • 展开逻辑

    for循环为框架,与list_for_each方向相反,分三步:

    • 初始化pos指向头节点head的前一个标准节点(即链表最后一个有效节点);
    • 循环条件pos未回到头节点head
    • 迭代pos通过prev指针移动到上一个标准节点。
  • 核心特性

    • 遍历对象:标准节点;
    • 遍历方向:反向(按prev指针顺序,从链表尾到首);
    • 安全性:非安全。若删除当前pos节点,pos->prev会被list_del设置为非法地址,导致下一次迭代pos = pos->prev访问非法内存;
    • 适用场景:仅需反向读取标准节点信息,不涉及节点删除或修改。
  • 遍历宏对比与说明

    宏名称功能安全与否(是否支持边遍历边删除)适用场景
    list_for_each正向遍历标准节点(list_head非安全仅遍历,不修改节点
    list_for_each_safe正向遍历标准节点安全(用 n 保存下一个节点)遍历中删除标准节点
    list_for_each_entry正向遍历用户节点非安全仅遍历,不修改用户节点
    list_for_each_entry_safe正向遍历用户节点安全(用 n 保存下一个用户节点)遍历中删除用户节点
    list_for_each_prev反向遍历标准节点非安全反向遍历,不修改节点

遍历示例(用户节点遍历)

// 遍历学生链表,打印所有学生信息
void print_student_list(student_t *head) {student_t *pos;  // 用户节点指针// 非安全遍历(仅打印,不删除)list_for_each_entry(pos, &head->list, list) {printf("ID:%d,姓名:%s,成绩:%.1f\n", pos->id, pos->name, pos->score);}
}// 反向遍历
void print_student_list_rev(student_t *head) {struct list_head *pos;  // 标准节点指针student_t *stu;         // 用户节点指针// 反向遍历标准节点,再转换为用户节点list_for_each_prev(pos, &head->list) {stu = list_entry(pos, student_t, list);printf("ID:%d,姓名:%s,成绩:%.1f\n", stu->id, stu->name, stu->score);}
}

辅助宏:链表判空(list_empty

判断链表是否为空(头节点的next是否指向自身):

// 若链表为空,返回1;否则返回0
#define list_empty(ptr) ((ptr)->next == (ptr) && (ptr)->prev == (ptr))// 使用示例
if (list_empty(&head->list)) {printf("链表为空\n");
} else {printf("链表非空\n");
}

内核链表与普通链表的对比

对比维度普通链表内核链表
通用性差(操作与数据强绑定)强(适配任意含list_head的节点)
操作函数复用性无(每种数据类型需重写)完全复用(基于list_head的通用操作)
遍历效率单向遍历(删除需遍历前驱)双向遍历(删除无需遍历前驱)
空链表处理需判断头节点next是否为 NULL统一(头节点next/prev指向自身)
内存开销每个节点仅含自身数据 + 指针每个节点需额外嵌入list_head(8 字节,64 位系统)
适用场景简单单数据类型场景(如单链表存整数)复杂多数据类型场景(如内核、大型工程)
http://www.dtcms.com/a/418455.html

相关文章:

  • 郑州设计网站公司东莞建设银行电话号码
  • 个人网站的建设目标绥德网站建设
  • Elasticsearch面试精讲 Day 23:安全认证与权限控制
  • 学习嵌入式的第四十三天——ARM——I2C
  • 玳瑁的嵌入式日记---0928(ARM--UART)
  • CentOS 8 部署 Zabbix 7.0 LTS 完整流程(PostgreSQL)及不同系统agent安装
  • 网站开发环境设计怎么在百度做网站推广
  • Langchain+Neo4j+Agent 的结合案例-电商销售
  • 计算机网络【第二章-物理层】
  • 计算机网络【第一章-计算机网络体系结构】
  • mac 安装npm之后,其他终端无法访问
  • 购物网站哪个最好记账公司如何拉客户
  • 汽车网络安全 CyberSecurity ISO/SAE 21434 测试之四
  • ARM芯片架构之APB,ATB总线
  • 死信队列(Dead-Letter Queue,DLQ)
  • Python 2025:网络安全与智能防御新范式
  • 2025软考甄选范文“论分布式事务及其解决方案”,软考高级,系统架构设计师论文
  • 学习前端开发的网站占酷设计网站官网入口
  • 【R语言验证统计量的渐进分布】
  • starrocks查询伪代码
  • R语言中的S3 泛型与方法
  • 安全运维实战指南:常见病毒防护操作手册
  • 爬虫逆向——RPC技术
  • tldr的安装与使用
  • Imatest-Star模块(西门子星图)
  • Unity 3D笔记——《B站阿发你好》
  • R语言从入门到精通Day3之【包的使用】
  • rocr专栏介绍
  • 济南网站建设 推搜点搜索优化的培训免费咨询
  • pc网站建设哪个好重庆seo网站运营