Linux中的哈希表:基于双链表的内核模块

news2024/12/24 2:19:01

1. 前言

Linux内核中选取双向链表作为其基本的数据结构,并将其嵌入到其他的数据结构中,使得其他的数据结构不必再一一实现其各自的双链表结构。实现了双链表结构的统一,同时可以演化出其他复杂数据结构。本文对linux中基于双链表实现的哈希表进行分析,并编写内核模块,调用其中的函数和宏,实现哈希表的建立和查找。

2. 哈希表的定义

散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存储存位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来进行访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。

通过哈希函数使用关键字计算存储地址的时候,不可避免地会产生冲突,通常处理冲突的方法有:开放定地址法(线性探测、平方探测)、单独链表法、双散列、再散列。linux中使用了其中的单独链表法,即利用了我们上面介绍的双向链表实现,将散列到同一个存储位置的所有元素保存在一个链表中。

linux中关于哈希表结构体的定义可以从/usr/src/linux-headers-5.4.0-48-generic/include/linux/types.h文件中找到。

struct hlist_head {
	struct hlist_node *first;
};
struct hlist_node {
	struct hlist_node *next, **pprev;
};

可以看到哈希表包含两个数据结构,一个是哈希链表节点hlist_node,另一个是哈希表头hlist_head。可以看到哈希节点hlist_node和内核普通双向链表的节点唯一的区别就在于,前向节点pprev是个两级指针。同时并没有使用hlist_node作为哈希表头,而是重新定义了hlist_head结构体,这是因为哈希链表并不需要双向循环,为了节省空间使用一个指针first指向该哈希表的第一个节点就可以了。整个哈希表结构如下图所示,其中ppre是个二级指针,它指向前一个节点的第一个指针变量,例如node1的ppre指向mylist的first指针,node2的ppre指向node1的next指针。

之所以使用ppre二级指针是为了避免在收集节点之后插入删除节点和在其他位置插入删除节点实现逻辑的不同,读者可以将ppre改成一级指针指向前一个节点,就可以发现实现逻辑的不同。

3. 哈希表的声明和初始化宏

Linux的链表和散列表的操作函数的定义在/usr/src/linux-headers-5.4.0-48-generic/include/linux/list.h文件中,接下来就打开这个文件看一下hlist数据结构的操作函数和宏。

#define HLIST_HEAD_INIT { .first = NULL }
#define HLIST_HEAD(name) struct hlist_head name = {  .first = NULL }
#define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL)

这三个初始化宏都是建立一个hlist_head结构体,并把first成员设置为NULL。

初始化hlist_node结构体,把两个成员变量赋值为NULL。

static inline void INIT_HLIST_NODE(struct hlist_node *h)
{
	h->next = NULL;
	h->pprev = NULL;
}

4. 在哈希表中增加节点

在内核代码list.h中增加节点的函数为:

tatic inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h)

static inline void hlist_add_before(struct hlist_node *n,
					struct hlist_node *next)

static inline void hlist_add_behind(struct hlist_node *n,
				    struct hlist_node *prev)

static inline void hlist_add_fake(struct hlist_node *n)

hlist_add_head是把一个哈希链表的节点插入到哈希链表的头节点的后边,也就是头插法。传入了哈希表头h和待插入的节点n,首先得到hlist_head的first成员,就是后边的节点的指针,这个节点可能是NULL,然后新插入的节点的next指向first后边的节点,如果first不为空,也就是后边有节点存在,head的后边的节点的pprev成员就指向新插入的节点的next成员的地址,head的first就指向新插入的节点,新插入节点的pprev成员指向head的first成员的地址。

内核资料直通车:最新Linux内核源码资料文档+视频资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h)
{
	struct hlist_node *first = h->first;
	n->next = first;
	if (first)
		first->pprev = &n->next;
	h->first = n;
	n->pprev = &h->first;
}

每次插入一个节点后,哈希表的存储情况如下图所示。

hlist_add_before作用是把一个节点插入到一个哈希链表的节点的前边,首先把将要插入的节点的pprev成员变量指向next的前边的节点,要插入的节点的next指向下一个节点,然后next节点的pprev就要指向已经插入的节点的next节点的地址,已经插入的节点的pprev指向的前一个节点的值就要变成已经插入节点的地址。

static inline void hlist_add_before(struct hlist_node *n,
					struct hlist_node *next)
{
	n->pprev = next->pprev;
	n->next = next;
	next->pprev = &n->next;
	*(n->pprev) = n;
}

