理解并使用Linux内核Hash表

1. 引言

1.1 为什么学习Linux内核Hash表?

在Linux内核开发中,Hash表(哈希表)是一种非常高效的数据结构,它广泛应用于内核中各类数据的存储和查找。学习并理解Linux内核中的Hash表,不仅可以帮助开发者在内核模块中高效地处理数据,还能够提升对内核设计和性能优化的理解。在日常的内核编程中,哈希表常常用于实现资源管理、内存管理、文件系统、网络协议等多个方面。

1.2 Hash表在日常编程中的应用

在日常编程中,哈希表被用于存储键值对,并支持常数时间复杂度的查找、插入和删除操作。无论是开发数据库、缓存系统,还是在处理大数据集合时,哈希表都是必不可少的工具。尤其是在需要快速查找或处理大量数据时,哈希表能极大提高性能。


2. Hash表基础回顾

2.1 什么是Hash表?

哈希表是一种基于哈希函数将数据映射到固定大小的存储位置(桶)的数据结构。每个桶存储一个或多个数据项,哈希函数通过键值计算出数据项的存储位置。哈希表提供了高效的插入、查找和删除操作,时间复杂度接近O(1),这使得它成为许多应用程序中理想的数据结构。

2.2 哈希函数的工作原理

哈希函数是哈希表的核心,它将键(key)转换为数组下标。理想的哈希函数应该具备以下特点:

  • 均匀分布:不同的键值应均匀分布在哈希表的各个桶中,以避免冲突。
  • 高效性:哈希函数计算简单,能够快速返回数组下标。

常见的哈希函数包括除法法、乘法法、位运算法等。

2.3 解决哈希冲突的方法

哈希冲突是指多个键值映射到同一个桶中。常见的冲突解决方法有:

  • 链地址法:每个桶存储一个链表,所有映射到同一桶的键值对都存储在这个链表中。
  • 开放地址法:如果桶已经被占用,则寻找下一个空桶来存储数据。
  • 双重哈希法:使用另一个哈希函数来解决冲突。

Linux内核中的哈希表通常使用链地址法来解决冲突。


3. Linux内核中的Hash表

3.1 内核Hash表的独特需求

与用户空间应用中的哈希表不同,Linux内核的哈希表需要满足以下特殊需求:

  • 高效性:内核代码往往需要处理大量数据,因此哈希表的性能至关重要。
  • 内存管理:内核中的数据结构管理要求内存分配和释放更加严格和高效。
  • 线程安全:内核操作往往需要支持并发执行,因此需要考虑并发访问时的数据一致性。

3.2 数据结构概览:链地址法和其他方法

Linux内核中的哈希表主要使用链地址法,它通过hlist_head和hlist_node结构实现。每个桶头是一个hlist_head结构,它包含一个指向链表的指针,所有映射到同一个桶的元素都通过hlist_node进行链接。

3.3 内核中常用的哈希算法介绍

Linux内核提供了一些常用的哈希算法,例如:

  • Jenkins hash:一种非加密的哈希算法,具有较高的性能和较低的冲突概率。内核中常用的 hash_long 函数就是基于 Jenkins 算法实现的。
  • FNV-1a hash:一种简单的哈希算法,适用于大多数场景。
  • MurmurHash:在内核中也有应用,通常用于非加密的场景。

这些哈希算法确保哈希表的数据分布尽可能均匀,从而减少哈希冲突,提高性能。

3.5 并发控制与同步机制

在内核中,哈希表经常用于多线程环境,因此需要确保并发访问时的同步。

  • 自旋锁:在修改哈希表时,加锁确保只有一个线程能够访问表。
  • 读写锁:对于读操作较多的情况,可以使用读锁,允许多个线程并发读取数据。

4. Hash表示例代码: 增删查遍历

通过这个示例,你可以理解如何在Linux内核中使用哈希表及其相关操作接口。

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/list.h>

// 定义哈希表的大小
#define HASH_TABLE_SIZE 16

// 哈希表节点结构
struct hash_node {
    int key;
    int value;
    struct hlist_node hnode;  // 链表节点
};

// 哈希表头
static struct hlist_head hash_table[HASH_TABLE_SIZE];

// 哈希函数:计算key的哈希值
static unsigned int hash_function(int key)
{
    return key % HASH_TABLE_SIZE;
}

// 插入元素到哈希表
static void insert_into_hash_table(int key, int value)
{
    unsigned int hash_index = hash_function(key);
    struct hash_node *new_node;

    new_node = kmalloc(sizeof(struct hash_node), GFP_KERNEL);
    if (!new_node) {
        pr_err("Memory allocation failed\n");
        return;
    }

    new_node->key = key;
    new_node->value = value;

    // 将节点添加到对应哈希表桶的头部
    hlist_add_head(&new_node->hnode, &hash_table[hash_index]);
    pr_info("Inserted key: %d, value: %d into hash table\n", key, value);
}

