klist 迭代器初始化:klist_iter_init_node 与 klist_iter_init
本代码片段展示了 Linux 内核中 klist
(一种引用计数的链表)迭代器的初始化函数。其核心功能是为一个 klist_iter
结构体(迭代器/游标)设置其初始状态,使其准备好开始遍历一个 klist
。它通过安全地获取对指定起始节点的引用,来确保遍历的起点是有效且不会被并发释放的,从而为后续的 klist_next
操作提供了安全保障。
实现原理分析
此代码是 klist
安全遍历机制的入口点,其实现原理的关键在于利用引用计数来解决并发环境下的 use-after-free 问题。
-
引用计数作为安全保障:
klist
的设计哲学是,任何对链表节点的有效访问都必须持有 该节点的一个引用。迭代器klist_iter
也不例外,它的i_cur
字段指向的当前节点,必须是一个已经被迭代器持有了引用的节点。- 初始化过程的本质,就是为迭代器安全地获取对第一个节点(即起始节点)的引用。
-
kref_get_unless_zero
原子操作:klist_iter_init_node
函数中最核心的操作是kref_get_unless_zero(&n->n_ref)
。这是一个原子的、有条件的引用计数增加操作。- 原子性: 它能保证在检查和增加引用计数的过程中,不会被其他任务或中断打断。
- 条件性 (
_unless_zero
) : 这是最关键的特性。它只在节点的当前引用计数不为零 的情况下,才会成功地将引用计数加一并返回true
。如果引用计数已经为零,意味着该节点正处于被销毁的过程中,此时尝试获取一个新的引用是不安全的。在这种情况下,kref_get_unless_zero
会失败并返回false
。 - 目的 : 这个函数完美地解决了在初始化迭代器时可能发生的竞态条件:当一个任务尝试以节点
n
为起点初始化迭代器时,另一个任务(或中断)可能正在释放节点n
。kref_get_unless_zero
确保了只有当节点n
仍然"存活"(引用计数 > 0)时,迭代器才能成功地将其设置为当前节点,否则i->i_cur
将保持为NULL
,表示一个无效或空的起始点。
-
封装与便利性 (
klist_iter_init
):klist_iter_init
是一个简单的内联封装函数。它调用klist_iter_init_node
并将起始节点n
显式地设置为NULL
。- 这为最常见的用例------从链表的头部开始遍历------提供了一个更简洁的 API。当
n
为NULL
时,i_cur
会被初始化为NULL
,后续的klist_next
调用将自动从klist
的链表头开始查找第一个有效的节点。
特定场景分析:单核、无MMU的STM32H750平台
功能相关性
klist
是内核设备模型的基础。前文分析的 class_dev_iter_init
函数,其核心正是调用了 klist_iter_init_node
。因此,对于在 STM32H750 上运行的、使用了任何标准设备驱动(块设备、输入、网络等)的系统,klist_iter_init_node
是一个频繁被调用的、基础性的函数,是实现安全设备遍历的基石。
单核环境影响
- 原子操作的必要性 :
kref_get_unless_zero
底层是基于原子操作(atomic_t
)实现的。ARMv7-M 架构为这些原子操作提供了专门的指令支持(如LDREX
/STREX
),确保了即使在单核系统上,操作也是原子的。 - 并发源 : 在多核系统中,并发主要来自不同的 CPU。在单核 处理器上,并发主要来自中断 和可抢占的任务 。一个中断处理程序(例如,处理设备热拔除的中断)完全有可能在主线任务(例如,一个正在读取
/proc/partitions
的进程)调用klist_iter_init_node
的过程中,尝试释放同一个klist_node
。 - 结论 : 因此,
kref_get_unless_zero
的原子性和条件性在单核环境下仍然是绝对必要的 。它保证了在任务上下文和中断上下文之间对klist
节点进行操作的安全性。
无MMU影响
klist
和kref
(引用计数)是纯粹的内核数据结构和算法。它们的所有操作,包括链表指针的移动和引用计数的增减,都发生在内核的内存空间中。- 这些机制完全不依赖于虚拟内存地址到物理地址的翻译。
- 因此,缺少 MMU 对
klist_iter_init_node
和klist_iter_init
函数的逻辑和正确性没有任何影响。
代码分析
c
/**
* @brief klist_iter_init_node - 初始化一个 klist_iter 结构体。
* @param k: 要遍历的 klist。
* @param i: 要被初始化的 klist_iter 结构体。
* @param n: 迭代开始的节点。
*
* 与 klist_iter_init() 类似,但从指定的节点 @n 开始,而不是从链表头开始。
*/
void klist_iter_init_node(struct klist *k, struct klist_iter *i,
struct klist_node *n)
{
// 在迭代器中保存要遍历的 klist 的指针。
i->i_klist = k;
// 将迭代器的当前节点指针初始化为 NULL,这是一个安全默认值。
i->i_cur = NULL;
// 如果提供了一个有效的起始节点 n,并且...
// ...成功地获取了该节点的引用(即该节点的引用计数不为0),则...
if (n && kref_get_unless_zero(&n->n_ref))
// ...将迭代器的当前节点设置为该起始节点。
// 这确保了迭代器持有一个对起始节点的有效引用。
i->i_cur = n;
}
EXPORT_SYMBOL_GPL(klist_iter_init_node);
/**
* @brief klist_iter_init - 初始化一个 klist_iter 结构体。
* @param k: 要遍历的 klist。
* @param i: 要被初始化的 klist_iter 结构体。
*
* 与 klist_iter_init_node() 类似,但总是从链表的头部开始。
*/
void klist_iter_init(struct klist *k, struct klist_iter *i)
{
// 调用核心实现,并将起始节点设置为 NULL,
// 表示后续的 klist_next() 将从链表头开始查找。
klist_iter_init_node(k, i, NULL);
}
EXPORT_SYMBOL_GPL(klist_iter_init);