5. 遍历哈希表

list.h中定义了如下遍历链表的宏,\代表换行

这个宏是对哈希链表进行遍历的宏,pos代表一个hlist_node结构体指针,head代表hlist_head结构体,就是哈希链表的头。得到pos后,在宏展开后就可以在循环体中取到结构体具体的数值。

6. 哈希表的应用

6.1 搭建Clion内核模块开发环境

Clion用CMake构建C或者C++工程(20版本的CLion也支持自定义Makefile),利用CMake的配置文件创建编辑器本身的代码环境,比如子项目,比如预处理宏等,这跟IntelliJ Idea利用Maven、Gradle构建项目管理依赖库是同样的道理。

在Ubuntu系统下启动Clion新建工程,选择C executable项目,C语言标准选择99。

项目新建后会有一个CMakeLists.txt文件,main.c文件,我们可以新建一个src文件夹存放linux模块代码。同时将src文件夹Mark Directory as Project sources and Headers。

接下来我们在CMake配置文件中引入我们开发所需要的内核头文件。

# 设置Cmake版本需要和系统中的Cmake版本保持一致
cmake_minimum_required(VERSION 3.10)
# 工程名称
project(os C)
# 设置编译选项
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99 -nostdinc") # C 编译器设置
add_definitions(-D__KERNEL__=1) #手动添加预处理宏

# 设置内核路径
set(KERNEL_ROOT /usr/src/linux-headers-5.4.0-48-generic)

# 添加需要的内核文件
include_directories(
        # kernel headers
        "${KERNEL_ROOT}/include"
        #"${KERNEL_ROOT}/arch/arm/include"
        #"${KERNEL_ROOT}/arch/arm64/include"
        # kernel source
        #"${KERNEL_ROOT}/mm"
)

set(SOURCE_FILES main.c)
add_executable(os ${SOURCE_FILES})

如何不通过add_definitions(-D__KERNEL__=1)手动添加KERNEL宏,有很多宏,CLion将无法识别。

kernel为了保护其代码,故意设置了这么一个

宏,只有在编译kernel的时候才会在命令行传递这个宏进去,像libc等C runtime编译时要用到kernel

的部分代码,但是它不会设置KERNEL这个宏,于是被这个宏保护的kernel代码它是看不到的。

自然,我们的Clion没有定义这个宏,那些kernel代码(大部分代码)它也是看不到的,于是parser在

解析文件时,很多符号是解析不出来的。

配置完成后就可以使用CLion进行开发了。

6.2 哈希表的使用

下面编写一个linux内核模块,用以创建、增加、删除和遍历一个哈希表。

// hashlist.c
// Created by linux on 2020/9/25.
//
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/list.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("linux");
#define N 10
//数字链表
struct numlist{
    struct hlist_head hlistHead;
};
//数字链表节点
struct numnode{
    int num;
    struct hlist_node hlistNode;
};
struct  numlist nhead;
struct  numnode nnode;

static int __init hlist_init(void){
    //init head node
    struct hlist_node *pos;
    struct numnode *listnode;
    int i;

    printk("hashlist is starting...\n");
    //初始化头节点
    INIT_HLIST_HEAD(&nhead.hlistHead);

    for ( i = 0; i < N; ++i) {
        listnode = (struct numnode *)kmalloc(sizeof(struct numnode),GFP_KERNEL);
        listnode->num = i+1;
        //添加节点在头节点之前
        hlist_add_head(&(listnode->hlistNode),&nhead.hlistHead);

        printk("Node %d has added to the hash list...\n",i+1);

    }
    //遍历链表
    i = 1;
    struct numnode *p;

    hlist_for_each(pos,&nhead.hlistHead){
        //取得数字节点的数据域
        p =   hlist_entry(pos,struct numnode,hlistNode);
        printk("Node %d data:%d\n",i,p->num);
        i++;
    }
    return 0;
}

static void __exit hashlist_exit(void){
    struct hlist_node *pos,*n;
    struct numnode *p;
    int i;

    i =1;
    //遍历数字链表
    hlist_for_each_safe(pos,n,&nhead.hlistHead){
        //删除哈希节点
        hlist_del(pos);
        //取得删除节点的数据域值
        p =   hlist_entry(pos,struct numnode,hlistNode);
        kfree(p);
        printk("Node %d has removed from the hashlist ...\n",i++);

    }
    printk("hash list is exiting...\n");
}