// 从哈希表中删除元素
static void delete_from_hash_table(int key)
{
    unsigned int hash_index = hash_function(key);
    struct hash_node *node;
    struct hlist_node *tmp;

    // 遍历链表寻找对应的节点
    hlist_for_each_entry_safe(node, tmp, &hash_table[hash_index], hnode) {
        if (node->key == key) {
            hlist_del(&node->hnode);  // 删除节点
            kfree(node);  // 释放内存
            pr_info("Deleted key: %d from hash table\n", key);
            return;
        }
    }

    pr_info("Key: %d not found in hash table\n", key);
}

// 查找哈希表中的元素
static struct hash_node *find_in_hash_table(int key)
{
    unsigned int hash_index = hash_function(key);
    struct hash_node *node;

    // 遍历链表寻找对应的节点
    hlist_for_each_entry(node, &hash_table[hash_index], hnode) {
        if (node->key == key) {
            return node;
        }
    }

    return NULL;  // 未找到
}

// 初始化哈希表
static int __init hash_table_init(void)
{
    int i;

    // 初始化哈希表
    for (i = 0; i < HASH_TABLE_SIZE; i++) {
        INIT_HLIST_HEAD(&hash_table[i]);
    }

    // 测试插入、查找和删除操作
    insert_into_hash_table(10, 100);
    insert_into_hash_table(20, 200);
    insert_into_hash_table(30, 300);

    struct hash_node *node = find_in_hash_table(20);
    if (node) {
        pr_info("Found key: %d, value: %d\n", node->key, node->value);
    } else {
        pr_info("Key: 20 not found\n");
    }

    delete_from_hash_table(10);
    node = find_in_hash_table(10);
    if (node) {
        pr_info("Found key: %d, value: %d\n", node->key, node->value);
    } else {
        pr_info("Key: 10 not found\n");
    }

    return 0;
}

// 清理哈希表
static void __exit hash_table_exit(void)
{
    int i;
    struct hash_node *node;
    struct hlist_node *tmp;

    // 清理哈希表
    for (i = 0; i < HASH_TABLE_SIZE; i++) {
        hlist_for_each_entry_safe(node, tmp, &hash_table[i], hnode) {
            hlist_del(&node->hnode);
            kfree(node);
        }
    }

    pr_info("Hash table module exited\n");
}

module_init(hash_table_init);
module_exit(hash_table_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Linux kernel module using hlist as a hash table");

解释

  1. 哈希函数:使用简单的取余方式 key % HASH_TABLE_SIZE 来计算哈希值。
  2. 插入操作:insert_into_hash_table 函数创建一个新的哈希节点,并将其插入到计算得到的哈希表桶的头部。
  3. 删除操作:delete_from_hash_table 函数遍历链表并删除匹配的节点。如果找到该节点,调用 hlist_del 将其从链表中删除,并使用 kfree 释放内存。
  4. 查找操作:find_in_hash_table 函数遍历链表并查找匹配的节点。如果找到,返回该节点;否则返回 NULL。
  5. 初始化和清理:模块加载时,初始化哈希表,并进行一些插入、查找和删除的测试。模块卸载时,释放所有分配的内存。

测试流程

  1. 插入了键值对 (10, 100)、(20, 200)、(30, 300) 到哈希表。
  2. 查找键为 20 的节点,并打印值。
  3. 删除键为 10 的节点,再次查找键为 10 的节点,验证删除是否成功。

注意事项

  • hlist_add_head 用于将新节点添加到链表的头部。
  • hlist_del 删除节点时不会将节点初始化,因此,如果你需要将节点移除并初始化,应该使用 hlist_del_init。
  • 通过 hlist_for_each_entry_safe 实现遍历操作,确保在遍历过程中删除节点时不会造成崩溃。

5. 深入理解

在这一节中,我们将分析Linux内核中哈希表的源代码,特别是插入、删除和遍历操作的实现。Linux内核中的哈希表通常使用链表处理哈希冲突,这样可以高效地管理键值对。

5.1 哈希表插入操作的代码分析

// 将节点 n 添加到链表 h 的头部
static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h)
{
    // 获取链表 h 的第一个节点
    struct hlist_node *first = h->first;
    
    // 将节点 n 的 next 指针指向当前链表的第一个节点
    n->next = first;
    
    // 如果链表非空,则更新第一个节点的 pprev 指针,指向新插入的节点
    if (first)
        first->pprev = &n->next;
    
    // 使用 WRITE_ONCE 来确保对 h->first 的更新在多线程环境中的正确性
    WRITE_ONCE(h->first, n);
    
    // 设置节点 n 的 pprev 指针为指向链表头部(h->first)
    n->pprev = &h->first;
}

