漫谈Linux内核哈希表(1)

简介: 关于哈希表,在内核里设计两个很重要的数据结构:    哈希链表节点: 点击(此处)折叠或打开 /*Kernel Version : 3.

关于哈希表,在内核里设计两个很重要的数据结构:
   
哈希链表节点

点击(此处)折叠或打开

  1. /*Kernel Version : 3.4.x [include/linux/types.h]*/
  2. struct hlist_node {
  3.     struct hlist_node *next, **pprev;
  4. };
    可以看到哈希节点和内核普通双向链表的节点唯一的区别就在于,前向节点 pprev 是个两级指针,至于为什么这样设计而不采用 struct list_head{} 来作为哈希链表的节点,我们后面会详细介绍。另外一个重要的数据结构是,哈希链表的表头。

   
哈希链表 表头

点击(此处)折叠或打开

  1. /*Kernel Version : 3.4.x [include/linux/types.h]*/
  2. struct hlist_head {
  3.     struct hlist_node *first;
  4. };
    因为哈希链表并不需要 双向循环 的技能,它一般适用于单向散列的场景。所以,为了减少开销,并没有用 struct hlist_node{} 来代表哈希表头,而是重新设计 struct hlist_head{} 这个数据结构。此时,一个哈希表头就只需要 4Byte 了,相比于 struct hlist_node{} 来说,存储空间已经减少了一半。这样一来,在需要大量用到哈希链表的场景,其存储空间的节约是非常明显的,特别是在嵌入式设备领域。


   接下来
,我们来重点回答一下哈希节点里那个两级指针的问题。先讲个小插曲,记得本人当年刚参加工作时,导师给安排了一个活儿,那时候年轻气盛、血气方刚,没一会儿功夫,三下五除二就搞定了。然后拿着自己的“杰作”去师傅看,师傅瞄了一眼说,你这函数简直是一坨shi(和乔老爷当年骂另外一个程序员的用词、语气差不多),谁让你函数入参传个三级指针进去的?这段代码TM能维护么?谁看得懂?完了之后感觉自己还受了莫大的委屈一样,不过谁的人生没有那么点波澜壮阔的过往呢,就像有句名言说的:程序写出来是给人看的,顺带能在机器上运行。OK,那这个故事跟我们要介绍的哈希节点的关系在哪儿呢?没错,就是struct hlist_node{}里那个前向的两级指针的存在意义。

    关于两级指针的目的与意义,让
我们采用反证法来看看,如果struct hlist_node{}被设计成如下一级指针的样子,会发生什么:

点击(此处)折叠或打开

  1. struct hlist_node {
  2.     struct hlist_node *next, *pprev;
  3. };
    假如我们现在已经有一个哈希链表了myhlist(先别管这个链表是怎么来的),链表里有4个节点node1~node4:

    
   然后就有以下两个问题跟着冒出来:
   
1)、在往哈希链myhlist里插入node1时必须这么写:

点击(此处)折叠或打开

  1. mylist.first = node1;
  2. node1->pprev=( struct hlist_node*)&mylist;
   除此之外,在插入 node2~node4 以及后续其他节点时 ( 假如按顺序插入的话 ) ,写法如下(X>=2

点击(此处)折叠或打开

  1. node[X]->next = node[X+1];
  2. node[X]->pprev = node[X-1];

简而言之啥意思呢?往哈希链表里插入元素时,如果在表头的第一个位置上插入元素,和插入在哈希链表的其他位置上的代码处理逻辑是不一样的。因为哈希表头是list_head类型,而其他节点都是list_node类型。

   2
)、同样,如果删除节点时,对于非首节点,以node2为例:

点击(此处)折叠或打开

  1. node2->pprev->next = node2->next;
  2. node2->next->pprev = node2->pprev;
    如果要删除首节点 node1 呢,则写法如下:

点击(此处)折叠或打开

  1. ((struct hlist_head*)(node1->pprev))->first = node1->next;
  2. node1->next->pprev = ( struct hlist_node*)&mylist或者 node1->next->pprev = node1->pprev;
    很明显,内核开发者们怎么会容许这样的代码存在,而且还要充分考虑效率的问题。那么,当 hlist_node.pprev 被设计成两级指针后有啥好处?
    还是以删除节点为例,如果要删除首节点,因为node1->pprev里保存的是myhlist的地址,而myhlist.first永远都指向哈希链表的第一个节点,我们要间接改变表头里的hlist_node类型的first指针的值,能想到的最直接的办法当然是二级指针,这是两级指针的宿命所决定的,为了间接改变一级指针所指的内存地址的场景。这样一来,node节点里的pprev其实指向的是其前一个节点里的第一个指针元素的地址。对于hlist_head来说,它里面只有一个指针元素,就是first指针;而对于hlist_node来说,第一个指针元素就是next。具体如下所示:

所以,记住,当我们在代码中看到类似与*(hlist_node->pprev)这样的代码时,我们心里应该清楚,此时正在哈希表里操作当前节点前一个节点里的第一个指针元素所指向的内存地址,只是以间接的方式实现罢了。那么回到删除哈希链表节点的场景,当删除首节点时,此时情况就变成了:

点击(此处)折叠或打开

  1. *(node1->pprev) = node1->next;
  2. node1->next->pprev = node1->pprev;
    删除非首节点的情况也一样:

点击(此处)折叠或打开

  1. *(node2->pprev) = node2->next;
  2. node2->next->pprev = node2->pprev;
    这样一来,我们对hlist_node里的谅解指针pprev的存在价值与意义应该很明白了,以后不至于再被眼花缭乱的取地址操作符给弄晕了。OK,扯了这么多,让我们看看内核是如何实现删除哈希链表里的节点的__hlist_del():
   
   