module_init(hlist_init);
module_exit(hashlist_exit);

对应的Makefile文件如下:

obj-m := hashlist.o
CURRENT_PATH := $(shell pwd)
LINUX_KERNEL := $(shell uname -r)
LINUX_KERNEL_PATH :=/usr/src/linux-headers-$(LINUX_KERNEL)

all:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

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

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

相关文章

java springboot+mybatis电影售票网站管理系统前台+后台设计和实现

java springbootmybatis电影售票网站管理系统前台后台设计和实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言…

RS—|遥感数字图像处理编程练习(python)

目录一&#xff1a;模拟计算图像直方图和累计直方图二&#xff1a;计算图像的均值、标准差、相关系数和协方差三&#xff1a;利用模板进行卷积运算四&#xff1a;获取彩色图像的直方图五&#xff1a;图像直方图均衡化一&#xff1a;模拟计算图像直方图和累计直方图 ① 调用的p…

【雷达入门 | FMCW毫米波雷达系统的性能参数分析】

本文编辑&#xff1a;调皮哥的小助理 FMCW毫米波雷达系统的性能参数主要包含&#xff1a; (1)距离估计、距离分辨率、距离精度、最大探测距离; (2)速度估计、速度分辨率、速度精度、最大不模糊速度&#xff1b; (3)角度估计、角度分辨率、角度精度、最大角度范围。 分析以及…

微服务框架SpringCloud从入门到通神(持续更新)

SpringCloud——>SpringBoot——>JavaWeb 微服务技术栈导学1 哔站up黑马程序员主讲老师&#xff0c;一上来就给介绍了SpringCloud出现的背景&#xff1a;微服务是分布式架构的一种&#xff0c;分布式架构就是要把服务做拆分&#xff0c;而SpringCloud只是解决了服务拆分式…

FTP协议原理简析

FTP服务器默认使用TCP协议的20、21端口与客户端进行通信。21端口用于建立控制连接&#xff0c;并传输FTP指令。20端口用于建立数据连接&#xff0c;传输数据流。 一&#xff1a;FTP功能简介 1&#xff1a;FTP服务器能够进行档案的传输与管理功能&#xff1b; 2&#xff1a;可以…

招生简章 | 欢迎报考中科院空天院网络信息体系技术重点实验室(七室)

官方公众号链接&#xff1a;招生简章 | 欢迎报考中科院空天院网络信息体系技术重点实验室&#xff08;七室&#xff09; 招生简章 | 欢迎报考中科院空天院网络信息体系技术重点实验室&#xff08;七室&#xff09; 中国科学院空天信息创新研究院&#xff08;简称空天院&#x…

【实战篇】38 # 如何使用数据驱动框架 D3.js 绘制常用数据图表?

说明 【跟月影学可视化】学习笔记。 图表库 vs 数据驱动框架 图表库只要调用 API 就能展现内容&#xff0c;灵活性不高&#xff0c;对数据格式要求也很严格&#xff0c;但方便数据驱动框架需要手动去完成内容的呈现&#xff0c;灵活&#xff0c;不受图表类型对应 API 的制约…

Smart Finance成为火必投票竞选项目,参与投票获海量奖励

最近&#xff0c;Huobi推出了新一期的“投票上币”活动&#xff0c;即用户可以通过HT为候选项目投票&#xff0c;在投票截止后&#xff0c;符合条件的优质项目将直接上线Huobi。而Smart Finance成为了新一期投票上币活动的竞选项目之一&#xff0c;并备受行业关注&#xff0c;与…

C++ 命令模式

什么是命令模式&#xff1f; 将请求转换为一个包含与请求相关的所有信息的独立对象。从而使你可以用不同的请求方法进行参数化&#xff0c;并且能够对请求进行排队、记录请求日志以及撤销请求操作。命令模式属于行为设计模式 如何理解命令模式 命令模式很像我们订外卖&#…

Hudi(10):Hudi集成Spark之并发控制

目录 0. 相关文章链接 1. Hudi支持的并发控制 1.1. MVCC 1.2. OPTIMISTIC CONCURRENCY 2. 使用并发写方式 3. 使用Spark DataFrame并发写入 4. 使用Delta Streamer并发写入 0. 相关文章链接 Hudi文章汇总 1. Hudi支持的并发控制 1.1. MVCC Hudi的表操作&#xff0c;如…

阿里云 EDAS Java服务日志中打印调用链TraceId

