Linux内核数据结构3(基于Linux6.6)---映射介绍
一、概述
一个映射,也称为关联数组。是一个由唯一键组成的集合,而每个键必然关联一个特定的值。这种键到值的关联关系称为映射。
1.1、散列表(Hash Table)
散列表是一种基于哈希函数的高效数据结构,它可以在常数时间内完成插入、删除和查找操作。散列表的主要思想是通过哈希函数将键值(Key)映射到一个数组中的位置,从而加速查找和存取。
散列表在 Linux 内核中的应用
在 Linux 内核中,散列表被广泛用于管理和快速查找各种类型的对象。以下是一些典型的例子:
- 进程表:Linux 使用散列表来管理进程信息,通过进程的标识符(PID)作为键,可以快速查找进程。
- 虚拟内存管理:内核中的虚拟内存管理也会使用散列表来映射虚拟页面和物理页面之间的关系。
- 文件系统:Linux 文件系统(如 ext4)使用散列表来管理文件的 inode 映射等信息。
- 网络协议栈:在 TCP/IP 协议栈中,散列表用于管理网络连接、套接字、路由表等。
1.2、自平衡二叉搜索树(Self-balancing Binary Search Tree)
自平衡二叉搜索树(如 AVL 树、红黑树等)是一种特殊的二叉树,它通过自动调整树的结构来保证树的高度始终保持平衡,从而保证插入、删除和查找操作的时间复杂度为 O(log n)。
自平衡二叉搜索树在 Linux 内核中的应用
自平衡二叉搜索树在 Linux 内核中的应用非常广泛,尤其是在需要快速查找和更新的场景。常见的应用包括:
- 调度器:Linux 调度器(如 CFS 调度器)使用红黑树来维护进程的运行队列,确保根据优先级选择合适的进程执行。
- 文件系统:文件系统如 ext4 使用红黑树来管理目录项和文件的索引。
- 内存管理:Linux 内核的内存管理系统(如虚拟内存管理)中也使用自平衡二叉搜索树来管理地址空间等信息。
二、Linux内核映射的实现
Linux内核提供了简单、有效的映射数据结构。但是它并非一个通用的映射。因为它的目标是 :映射一个唯一的标识数(UID)到一个指针。
除了提供三个标准的映射操作外,Linux还在add操作基础上实现了allocate操作。这个allocate操作不但向map中加入了键值对,而且还可产生UID。
struct idr数据结构
include/linux/idr.h
struct idr {
struct radix_tree_root idr_rt;
unsigned int idr_base;
unsigned int idr_next;
};
三、初始化一个idr
建立一个idr简单,首先你需要静态定义或者动态分配一个idr数据结构。然后调用idr_init()。
idr_init()
include/linux/idr.h
/**
* idr_init_base() - Initialise an IDR.
* @idr: IDR handle.
* @base: The base value for the IDR.
*
* This variation of idr_init() creates an IDR which will allocate IDs
* starting at %base.
*/
static inline void idr_init_base(struct idr *idr, int base)
{
INIT_RADIX_TREE(&idr->idr_rt, IDR_RT_MARKER);
idr->idr_base = base;
idr->idr_next = 0;
}
/**
* idr_init() - Initialise an IDR.
* @idr: IDR handle.
*
* Initialise a dynamically allocated IDR. To initialise a
* statically allocated IDR, use DEFINE_IDR().
*/
static inline void idr_init(struct idr *idr)
{
idr_init_base(idr, 0);
}
演示案例:
struct idr id_huh; //静态定义idr结构
idr_init(&id_huh); //初始化idr结构
四、分配一个新的UID
通过上面建立了一个idr之后,接下来就可以分配新的UID了。这个过程分为两步完成:
第一步:告 诉idr你需要分配新的UID,允许其在必要时调整后备树的大小。
第二步:此步骤才是真正请求新的UID。
之所以需要这两个组合动作是因为要允许调整初始大小——这中间涉及在无锁情况下分配内存的场景。
lib/radix-tree.c
/**
* idr_preload - preload for idr_alloc()
* @gfp_mask: allocation mask to use for preloading
*
* Preallocate memory to use for the next call to idr_alloc(). This function
* returns with preemption disabled. It will be enabled by idr_preload_end().
*/
void idr_preload(gfp_t gfp_mask)
{
if (__radix_tree_preload(gfp_mask, IDR_PRELOAD_SIZE))
local_lock(&radix_tree_preloads.lock);
}
EXPORT_SYMBOL(idr_preload);
static __must_check int __radix_tree_preload(gfp_t gfp_mask, unsigned nr)
{
struct radix_tree_preload *rtp;
struct radix_tree_node *node;
int ret = -ENOMEM;
/*
* Nodes preloaded by one cgroup can be used by another cgroup, so
* they should never be accounted to any particular memory cgroup.
*/
gfp_mask &= ~__GFP_ACCOUNT;
local_lock(&radix_tree_preloads.lock);
rtp = this_cpu_ptr(&radix_tree_preloads);
while (rtp->nr < nr) {
local_unlock(&radix_tree_preloads.lock);
node = kmem_cache_alloc(radix_tree_node_cachep, gfp_mask);
if (node == NULL)
goto out;
local_lock(&radix_tree_preloads.lock);
rtp = this_cpu_ptr(&radix_tree_preloads);
if (rtp->nr < nr) {
node->parent = rtp->nodes;
rtp->nodes = node;
rtp->nr++;
} else {
kmem_cache_free(radix_tree_node_cachep, node);
}
}
ret = 0;
out:
return ret;
}
lib/idr.c
int idr_alloc(struct idr *idr, void *ptr, int start, int end, gfp_t gfp)
{
u32 id = start;
int ret;
if (WARN_ON_ONCE(start < 0))
return -EINVAL;
ret = idr_alloc_u32(idr, ptr, &id, end > 0 ? end - 1 : INT_MAX, gfp);
if (ret)
return ret;
return id;
}
EXPORT_SYMBOL_GPL(idr_alloc);
int idr_alloc_u32(struct idr *idr, void *ptr, u32 *nextid,
unsigned long max, gfp_t gfp)
{
struct radix_tree_iter iter;
void __rcu **slot;
unsigned int base = idr->idr_base;
unsigned int id = *nextid;
if (WARN_ON_ONCE(!(idr->idr_rt.xa_flags & ROOT_IS_IDR)))
idr->idr_rt.xa_flags |= IDR_RT_MARKER;
id = (id < base) ? 0 : id - base;
radix_tree_iter_init(&iter, id);
slot = idr_get_free(&idr->idr_rt, &iter, gfp, max - base);
if (IS_ERR(slot))
return PTR_ERR(slot);
*nextid = iter.index + base;
/* there is a memory barrier inside radix_tree_iter_replace() */
radix_tree_iter_replace(&idr->idr_rt, &iter, slot, ptr);
radix_tree_iter_tag_clear(&idr->idr_rt, &iter, IDR_FREE);
return 0;
}
EXPORT_SYMBOL_GPL(idr_alloc_u32);
五、查找UID(idr_find)
lib/idr.c
/**
* idr_find() - Return pointer for given ID.
* @idr: IDR handle.
* @id: Pointer ID.
*
* Looks up the pointer associated with this ID. A %NULL pointer may
* indicate that @id is not allocated or that the %NULL pointer was
* associated with this ID.
*
* This function can be called under rcu_read_lock(), given that the leaf
* pointers lifetimes are correctly managed.
*
* Return: The pointer associated with this ID.
*/
void *idr_find(const struct idr *idr, unsigned long id)
{
return radix_tree_lookup(&idr->idr_rt, id - idr->idr_base);
}
EXPORT_SYMBOL_GPL(idr_find);
lib/radix-tree.c
void *radix_tree_lookup(const struct radix_tree_root *root, unsigned long index)
{
return __radix_tree_lookup(root, index, NULL, NULL);
}
EXPORT_SYMBOL(radix_tree_lookup);
void *__radix_tree_lookup(const struct radix_tree_root *root,
unsigned long index, struct radix_tree_node **nodep,
void __rcu ***slotp)
{
struct radix_tree_node *node, *parent;
unsigned long maxindex;
void __rcu **slot;
restart:
parent = NULL;
slot = (void __rcu **)&root->xa_head;
radix_tree_load_root(root, &node, &maxindex);
if (index > maxindex)
return NULL;
while (radix_tree_is_internal_node(node)) {
unsigned offset;
parent = entry_to_node(node);
offset = radix_tree_descend(parent, &node, index);
slot = parent->slots + offset;
if (node == RADIX_TREE_RETRY)
goto restart;
if (parent->shift == 0)
break;
}
if (nodep)
*nodep = parent;
if (slotp)
*slotp = slot;
return node;
}
六、删除UID(idr_remove)
lib/idr.c
/**
* idr_remove() - Remove an ID from the IDR.
* @idr: IDR handle.
* @id: Pointer ID.
*
* Removes this ID from the IDR. If the ID was not previously in the IDR,
* this function returns %NULL.
*
* Since this function modifies the IDR, the caller should provide their
* own locking to ensure that concurrent modification of the same IDR is
* not possible.
*
* Return: The pointer formerly associated with this ID.
*/
void *idr_remove(struct idr *idr, unsigned long id)
{
return radix_tree_delete_item(&idr->idr_rt, id - idr->idr_base, NULL);
}
EXPORT_SYMBOL_GPL(idr_remove);
lib/radix-tree.c
void *radix_tree_delete_item(struct radix_tree_root *root,
unsigned long index, void *item)
{
struct radix_tree_node *node = NULL;
void __rcu **slot = NULL;
void *entry;
entry = __radix_tree_lookup(root, index, &node, &slot);
if (!slot)
return NULL;
if (!entry && (!is_idr(root) || node_tag_get(root, node, IDR_FREE,
get_slot_offset(node, slot))))
return NULL;
if (item && entry != item)
return NULL;
__radix_tree_delete(root, node, slot);
return entry;
}
EXPORT_SYMBOL(radix_tree_delete_item);
七、撤销idr(idr_destroy)
lib/radix-tree.c
/**
* idr_destroy - release all internal memory from an IDR
* @idr: idr handle
*
* After this function is called, the IDR is empty, and may be reused or
* the data structure containing it may be freed.
*
* A typical clean-up sequence for objects stored in an idr tree will use
* idr_for_each() to free all objects, if necessary, then idr_destroy() to
* free the memory used to keep track of those objects.
*/
void idr_destroy(struct idr *idr)
{
struct radix_tree_node *node = rcu_dereference_raw(idr->idr_rt.xa_head);
if (radix_tree_is_internal_node(node))
radix_tree_free_nodes(node);
idr->idr_rt.xa_head = NULL;
root_tag_set(&idr->idr_rt, IDR_FREE);
}
EXPORT_SYMBOL(idr_destroy);
八、举例应用
1. 散列表(Hash Table)在 Linux 内核中的应用
散列表(Hash Table)是一种基于哈希函数的高效数据结构。它通过将键值映射到固定大小的数组中,能够实现平均常数时间(O(1))的插入、删除和查找操作。因此,在许多需要快速查找和更新的场景中,散列表是一个非常常用的数据结构。
散列表应用举例
进程表(PID 表)
在 Linux 内核中,每个进程都有一个唯一的进程标识符(PID)。Linux 使用散列表来管理进程,以便能够高效地查找和访问特定的进程信息。PID 是散列表的键,而对应的进程控制块(PCB,Process Control Block)是值。
struct pid_hash_table {
struct hlist_head table[PID_HASH_SIZE];
};
在 Linux 内核中,pid_hash_table
是一个散列表,每个哈希桶使用一个链表存储相同哈希值的进程。通过 pid
哈希查找,内核可以高效地找到与特定 PID 相关的进程。
虚拟内存管理(VM)
Linux 内核在虚拟内存管理中也大量使用散列表。每个进程有一个虚拟地址空间,内核会通过散列表来管理虚拟地址与物理地址之间的映射关系。在页表(Page Tables)中,虚拟地址通过散列函数映射到相应的物理页面,从而加速页面的查找。
例如,Linux 内核中使用的 mm_struct
结构体就包含了虚拟内存的管理信息。在页表查找过程中,散列表被用来快速找到虚拟地址对应的物理地址。
文件系统(如 ext4)
在文件系统中,散列表常常用于管理 inode(文件的元数据)。例如,在 ext4 文件系统中,散列表用于根据文件的 inode number 快速查找文件的详细信息。每个 inode 被映射到散列表的桶中,内核可以通过 inode number 快速定位文件的 inode 结构体。
Linux 内核中散列表的实现
在 Linux 内核中,散列表通常使用 hlist_head
来表示哈希表的桶(Bucket)。每个桶是一个链表,哈希冲突通过链表来处理。Linux 内核提供了一些宏和函数来方便地操作散列表。
- 哈希桶定义:
struct hlist_head {
struct hlist_node *first; // 指向链表头的指针
};
- 哈希操作函数:
void hash_add(struct hlist_head *head, struct hlist_node *node); // 插入元素
void hash_del(struct hlist_node *node); // 删除元素
通过这些函数和结构,内核能够高效地管理哈希表中的元素。
2. 自平衡二叉搜索树(Red-Black Tree)在 Linux 内核中的应用
红黑树(Red-Black Tree)是一种自平衡的二叉搜索树,它保证了树的高度始终处于对数级别,因此能够在 O(log n) 的时间复杂度内执行查找、插入和删除操作。红黑树的平衡性确保了在最坏情况下也能保持较好的性能,这对于需要频繁更新和查找的内核子系统非常有用。
红黑树应用举例
调度器
Linux 调度器(特别是 CFS 调度器)使用红黑树来维护进程的运行队列。调度器根据进程的时间片和优先级选择合适的进程来执行。为了保证高效的进程调度,调度器使用红黑树来对进程进行排序,并在每次调度时能够快速找到优先级最高的进程。
struct rb_root run_queue;
在 Linux 调度器中,run_queue
是一个红黑树,存储所有就绪的进程。每个进程按照优先级(如 CFS 树中的虚拟运行时间)在红黑树中插入或删除,确保查找最优进程的操作能够高效完成。
内存管理
在 Linux 内核的内存管理中,红黑树也用于高效管理物理内存块(例如,管理内存页的分配和释放)。例如,内核中的 buddy system
使用红黑树来管理空闲内存块的合并和分割。当内核分配内存时,它会使用红黑树查找合适大小的空闲块。
另外,vm_area_struct
结构体中会通过红黑树来维护进程的虚拟内存区域。每个进程的内存区域都会按地址大小顺序插入红黑树中,确保能够高效地执行内存区域的查找和合并操作。
文件系统(ext4)
在 ext4 文件系统中,红黑树用于管理目录项(Directory Entries)。当目录中的文件增多时,红黑树能够帮助内核在 O(log n) 时间内高效地查找文件,并动态调整目录项的顺序。此外,ext4 文件系统还使用红黑树来管理块设备上的空间,确保高效的块分配和回收。
Linux 内核中红黑树的实现
在 Linux 内核中,红黑树的实现非常高效,它通过 struct rb_node
来表示树的节点,struct rb_root
来表示树的根节点。Linux 内核提供了一系列的宏来操作红黑树,例如插入、删除和查找。
- 结构体定义:
struct rb_node {
unsigned long rb_parent_color;
struct rb_node *rb_left;
struct rb_node *rb_right;
};
struct rb_root {
struct rb_node *rb_node;
};
- 红黑树操作函数:
void rb_insert_color(struct rb_node *node, struct rb_root *root); // 插入节点
void rb_erase(struct rb_node *node, struct rb_root *root); // 删除节点
struct rb_node *rb_search(struct rb_root *root, unsigned long key); // 查找节点
通过这些操作,Linux 内核能够高效地进行红黑树的管理和操作。
3. 散列表与红黑树的对比与选择
特性 | 散列表(Hash Table) | 红黑树(Red-Black Tree) |
---|---|---|
查找/插入/删除复杂度 | 平均 O(1),最坏 O(n)(哈希冲突的情况) | O(log n) |
内存开销 | 较低,除非哈希表桶数量过多 | 较高,每个节点需要存储颜色信息和左右子节点指针 |
有序性 | 无序 | 有序(能够按排序顺序进行遍历) |
适用场景 | 快速查找、插入和删除,且对顺序无要求 | 需要维持顺序、按优先级查找、内存管理等 |
常见应用 | 进程表、虚拟内存管理、网络连接表、文件系统索引等 | 调度器(CFS)、内存管理、文件系统(ext4)等 |
4. 总结
- 散列表:适合用在需要快速查找、插入和删除的场景,尤其是当数据量大且不需要维护顺序时。在 Linux 内核中,散列表被广泛用于管理进程表、虚拟内存、文件系统 inode 等。
- 红黑树:适合用在需要维护有序数据结构的场景,尤其是当数据插入、删除频繁并且需要按顺序访问时。在 Linux 内核中,红黑树常见于调度器、内存管理、文件系统等场景。