解释:

  • hlist_add_head:该函数将节点 n 插入到链表 h 的头部。
    • struct hlist_node *first = h->first;:首先获取链表 h 的第一个节点。如果链表为空,h->first 是 NULL。
    • n->next = first;:将新节点 n 的 next 指针指向当前的第一个节点。
    • if (first) first->pprev = &n->next;:如果链表非空,则将当前第一个节点的 pprev 指针指向新节点 n。这样,新节点 n 成为新的头节点。
    • WRITE_ONCE(h->first, n);:将链表的头指针 h->first 更新为新节点 n。WRITE_ONCE 用于防止在并发环境下发生不一致的写入操作。
    • n->pprev = &h->first;:将新节点 n 的 pprev 指针指向链表头指针的地址,以保持双向链表的正确性。

5.2 哈希表删除操作的代码分析

删除操作用于从哈希表中移除一个指定节点。内核提供了两种删除操作:一种是删除节点并不重新初始化,另一种则是删除后重新初始化节点。

// 内部删除节点函数,实际执行删除操作
static inline void __hlist_del(struct hlist_node *n)
{
    // 获取节点的下一个节点
    struct hlist_node *next = n->next;
    // 获取节点的前驱节点的指针(指向当前节点的前一个节点)
    struct hlist_node **pprev = n->pprev;

    // 将当前节点从链表中删除:将前驱节点的next指针指向当前节点的next指针
    WRITE_ONCE(*pprev, next);
    // 如果当前节点后面还有节点,将后继节点的pprev指针指向前驱节点
    if (next)
        next->pprev = pprev;
}

// 删除节点并使其指向特定的“毒值”以防止后续误用
static inline void hlist_del(struct hlist_node *n)
{
    // 调用 __hlist_del 函数实际删除节点
    __hlist_del(n);
    // 设置节点的 next 和 pprev 指针为“毒值”,以便调试时发现已删除节点的访问
    n->next = LIST_POISON1;
    n->pprev = LIST_POISON2;
}

// 删除节点并重新初始化它,以便重新使用
static inline void hlist_del_init(struct hlist_node *n)
{
    // 如果节点未被删除(即未被移除链表)
    if (!hlist_unhashed(n)) {
        // 调用 __hlist_del 删除节点
        __hlist_del(n);
        // 重新初始化节点,以防它再次被误用
        INIT_HLIST_NODE(n);
    }
}

  • hlist_del:首先调用 __hlist_del 函数从链表中删除节点,然后将节点的 next 和 pprev 指针设置为特殊值 LIST_POISON1 和 LIST_POISON2,以便在调试时检测。
  • hlist_del_init:此函数在删除节点后会重新初始化节点,避免节点重复使用时引发问题。

5.3 哈希表遍历操作的代码分析

哈希表的遍历操作是操作哈希表时常见的任务,特别是在需要遍历哈希桶中的所有元素时。在 Linux 内核中,哈希表的遍历有几种常见的方式,包括基本的遍历、删除安全遍历以及通过结构体成员进行的遍历。

以下是 Linux 内核中用于哈希表遍历的宏定义:

// 遍历哈希表中的所有节点,从链表头开始
#define hlist_for_each(pos, head) \
    for (pos = (head)->first; pos; pos = pos->next)

// 遍历哈希表中的所有节点,保证在遍历过程中删除节点的安全
#define hlist_for_each_safe(pos, n, head) \
    for (pos = (head)->first; pos && ({ n = pos->next; 1; }); \
         pos = n)

// 遍历哈希表中的每个元素,适用于结构体嵌套链表节点的遍历
#define hlist_for_each_entry(pos, head, member) \
    for (pos = hlist_entry_safe((head)->first, typeof(*(pos)), member); \
         pos; \
         pos = hlist_entry_safe((pos)->member.next, typeof(*(pos)), member))
1. hlist_for_each

这是最基础的遍历方式,用于遍历哈希表中链表的所有节点。它从链表头部开始,依次访问每个节点,直到链表末尾。

  • pos = (head)->first:首先将 pos 设置为链表头部 head->first,即哈希桶中的第一个节点。
  • pos = pos->next:然后在每次迭代中,将 pos 移动到下一个节点,直到 pos 为 NULL(即链表末尾)。
#define hlist_for_each(pos, head) \
    for (pos = (head)->first; pos ; pos = pos->next)