最近要搭建阿里云的日志服务SLS&#xff0c;收集服务日志&#xff0c;进行统一的搜索查询。但遇到一个问题如何在日志中打印链路的TraceId&#xff0c;本文章记录一下对EDAS免费的解决方法。 先看一下阿里官方文档 业务日志关联调用链的TraceId信息 从文档上看&#xff0c;想要…

基于SSM的资源发布系统

项目介绍&#xff1a; 该系统基于SSM技术&#xff0c;数据层为MyBatis&#xff0c;数据库使用mysql&#xff0c;MVC模式&#xff0c;B/S架构&#xff0c;具有完整的业务逻辑。系统共分为管理员&#xff0c;用户两种角色&#xff0c;主要功能&#xff1a;登陆注册&#xff0c;用…

数据结构:跳表

文章目录跳表跳表的由来单链表的查找效率太低提高单链表的查找效率跳表的时间复杂度分析跳表的空间复杂度分析跳表的插入操作跳表的删除操作跳表索引动态更新跳表 对链表进行改造&#xff0c;在链表上加多级索引的结构就是跳表&#xff0c;使其可以支持类似“二分”的查找算法。…

Redis查询之RediSearch和RedisJSON讲解

文章目录1 Redis查询1.1 RedisMod介绍1.2 安装Redis1.3 RediSearchRedisJSON安装1.3.1 下载安装1.3.2 修改配置1.4 RedisJSON操作1.4.1 基本操作1.4.1.1 保存操作JSON.SET1.4.1.2 读取操作JSON.GET1.4.1.3 批量读取操作JSON.MGET1.4.1.4 删除操作JSON.DEL1.4.1.5 其他命令1.4.1…

鲲鹏Bigdata pro之Hive的基本操作(创建表、查询表)

1 介绍 本文主要依据《鲲鹏Bigdata pro之Hive集群部署》实验教程上的Hive操作例子讲解&#xff0c;方便大数据学员重用相应的操作语句。同时对实验过程中出现的问题给以解决方法&#xff0c;重现问题解决的过程。以让大家认识到&#xff0c;出现问题很正常&#xff1b;同时&am…

Java设计模式中接口隔离原则是什么?迪米特原则又是什么,啥又是合成复用原则,这些又怎么运用

继续整理记录这段时间来的收获&#xff0c;详细代码可在我的Gitee仓库SpringBoot克隆下载学习使用&#xff01; 3.5 接口隔离原则 3.5.1 特点 使用的类不应该被迫依赖于不想使用的方法&#xff0c;应该依赖接口方法 3.5.2 案例(安全门) 防火功能代码 public interface Fi…

第一章:统计学习方法概论

大纲1.1统计学习的特点1.2统计学习方法步骤1.3 统计学习的分类基本分类&#xff1a;1.4 监督学习方法的三要素模型&#xff1a;条件概率分布P(Y∣X)P(Y|X)P(Y∣X)或决策分布Yf(X)Yf(X)Yf(X)策略&#xff1a;在所有假设空间中选择一个最优模型注意事项&#xff1a;算法&#xff…

Java设计模式中适配器模式是什么/适配器模式可以干什么/又如何实现

继续整理记录这段时间来的收获&#xff0c;详细代码可在我的Gitee仓库SpringBoot克隆下载学习使用&#xff01; 5.3 适配器模式 5.3.1 概述 将一个类的接口转换为客户希望的另一种接口&#xff0c;使得原本由于接口不兼容而不能一起工作的那些类能一起工作分为类适配器模式和…

一套采用ASP.NET开发的工作通OA协同办公系统源码 流程审批 公文流转 文档管理

分享一套采用ASP.NET基于C#开发&#xff0c;使用桌面式的OA协同办公系统&#xff0c;超好用户体验效果的后台管理界面&#xff0c;集成 资讯、邮件、日程、文档&#xff08;在线文件档案管理&#xff09;、流程审批、公文流转、沟通与分享&#xff08;在线聊天和内部论坛&#…

基于LLVM的C编译器--lcc——以CLion用SSH连接WSL Ubuntu22.04为例

Windows 10 22H2CLion 2022.3.1Ubuntu 20.04 &#xff08;Microsoft Store内的WSL发行版&#xff09; 一、下载WSL&#xff0c;换源&#xff0c;切换到WSL2 1.1 保证windows版本 在设置->系统->关于中查看 必须是win10及以上对于x64系统&#xff1a;版本1903或更高版…