如何在Linux内核中使用Map实现键值存储?求示例与保护机制
内核态基于RCU保护的哈希表键值存储实现示例
嘿,刚好做过类似的内核模块开发,给你整理一个实用的示例,同时解答你关于RCU保护机制的疑问~
一、基础结构定义
首先我们需要定义键值对的结构体,以及哈希表本身。内核中常用hlist配合RCU来实现高效的哈希表,同时用自旋锁保护写操作:
#include <linux/init.h> #include <linux/module.h> #include <linux/hash.h> #include <linux/rculist.h> #include <linux/spinlock.h> #include <linux/slab.h> #include <linux/uaccess.h> #include <linux/ioctl.h> // 定义键值对结构体,用hlist_bl_node做哈希节点(支持RCU) struct kv_entry { char key[64]; // 用户态传入的键 int value; // 存储的值 struct hlist_bl_node node; // 哈希节点,用于RCU保护 }; // 定义哈希表和自旋锁(保护写操作) #define KV_HASH_BITS 10 // 哈希表大小为2^10=1024 static DEFINE_HASHTABLE(kv_hash_table, KV_HASH_BITS); static DEFINE_SPINLOCK(kv_hash_lock);
二、核心操作实现
1. 添加/更新键值对(带RCU保护)
当需要添加或更新值时,我们需要先加自旋锁,然后查找是否存在对应键,存在则更新,不存在则分配新节点并通过hash_add_rcu加入哈希表:
static int kv_set(const char __user *user_key, int value) { char key[64]; struct kv_entry *entry; unsigned int hash; // 从用户态拷贝键,检查合法性 if (copy_from_user(key, user_key, sizeof(key))) return -EFAULT; key[sizeof(key)-1] = '\0'; // 确保字符串终止 hash = hash_str(key, KV_HASH_BITS); // 加自旋锁保护写操作 spin_lock(&kv_hash_lock); // 查找是否已存在该键 hash_for_each_possible(kv_hash_table, entry, node, hash) { if (!strcmp(entry->key, key)) { entry->value = value; // 更新值 spin_unlock(&kv_hash_lock); return 0; } } // 不存在则分配新节点 entry = kmalloc(sizeof(*entry), GFP_ATOMIC); if (!entry) { spin_unlock(&kv_hash_lock); return -ENOMEM; } strscpy(entry->key, key, sizeof(entry->key)); entry->value = value; // 用hash_add_rcu将节点加入哈希表,支持RCU读 hash_add_rcu(kv_hash_table, &entry->node, hash); spin_unlock(&kv_hash_lock); return 0; }
2. 读取键值对(带RCU保护)
读操作不需要加锁,只需要在RCU读临界区内操作,用hash_for_each_possible_rcu遍历查找:
static int kv_get(const char __user *user_key, int __user *user_value) { char key[64]; struct kv_entry *entry; unsigned int hash; int ret = -ENOENT; if (copy_from_user(key, user_key, sizeof(key))) return -EFAULT; key[sizeof(key)-1] = '\0'; hash = hash_str(key, KV_HASH_BITS); // 进入RCU读临界区,读操作无锁 rcu_read_lock(); hash_for_each_possible_rcu(kv_hash_table, entry, node, hash) { if (!strcmp(entry->key, key)) { if (copy_to_user(user_value, &entry->value, sizeof(int))) { ret = -EFAULT; } else { ret = 0; } break; } } rcu_read_unlock(); return ret; }
3. 删除键值对(带RCU保护)
删除操作需要先加自旋锁,用hash_del_rcu移除节点,然后通过call_rcu延迟释放内存(避免读操作访问已释放的内存):
static void kv_entry_free(struct rcu_head *head) { struct kv_entry *entry = container_of(head, struct kv_entry, node.rcu); kfree(entry); } static int kv_del(const char __user *user_key) { char key[64]; struct kv_entry *entry; unsigned int hash; int ret = -ENOENT; if (copy_from_user(key, user_key, sizeof(key))) return -EFAULT; key[sizeof(key)-1] = '\0'; hash = hash_str(key, KV_HASH_BITS); spin_lock(&kv_hash_lock); hash_for_each_possible(kv_hash_table, entry, node, hash) { if (!strcmp(entry->key, key)) { // 用hash_del_rcu移除节点,RCU读仍可访问直到RCU grace period结束 hash_del_rcu(&entry->node); // 延迟释放节点,保证读操作完成后再释放 call_rcu(&entry->node.rcu, kv_entry_free); ret = 0; break; } } spin_unlock(&kv_hash_lock); return ret; }
三、RCU相关保护机制说明
你提到的hash_add_rcu确实是内核提供的RCU保护哈希操作,配套的还有这些关键函数:
hash_add_rcu:将节点添加到哈希表,允许RCU读操作并发访问hash_del_rcu:从哈希表移除节点,节点内存需要通过call_rcu延迟释放hash_for_each_possible_rcu:在RCU读临界区内遍历哈希表的可能节点,无锁高效rcu_read_lock()/rcu_read_unlock():包裹读操作,进入/退出RCU读临界区
这些机制的核心是读操作无锁,写操作加自旋锁(或其他互斥锁),内存延迟释放,非常适合你这种用户态发起读写、内核态处理的场景(通常读操作远多于写操作)。
四、用户态交互示例
可以通过ioctl来实现用户态和内核态的交互,比如定义三个命令:SET、GET、DEL,这里给出简化的ioctl处理逻辑:
#define KV_SET _IOW('k', 1, struct kv_ioctl_arg) #define KV_GET _IOR('k', 2, struct kv_ioctl_arg) #define KV_DEL _IOW('k', 3, struct kv_ioctl_arg) struct kv_ioctl_arg { char key[64]; int value; }; static long kv_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct kv_ioctl_arg arg_buf; if (copy_from_user(&arg_buf, (void __user *)arg, sizeof(arg_buf))) return -EFAULT; switch (cmd) { case KV_SET: return kv_set(arg_buf.key, arg_buf.value); case KV_GET: { int ret = kv_get(arg_buf.key, &arg_buf.value); if (!ret && copy_to_user((void __user *)arg, &arg_buf, sizeof(arg_buf))) return -EFAULT; return ret; } case KV_DEL: return kv_del(arg_buf.key); default: return -ENOTTY; } } // 这里还需要实现file_operations结构体、模块初始化/退出函数,篇幅原因就不展开了
最后要注意模块开发的规范,比如内存泄漏检查、锁的正确使用、用户空间指针的安全处理等等。
内容的提问来源于stack exchange,提问作者SudipM
相关产品推荐
相关产品推荐