2. hlist_for_each_safe

这个宏比 hlist_for_each 更为安全,特别适用于在遍历过程中可能会删除链表节点的情况。它通过使用临时变量 n 来保存当前节点的下一个节点,从而避免了在删除当前节点时链表结构的破坏。

  • pos = (head)->first:将 pos 设置为链表头部 head->first,开始遍历。
  • n = pos->next:在每次循环开始时,将 n 设置为当前节点的下一个节点,确保即使当前节点被删除,遍历过程依然能继续。
  • pos = n:每次循环结束时,更新 pos 为 n,继续遍历下一个节点。
#define hlist_for_each_safe(pos, n, head) \
    for (pos = (head)->first; pos && ({ n = pos->next; 1; }); \
         pos = n)
3. hlist_for_each_entry

这个宏用于遍历哈希表中每个元素,尤其是当链表节点是结构体中的一部分时。这种遍历方式需要特别注意,遍历过程中会从链表头部 head->first 开始,查找每个节点中的嵌套链表节点。它确保了可以直接访问结构体中的数据。

  • hlist_entry_safe:该宏是对节点指针的封装,它将 hlist_node 指针转换为指向结构体的指针,确保遍历过程不会由于空指针而出错。
  • pos = hlist_entry_safe((head)->first, typeof(*(pos)), member):将 pos 设置为链表中的第一个元素,hlist_entry_safe 将链表节点指针转换为结构体指针。
  • pos = hlist_entry_safe((pos)->member.next, typeof(*(pos)), member):然后遍历结构体中嵌套的链表节点,直到遍历完整个链表。
#define hlist_for_each_entry(pos, head, member) \
    for (pos = hlist_entry_safe((head)->first, typeof(*(pos)), member); \
         pos; \
         pos = hlist_entry_safe((pos)->member.next, typeof(*(pos)), member))

hlist_entry 和 hlist_entry_safe 解释

在 Linux 内核中,hlist_entry 和 hlist_entry_safe 是常用的宏,用于将哈希链表节点(hlist_node)指针转换为包含该节点的结构体指针。哈希链表节点通常是结构体中的一部分,使用这些宏可以便捷地访问和操作链表中的结构体。类似链表的list_entry

hlist_entry 宏用于将 hlist_node 指针(通常是链表节点)转换为包含它的结构体指针。它的定义如下:

#define hlist_entry(ptr, type, member) container_of(ptr, type, member)
  • ptr:传入的指向链表节点(hlist_node)的指针。
  • type:目标结构体的类型,通常是链表中包含 hlist_node 的结构体类型。
  • member:指向 hlist_node 的结构体成员名。

container_of 是一个常见的宏,用于从结构体的成员获取包含该成员的结构体指针。hlist_entry 通过 container_of 实现了这个功能。

示例:
假设有以下结构体定义,其中 hlist_node 是结构体 my_struct 的一部分:

struct my_struct {
    int value;
    struct hlist_node node;  // 链表节点
};

如果我们有一个指向 hlist_node 类型的指针 pos,我们可以使用 hlist_entry 将其转换为指向 my_struct 类型的指针:

struct my_struct *entry = hlist_entry(pos, struct my_struct, node);

这段代码的作用是:通过链表节点 pos,获取到包含该节点的结构体 my_struct 的指针 entry。

hlist_entry_safe 宏是在 hlist_entry 的基础上加了一些安全检查,以防传入 NULL 指针。在某些情况下,传入的 ptr 可能是 NULL,此时使用 hlist_entry 会导致未定义的行为。hlist_entry_safe 通过增加 NULL 检查来避免这种情况。

小结:

  • hlist_for_each 是最简单的遍历方式,适用于不需要删除节点的情况。
  • hlist_for_each_safe 是安全遍历方式,避免在遍历过程中修改链表时出错,常用于需要删除节点的场景。
  • hlist_for_each_entry 则适用于结构体中嵌套链表节点的遍历,通常在哈希表中每个元素本身是结构体时使用。

6. 结语

6.1 总结学到的知识点

通过本文的学习,你应该掌握了Linux内核中的哈希表的使用方法,包括哈希表的定义、操作、性能优化等方面。哈希表作为内核中的一种高效数据结构,在多个子系统中发挥着重要作用。

6.2 进一步学习资源推荐

  • Linux内核源代码
  • 《Linux Device Drivers》by Jonathan Corbet
  • 《Linux内核设计与实现》by Robert Love
  • 在线资源:Kernel Newbies, LWN.net

通过进一步的实践和学习,你可以更深入地理解Linux内核的工作原理,并提升自己的内核编程技能。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