大家自行将上述函数里的入参n换成node2,最终和我们上面推断的结果是一致的:
   
    在标准的哈希链表里,因为最后一个节点的 next=NULL ,所以在执行第二句有效代码前首先要对当前节点的 next 值进行判断才行。
    内核提供了 hlist_add_head() ,用于实现向哈希链表里插入节点:

点击(此处)折叠或打开

  1. hlist_add_head(struct hlist_node *n, struct hlist_head *h)
    其中n表示待插入的节点,h表示哈希链表表头。在刚初始化完哈希表myhlist的情况下,依次调用四次hlist_add_head(),每次调用后myhlist哈希表的情况如下:
    ( 备注:双箭头表示两级指针,单箭头表示一级指针 )
   
理论上说,内核应该再提供一个对称的方法hlist_add_tail()才算完美,用于将哈希链表操作成如下的样子:


   还有
hlist_add_behind()hlist_add_before(),在3.17版本之前hlist_add_behind()的名字还是hlist_add_after(),不过作用都一样。两个函数原型分别如下:

点击(此处)折叠或打开

  1. hlist_add_before(struct hlist_node *n,struct hlist_node *next);
  2. hlist_add_behind(struct hlist_node *n,struct hlist_node *prev);
    其中 n 是待插入的节点, next 或者 prev 都是 n 的相对位置参考节点,其作用分别是:
   
hlist_add_before() :在 next 节点的 前面 插入 n 节点;
 
hlist_add_behind():在prev节点的 后面 插入n节点;

    接下来, 让我们…..

    1) 、在 node4 节点的 前面 插入 node3
    注意 hlist_add_before() 有个约束条件,那就是 next!=NULL。

   
2) 、在 node1 的节点 后面 插入 node5
    同样的约束条件也适用于hlist_add_behind(),即prev!=NULL
   未完,待续...
目录
相关文章
|
15天前
|
安全 Linux 编译器
探索Linux内核的奥秘:从零构建操作系统####
本文旨在通过深入浅出的方式,带领读者踏上一段从零开始构建简化版Linux操作系统的旅程。我们将避开复杂的技术细节,以通俗易懂的语言,逐步揭开Linux内核的神秘面纱,探讨其工作原理、核心组件及如何通过实践加深理解。这既是一次对操作系统原理的深刻洞察,也是一场激发创新思维与实践能力的冒险。 ####
|
3天前
|
算法 Linux 开发者
深入探究Linux内核中的内存管理机制
本文旨在对Linux操作系统的内存管理机制进行深入分析,探讨其如何通过高效的内存分配和回收策略来优化系统性能。文章将详细介绍Linux内核中内存管理的关键技术点,包括物理内存与虚拟内存的映射、页面置换算法、以及内存碎片的处理方法等。通过对这些技术点的解析,本文旨在为读者提供一个清晰的Linux内存管理框架,帮助理解其在现代计算环境中的重要性和应用。
|
1天前
|
缓存 网络协议 Linux
Linux操作系统内核
Linux操作系统内核 1、进程管理: 进程调度 进程创建与销毁 进程间通信 2、内存管理: 内存分配与回收 虚拟内存管理 缓存管理 3、驱动管理: 设备驱动程序接口 硬件抽象层 中断处理 4、文件和网络管理: 文件系统管理 网络协议栈 网络安全及防火墙管理
18 4
|
3天前
|
人工智能 算法 大数据
Linux内核中的调度算法演变:从O(1)到CFS的优化之旅###
本文深入探讨了Linux操作系统内核中进程调度算法的发展历程,聚焦于O(1)调度器向完全公平调度器(CFS)的转变。不同于传统摘要对研究背景、方法、结果和结论的概述,本文创新性地采用“技术演进时间线”的形式,简明扼要地勾勒出这一转变背后的关键技术里程碑,旨在为读者提供一个清晰的历史脉络,引领其深入了解Linux调度机制的革新之路。 ###
|
5天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
29 4
|
6天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
9天前
|
缓存 Linux
揭秘Linux内核:探索CPU拓扑结构
【10月更文挑战第26天】
25 1
|
9天前
|
缓存 运维 Linux
深入探索Linux内核:CPU拓扑结构探测
【10月更文挑战第18天】在现代计算机系统中,CPU的拓扑结构对性能优化和资源管理至关重要。了解CPU的核心、线程、NUMA节点等信息,可以帮助开发者和系统管理员更好地调优应用程序和系统配置。本文将深入探讨如何在Linux内核中探测CPU拓扑结构,介绍相关工具和方法。
10 0
|
18天前
|
网络协议 Linux 调度
深入探索Linux操作系统的心脏:内核与系统调用####
本文旨在揭开Linux操作系统中最为核心的部分——内核与系统调用的神秘面纱,通过生动形象的语言和比喻,让读者仿佛踏上了一段奇妙的旅程,从宏观到微观,逐步深入了解这两个关键组件如何协同工作,支撑起整个操作系统的运行。不同于传统的技术解析,本文将以故事化的方式,带领读者领略Linux内核的精妙设计与系统调用的魅力所在,即便是对技术细节不甚了解的读者也能轻松享受这次知识之旅。 ####
|
14天前
|
缓存 算法 安全
深入理解Linux操作系统的心脏:内核与系统调用####
【10月更文挑战第20天】 本文将带你探索Linux操作系统的核心——其强大的内核和高效的系统调用机制。通过深入浅出的解释,我们将揭示这些技术是如何协同工作以支撑起整个系统的运行,同时也会触及一些常见的误解和背后的哲学思想。无论你是开发者、系统管理员还是普通用户,了解这些基础知识都将有助于你更好地利用Linux的强大功能。 ####
25 1