title: cacheinfo
categories:
- linux
- drivers
- base
tags: - linux
- drivers
- base
abbrlink: 7a40c4d3
date: 2025-11-02 13:23:38
文章目录
- 核心数据结构与缓存共享判断 (get_cpu_cacheinfo, cache_leaves_are_shared, last_level_cache_is_valid, last_level_cache_is_shared)
- [从设备树 (OF) 解析缓存属性 (init_of_cache_level, cache_setup_of_node)](#从设备树 (OF) 解析缓存属性 (init_of_cache_level, cache_setup_of_node))
- 缓存信息获取与填充总控 (fetch_cache_info, detect_cache_attributes)
- 缓存共享关系建立与维护 (cache_shared_cpu_map_setup, cache_shared_cpu_map_remove)
- [Sysfs 接口创建与 CPU 热插拔管理 (cacheinfo_sysfs_init, cacheinfo_cpu_online, cacheinfo_cpu_pre_down)](#Sysfs 接口创建与 CPU 热插拔管理 (cacheinfo_sysfs_init, cacheinfo_cpu_online, cacheinfo_cpu_pre_down))

/drivers/base/cacheinfo.c:Linux 内核 CPU 缓存信息探测
介绍
/drivers/base/cacheinfo.c 是 Linux 内核驱动核心(driver core)的一部分,其主要功能是探测处理器的缓存信息,并通过 sysfs 文件系统将这些信息导出到用户空间。 这使得用户和应用程序能够方便地获取关于 CPU 缓存层级、大小、关联性等详细信息,而无需编写复杂的底层代码。该机制的设计旨在提供一个统一和通用的接口,以适应不同处理器架构的差异。
历史与背景
为了解决什么特定问题而诞生?
在 cacheinfo.c 通用框架出现之前,不同的处理器架构(如 x86, PowerPC, IA-64, S390)在 Linux 内核中拥有各自独立的缓存信息探测和导出实现。 这种碎片化的实现方式导致了代码冗余,并且为新的架构(如 ARM 和 ARM64)添加支持时带来了不便。因此,引入一个通用的 cacheinfo 框架是为了解决以下问题:
- 代码重复:消除不同架构下功能相似的代码。
- 缺乏统一接口:为用户空间工具和应用程序提供一个稳定、一致的方式来查询缓存信息。
- 可移植性差:简化向新处理器架构移植缓存信息探测功能的过程。
cacheinfo.c 的诞生,标志着 Linux 内核在处理器拓扑结构信息管理上向更通用、更标准化的方向发展。
发展经历了哪些重要的里程碑或版本迭代?
drivers/base/cacheinfo.c 的演进是 Linux 内核逐步统一和抽象硬件信息过程的一部分。
- 早期实现 :最初,缓存信息的探测和导出逻辑散布在各个特定架构的代码中,例如
arch/x86/kernel/cpu/intel_cacheinfo.c。 - 通用框架的提出:为了解决代码冗余和接口不一的问题,社区提出了将缓存信息支持通用化的想法,类似于已有的 CPU 拓扑信息支持。
- v5 补丁集(2014年) :由 Sudeep Holla 提交的补丁集是
cacheinfo.c通用化的一个重要里程碑。 这个补丁集基于 x86 的实现,创建了一个通用的cacheinfo基础设施,并将当时已有的 x86, ia64, powerpc 和 s390 架构的实现迁移到这个新框架下。同时,该补丁集也为 ARM 和 ARM64 架构添加了缓存信息支持。 - 后续完善 :自通用框架建立以来,
cacheinfo.c不断进行着维护和完善,以支持新的处理器特性和固件接口(如 ACPI 和设备树)。
目前该技术的社区活跃度和主流应用情况如何?
cacheinfo.c 作为 Linux 内核中一个成熟且基础的功能,其代码相对稳定。社区的活跃度主要体现在为新出现的处理器架构和特性提供支持,以及在固件接口(ACPI, Device Tree)解析方面的持续改进。
该技术被主流 Linux 发行版广泛采用,是许多系统信息查看和性能分析工具的基础。例如,lscpu 命令以及一些性能剖析工具会利用 sysfs 中由 cacheinfo.c 导出的信息来展示处理器的缓存布局。
核心原理与设计
它的核心工作原理是什么?
cacheinfo.c 的核心工作原理可以概括为以下几个步骤:
- 初始化 :在内核启动过程中,当 CPU 设备被添加到系统时,
cacheinfo子系统会为每个逻辑 CPU 进行初始化。 - 信息获取 :它会调用特定于具体架构的函数来获取缓存信息。这些函数通过不同的方式获取信息:
- 硬件指令 :在 x86 架构上,通常使用
CPUID指令来获取缓存的详细参数。 - 专用寄存器 :在 ARM 架构上,则会读取像
CCSIDR,CLIDR,CSSELR等专用寄存器。 - 固件接口:通过解析设备树(Device Tree)或 ACPI 表来获取无法由 CPU 核心直接探测到的信息,例如多核之间共享的缓存(如 L3 缓存)的拓扑结构。
- 硬件指令 :在 x86 架构上,通常使用
- 数据结构填充 :获取到的缓存信息会被填充到内核的
struct cacheinfo数据结构中。每个缓存层级(L1, L2, L3 等)和类型(数据缓存, 指令缓存, 统一缓存)都会作为一个独立的 "leaf" 来描述。 - sysfs 接口导出 :最后,内核会通过 sysfs 在
/sys/devices/system/cpu/cpuX/cache/目录下创建相应的目录和文件,将struct cacheinfo中的信息以文件的形式展现给用户空间。 例如,level,size,type,shared_cpu_map等文件。
它的主要优势体现在哪些方面?
- 统一性与标准化:为所有支持的处理器架构提供了一个统一的、标准的接口来访问缓存信息,简化了用户空间工具的开发。
- 通用性与可扩展性:基于驱动核心(driver core)的设计使其易于扩展,能够方便地为新的处理器架构添加支持。
- 无需特殊权限:用户可以通过标准的文件系统接口读取 sysfs 中的信息,而不需要特殊的权限或执行特权指令。
- 信息相对全面:能够结合硬件探测和固件信息,提供包括缓存大小、级别、类型、行大小、关联方式以及共享 CPU 列表等在内的丰富信息。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 静态信息 :
cacheinfo.c提供的是处理器的物理缓存属性,这些是静态信息。它无法提供动态的缓存使用情况,如缓存命中率、未命中率等性能指标。 - 依赖于架构和固件的实现 :导出信息的准确性和完整性高度依赖于底层特定架构代码的实现质量,以及固件(BIOS/UEFI, 设备树)所提供信息的准确性。如果固件信息不完整或不正确,
cacheinfo导出的信息也可能出错。 - 虚拟化环境下的限制 :在虚拟机中,
cacheinfo.c显示的是虚拟机监控器(Hypervisor)暴露给客户机(Guest OS)的缓存信息,这可能与物理硬件的实际情况不符。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
cacheinfo.c 是获取 CPU 物理缓存布局信息的首选方案。具体场景包括:
- 性能优化和分析:开发者和系统管理员可以根据缓存的大小和结构来调整其应用程序的数据结构和访问模式,以提高缓存命中率,从而提升程序性能。例如,了解 L1 数据缓存的大小有助于确定最优的循环分块大小。
- 系统信息和监控工具 :需要展示系统硬件信息的工具,如
lscpu、hwloc等,都会读取 sysfs 中的缓存信息来向用户报告 CPU 的详细规格。 - 任务调度和资源绑定 :在高性能计算(HPC)场景下,调度器可以利用
shared_cpu_map等信息来决定如何将进程或线程绑定到特定的 CPU 核心,以最大化地利用共享缓存,或避免不同任务在共享缓存上的相互干扰。 - 教学与研究 :在操作系统和计算机体系结构的学习和研究中,
cacheinfo.c提供的接口是一个直观、便捷的工具,用于观察和理解不同处理器的缓存层次结构。
是否有不推荐使用该技术的场景?为什么?
不推荐单独使用 cacheinfo.c 导出的信息来进行动态的性能瓶颈分析。
- 分析缓存命中/未命中率 :如前所述,
cacheinfo.c只提供静态的硬件参数。要分析程序运行时的缓存效率,应该使用性能剖析工具,如perf。perf可以利用处理器的性能监控单元(PMU)来直接测量缓存命中、未命中等硬件事件。 - 实时性能监控 :对于需要实时监控缓存使用情况的场景,同样应该依赖
perf等能够提供动态性能计数器的工具。
对比分析
请将其 与 其他相似技术 进行详细对比。
| 特性 | /drivers/base/cacheinfo.c (通过 sysfs) |
perf 工具 |
CPUID 指令 (x86) / 专用寄存器 |
|---|---|---|---|
| 实现方式 | 内核驱动,读取硬件/固件信息并导出到 sysfs 文件系统。 | 内核子系统,利用处理器的性能监控单元(PMU)来采样或计数硬件事件。 | 直接在用户空间或内核空间执行特定的 CPU 指令或访问 MSR。 |
| 信息类型 | 静态的物理缓存属性(大小、级别、关联性、共享关系等)。 | 动态的性能事件(缓存命中数、未命中数、访存延迟等)。 | 静态的物理缓存属性,与 sysfs 类似,但需要自行解析。 |
| 性能开销 | 极低。仅在读取 sysfs 文件时有少量开销。 | 较低(采样模式)到中等(计数模式)。持续监控会对系统产生一定开销。 | 极低。单次指令执行的开销可忽略不计。 |
| 资源占用 | 极低。在 sysfs 中表现为一些文本文件。 | 采样模式下会产生数据文件,可能占用较大磁盘空间。 | 几乎没有额外的资源占用。 |
| 隔离级别 | 用户态可直接访问,无需特殊权限。 | 通常需要 root 权限来访问系统范围的性能事件。 | 在用户态执行 CPUID 通常受限,可能需要内核模块或特殊权限。 |
| 启动速度 | 信息在系统启动时生成,读取速度快。 | 启动和附加到进程需要一定时间。 | 指令执行速度极快。 |
| 易用性 | 非常高。简单的文件读写操作即可获取信息。 | 较高。命令行工具,功能强大但参数较多。 | 低。需要编写汇编或特定的 C 代码,并要处理不同 CPU 型号的差异。 |
总结
请为我总结其关键特性,并提供学习该技术的要点建议。
关键特性总结:
- 核心功能:作为 Linux 内核的一部分,负责探测 CPU 缓存信息并将其通过 sysfs 导出到用户空间。
- 统一接口:为用户空间提供了一个跨架构的、标准的、基于文件的接口来查询静态的 CPU 缓存属性。
- 信息全面:提供包括缓存级别、大小、类型、行大小、关联方式和 CPU 共享关系在内的详细信息。
- 实现通用:采用通用的驱动框架,简化了对新处理器架构的支持。
学习该技术的要点建议:
- 理解 sysfs :首先要熟悉 Linux 的 sysfs 文件系统,理解其作为内核与用户空间交互桥梁的角色。亲手去浏览
/sys/devices/system/cpu/目录,特别是cpuX/cache/子目录,查看其中的文件和内容。 - 实践操作 :使用
cat命令读取level,size,type,coherency_line_size,ways_of_associativity,shared_cpu_list等文件,并将这些信息与lscpu命令的输出进行比对,以建立直观的认识。 - 区分静态与动态信息 :明确
cacheinfo提供的是静态的硬件参数,而perf等工具提供的是动态的性能数据。理解两者的应用场景和界限是关键。 - 内核源码阅读(进阶) :如果想深入理解其工作原理,可以阅读
drivers/base/cacheinfo.c的源码,并结合某一特定架构的实现代码(如arch/x86/kernel/cpu/cacheinfo.c)来理解信息获取的完整流程。这将有助于理解内核如何与硬件和固件进行交互。
核心数据结构与缓存共享判断 (get_cpu_cacheinfo, cache_leaves_are_shared, last_level_cache_is_valid, last_level_cache_is_shared)
核心功能
该部分代码定义了用于存储每个CPU缓存信息的关键数据结构 cpu_cacheinfo,并提供了一系列宏和函数来访问这些数据。它还实现了用于判断不同CPU之间或同一CPU的不同缓存层级之间是否存在共享关系的核心逻辑。get_cpu_cacheinfo 用于获取指定CPU的缓存信息结构体指针。cache_leaves_are_shared 判断两个缓存条目(leaf)是否属于同一个物理缓存。last_level_cache_is_valid 和 last_level_cache_is_shared 则专门用于检查并判断末级缓存(LLC)的有效性和共享状态。
实现原理分析
cache_leaves_are_shared 函数的实现包含了两种策略。在不使用设备树(DT)或ACPI的系统中,它采用一种简化的假设:仅L1缓存是CPU独占的,所有其他级别的缓存(L2, L3等)都被视为系统范围内所有核心共享。在现代嵌入式系统中,通常使用DT或ACPI。此时,该函数依赖于固件提供的信息来判断共享关系。它优先比较 id 字段(如果 CACHE_ID 属性被设置),这是一个唯一的缓存标识符。如果 id 不可用,它会比较 fw_token。fw_token 通常被设置为指向设备树中描述该缓存的设备节点 (device_node) 的指针。如果两个缓存条目的 fw_token 相同,意味着它们由设备树中的同一个节点描述,因此它们必然是同一个物理缓存的不同端口或视图,即它们是共享的。这种通过比较固件节点指针来确定硬件共享性的方法是一种高效且可靠的技巧。
特定场景分析 (单核、无MMU的 STM32H750)
在STM32H750这样的单核(single-core)平台上,关于多CPU之间缓存共享的逻辑实际上是被简化或无效化的。
- 单核行为 : 由于系统中只有一个核心(CPU0),
cache_shared_cpu_map(后续代码将分析) 这个位掩码中将永远只设置第0位。函数last_level_cache_is_shared(cpu_x, cpu_y)如果被调用,其参数cpu_x和cpu_y必然都为0,因此其判断失去了"多核间共享"的意义,仅仅是检查自身LLC的有效性。整个共享判断机制在此场景下主要用于正确识别和关联由设备树定义的、属于这唯一核心的各级缓存。 - 无MMU影响 : 无MMU(内存管理单元)的设定主要影响虚拟内存和物理内存的映射关系。此部分代码专注于通过固件(设备树)接口读取硬件的拓扑结构和物理属性(如大小、行大小、关联度),不涉及内存地址的转换。因此,MMU的存在与否对
cacheinfo子系统的这部分逻辑没有直接影响。 - 设备树依赖 : 在STM32H750平台上,
cacheinfo的所有信息来源将是设备树(Device Tree)。CONFIG_OF必须被使能。代码中cache_leaves_are_shared函数将执行sib_leaf->fw_token == this_leaf->fw_token的判断逻辑。因此,STM32H750的设备树文件(.dts) 中对缓存节点的正确描述是该功能能够正常工作的根本前提。
源码及逐行注释
c
#include <linux/acpi.h>
#include <linux/bitfield.h>
#include <linux/bitops.h>
#include <linux/cacheinfo.h>
#include <linux/compiler.h>
#include <linux/cpu.h>
#include <linux/device.h>
#include <linux/init.h>
#include <linux/of.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/smp.h>
#include <linux/sysfs.h>
/**
* @var ci_cpu_cacheinfo
* @brief 每个CPU的缓存信息结构体实例。
* @note 使用 DEFINE_PER_CPU 宏为系统中每个CPU核心定义一个独立的 cpu_cacheinfo 结构体变量。
*/
static DEFINE_PER_CPU(struct cpu_cacheinfo, ci_cpu_cacheinfo);
/**
* @def ci_cacheinfo(cpu)
* @brief 获取指定CPU的 cpu_cacheinfo 结构体指针的宏。
* @param cpu [in] CPU的逻辑编号。
* @return 指向指定CPU的 cpu_cacheinfo 结构体的指针。
*/
#define ci_cacheinfo(cpu) (&per_cpu(ci_cpu_cacheinfo, cpu))
/**
* @def cache_leaves(cpu)
* @brief 获取指定CPU的缓存条目(leaf)数量的宏。
* @param cpu [in] CPU的逻辑编号。
* @return 缓存条目的数量。
*/
#define cache_leaves(cpu) (ci_cacheinfo(cpu)->num_leaves)
/**
* @def per_cpu_cacheinfo(cpu)
* @brief 获取指定CPU的 cacheinfo 结构体数组(信息列表)的宏。
* @param cpu [in] CPU的逻辑编号。
* @return 指向 cacheinfo 结构体数组的指针。
*/
#define per_cpu_cacheinfo(cpu) (ci_cacheinfo(cpu)->info_list)
/**
* @def per_cpu_cacheinfo_idx(cpu, idx)
* @brief 获取指定CPU的特定索引的 cacheinfo 结构体指针的宏。
* @param cpu [in] CPU的逻辑编号。
* @param idx [in] 缓存条目的索引。
* @return 指向指定索引的 cacheinfo 结构体的指针。
*/
#define per_cpu_cacheinfo_idx(cpu, idx) \
(per_cpu_cacheinfo(cpu) + (idx))
/**
* @var use_arch_info
* @brief 一个布尔标志,若在设备树(DT)或ACPI中未找到缓存信息,则此标志被置为true。
*/
static bool use_arch_info;
/**
* @brief 获取指定CPU的缓存信息结构体。
* @param[in] cpu CPU的逻辑编号。
* @return 指向该CPU的 cpu_cacheinfo 结构体的指针。
*/
struct cpu_cacheinfo *get_cpu_cacheinfo(unsigned int cpu)
{
return ci_cacheinfo(cpu); //!< 调用宏返回指定CPU的缓存信息结构体指针。
}
/**
* @brief 判断两个缓存条目(leaf)是否指向同一个共享的物理缓存。
* @param[in] this_leaf 指向第一个缓存条目的指针。
* @param[in] sib_leaf 指向第二个(兄弟)缓存条目的指针。
* @return 如果缓存是共享的,则返回true;否则返回false。
*/
static inline bool cache_leaves_are_shared(struct cacheinfo *this_leaf,
struct cacheinfo *sib_leaf)
{
/*
* 对于没有设备树(DT)或ACPI的系统,或强制使用架构信息的系统,
* 采用一个简化的假设:L1缓存是核心独占的,而所有其他级别的缓存都是系统范围内共享的。
*/
if (!(IS_ENABLED(CONFIG_OF) || IS_ENABLED(CONFIG_ACPI)) ||
use_arch_info)
return (this_leaf->level != 1) && (sib_leaf->level != 1); //!< 如果两个缓存的级别都不为1,则认为是共享的。
/*
* 如果两个缓存条目都设置了CACHE_ID属性,
* 则通过比较它们的ID来判断是否共享。
*/
if ((sib_leaf->attributes & CACHE_ID) &&
(this_leaf->attributes & CACHE_ID))
return sib_leaf->id == this_leaf->id; //!< 如果ID相同,则它们是同一个物理缓存,是共享的。
/*
* 如果没有CACHE_ID,则比较它们的固件令牌(fw_token)。
* fw_token通常指向设备树(DT)中的同一个设备节点。
*/
return sib_leaf->fw_token == this_leaf->fw_token; //!< 如果固件令牌相同,则它们是共享的。
}
/**
* @brief 检查指定CPU的末级缓存(Last-Level Cache, LLC)信息是否有效。
* @param[in] cpu CPU的逻辑编号。
* @return 如果LLC信息有效,则返回true;否则返回false。
*/
bool last_level_cache_is_valid(unsigned int cpu)
{
struct cacheinfo *llc; //!< 定义一个指针用于指向末级缓存的信息。
if (!cache_leaves(cpu) || !per_cpu_cacheinfo(cpu)) //!< 检查该CPU是否存在缓存条目以及信息列表是否已分配。
return false; //!< 如果没有,则直接返回false。
llc = per_cpu_cacheinfo_idx(cpu, cache_leaves(cpu) - 1); //!< 获取末级缓存的条目,它位于信息列表的最后一个位置。
return (llc->attributes & CACHE_ID) || !!llc->fw_token; //!< 如果LLC设置了CACHE_ID属性或其固件令牌不为空,则认为它是有效的。
}
/**
* @brief 判断两个不同CPU的末级缓存(LLC)是否是共享的。
* @param[in] cpu_x 第一个CPU的逻辑编号。
* @param[in] cpu_y 第二个CPU的逻辑编号。
* @return 如果两个CPU的LLC是共享的,则返回true;否则返回false。
*/
bool last_level_cache_is_shared(unsigned int cpu_x, unsigned int cpu_y)
{
struct cacheinfo *llc_x, *llc_y; //!< 定义指针分别指向两个CPU的LLC信息。
if (!last_level_cache_is_valid(cpu_x) || //!< 检查两个CPU的LLC信息是否都有效。
!last_level_cache_is_valid(cpu_y))
return false; //!< 如果有任何一个无效,则它们不可能是共享的。
llc_x = per_cpu_cacheinfo_idx(cpu_x, cache_leaves(cpu_x) - 1); //!< 获取CPU_X的末级缓存条目。
llc_y = per_cpu_cacheinfo_idx(cpu_y, cache_leaves(cpu_y) - 1); //!< 获取CPU_Y的末级缓存条目。
return cache_leaves_are_shared(llc_x, llc_y); //!< 调用底层函数来判断这两个LLC条目是否代表同一个物理缓存。
}
从设备树 (OF) 解析缓存属性 (init_of_cache_level, cache_setup_of_node)
核心功能
该部分代码实现了从设备树中发现、计数并解析缓存节点属性的功能。init_of_cache_level 遍历与一个CPU关联的缓存层级链(通过 next-level-cache 属性链接),计算出总的缓存级别数(num_levels)和缓存条目数(num_leaves)。cache_setup_of_node 则负责再次遍历这个链条,并对每一个缓存节点,调用辅助函数(如 cache_size, cache_get_line_size, cache_nr_sets 等)来读取具体的硬件参数(如大小、行大小、组数),计算关联度,并为共享缓存分配一个唯一的ID,最终将这些信息填充到对应CPU的 cacheinfo 结构体中。
实现原理分析
-
缓存拓扑遍历 : 内核通过
of_find_next_cache_node函数在设备树中遍历缓存层级。这个遍历的起点是CPU节点本身(可能描述L1缓存),然后通过节点中的next-level-cache属性(一个指向下一个缓存节点的句柄,即phandle)来找到L2缓存节点,再从L2找到L3,以此类推,形成一个缓存拓扑链。 -
缓存属性读取 : 代码定义了一个
cache_type_info结构体数组,用于映射不同缓存类型(统一、指令、数据)到设备树中不同的属性名(例如,统一缓存的大小属性是 "cache-size",指令缓存是 "i-cache-size")。这种结构化的方式使得代码可以灵活地处理不同类型的缓存。 -
关联度的计算 : 缓存的关联度(ways of associativity)不是直接从设备树读取的,而是通过一个重要的数学公式计算得出:
关联度 = (缓存总大小 / 缓存组数) / 缓存行大小即
ways_of_associativity = (size / number_of_sets) / coherency_line_size。这个公式基于缓存组织的基本原理:
缓存总大小 = 组数 × 每组的行数(即关联度) × 行大小。通过读取总大小、组数和行大小,就可以反推出关联度。这是一个典型的利用硬件基本原理来推导高级参数的技巧。 -
共享缓存ID的确定 :
cache_of_set_id函数实现了一个为共享缓存确定唯一ID的机制。它会遍历系统中所有的CPU,检查每个CPU是否与当前的缓存节点相关联(通过match_cache_node)。对于所有共享此缓存的CPU,它会获取它们的硬件ID(hwid),并选择其中最小的那个ID作为这个共享缓存的唯一标识符。这确保了一个共享物理缓存,无论从哪个核心的角度去看,都获得相同的、稳定的ID。
特定场景分析 (单核、无MMU的 STM32H750)
在STM32H750平台上,这部分代码的行为是确定且至关重要的。
-
设备树是唯一信息源 : 由于没有ACPI,
of_have_populated_dt()会返回真,因此系统将完全依赖这部分代码来获取缓存信息。STM32H750的设备树文件(.dts)必须精确地描述其缓存结构。例如,CPU节点需要有next-level-cache属性指向描述其L1缓存的节点。如果存在L2缓存,L1缓存节点也需要有next-level-cache指向L2节点。 -
缓存节点描述 : Cortex-M7内核通常具有分离的L1指令缓存和数据缓存。因此,在设备树中,CPU节点本身(或其直接关联的L1节点)需要包含类似
i-cache-size和d-cache-size的属性。of_count_cache_leaves函数会根据这些属性的存在来确定L1层级有两个缓存条目(leaves)。 -
单核简化 :
cache_of_set_id函数的逻辑虽然会执行,但由于for_each_of_cpu_node循环只会找到一个CPU(CPU0),因此min_id的计算会变得非常简单,最终结果就是CPU0的硬件ID。 -
函数调用流程 : 当CPU0上线时,
detect_cache_attributes(后续分析) 会被调用,它会触发init_of_cache_level(0)来确定缓存级别和条目数。然后,cache_setup_of_node(0)会被调用,它将遍历设备树中的缓存节点,并用解析出的属性(大小、行大小、组数等)填充per_cpu(ci_cpu_cacheinfo, 0)这个全局变量。
源码及逐行注释
c
#ifdef CONFIG_OF
/**
* @brief 检查一个CPU设备节点是否有关联的缓存节点。
* @param[in] np 指向CPU设备节点的指针。
* @return 如果存在缓存相关属性或下一个缓存节点,则返回true;否则返回false。
*/
static bool of_check_cache_nodes(struct device_node *np);
/**
* @struct cache_type_info
* @brief 定义了用于从设备树查询特定类型缓存属性的属性名。
*/
struct cache_type_info {
const char *size_prop; //!< 缓存大小属性的名称。
const char *line_size_props[2]; //!< 缓存行大小属性的两个可能名称。
const char *nr_sets_prop; //!< 缓存组数属性的名称。
};
/**
* @var cache_type_info
* @brief 一个静态常量数组,存储了不同缓存类型(统一、指令、数据)对应的设备树属性名。
*/
static const struct cache_type_info cache_type_info[] = {
{ // 对应 CACHE_TYPE_UNIFIED (索引 0)
.size_prop = "cache-size",
.line_size_props = { "cache-line-size",
"cache-block-size", },
.nr_sets_prop = "cache-sets",
}, { // 对应 CACHE_TYPE_INST (索引 1)
.size_prop = "i-cache-size",
.line_size_props = { "i-cache-line-size",
"i-cache-block-size", },
.nr_sets_prop = "i-cache-sets",
}, { // 对应 CACHE_TYPE_DATA (索引 2)
.size_prop = "d-cache-size",
.line_size_props = { "d-cache-line-size",
"d-cache-block-size", },
.nr_sets_prop = "d-cache-sets",
},
};
/**
* @brief 根据缓存类型获取在 cache_type_info 数组中的索引。
* @param[in] type 缓存类型枚举值。
* @return 对应的索引。
*/
static inline int get_cacheinfo_idx(enum cache_type type)
{
if (type == CACHE_TYPE_UNIFIED) //!< 如果是统一缓存。
return 0; //!< 返回索引0。
return type; //!< 否则,枚举值(1或2)直接对应索引。
}
/**
* @brief 从设备树节点中读取缓存大小属性。
* @param[in,out] this_leaf 指向要填充的cacheinfo结构体的指针。
* @param[in] np 指向设备树中缓存节点的指针。
*/
static void cache_size(struct cacheinfo *this_leaf, struct device_node *np)
{
const char *propname; //!< 用于存储属性名的指针。
int ct_idx; //!< 存储 cache_type_info 数组的索引。
ct_idx = get_cacheinfo_idx(this_leaf->type); //!< 获取当前缓存类型的索引。
propname = cache_type_info[ct_idx].size_prop; //!< 获取对应的缓存大小属性名。
of_property_read_u32(np, propname, &this_leaf->size); //!< 从设备树节点读取u32类型的属性值并存入this_leaf->size。
}
/**
* @brief 从设备树节点中读取缓存行大小属性。
* @param[in,out] this_leaf 指向要填充的cacheinfo结构体的指针。
* @param[in] np 指向设备树中缓存节点的指针。
*/
static void cache_get_line_size(struct cacheinfo *this_leaf,
struct device_node *np)
{
int i, lim, ct_idx; //!< i是循环变量,lim是属性名数组大小,ct_idx是类型索引。
ct_idx = get_cacheinfo_idx(this_leaf->type); //!< 获取当前缓存类型的索引。
lim = ARRAY_SIZE(cache_type_info[ct_idx].line_size_props); //!< 获取行大小属性名数组的元素个数。
for (i = 0; i < lim; i++) { //!< 遍历所有可能的行大小属性名。
int ret; //!< 存储 of_property_read_u32 的返回值。
u32 line_size; //!< 存储读取到的行大小。
const char *propname; //!< 用于存储属性名的指针。
propname = cache_type_info[ct_idx].line_size_props[i]; //!< 获取当前要尝试的属性名。
ret = of_property_read_u32(np, propname, &line_size); //!< 尝试读取该属性。
if (!ret) { //!< 如果读取成功 (返回值为0)。
this_leaf->coherency_line_size = line_size; //!< 将读取到的值赋给结构体成员。
break; //!< 成功找到后即退出循环。
}
}
}
/**
* @brief 从设备树节点中读取缓存组数属性。
* @param[in,out] this_leaf 指向要填充的cacheinfo结构体的指针。
* @param[in] np 指向设备树中缓存节点的指针。
*/
static void cache_nr_sets(struct cacheinfo *this_leaf, struct device_node *np)
{
const char *propname; //!< 用于存储属性名的指针。
int ct_idx; //!< 存储 cache_type_info 数组的索引。
ct_idx = get_cacheinfo_idx(this_leaf->type); //!< 获取当前缓存类型的索引。
propname = cache_type_info[ct_idx].nr_sets_prop; //!< 获取对应的缓存组数属性名。
of_property_read_u32(np, propname, &this_leaf->number_of_sets); //!< 从设备树节点读取u32类型的属性值并存入this_leaf->number_of_sets。
}
/**
* @brief 根据已知的缓存大小、组数和行大小计算缓存的关联度。
* @param[in,out] this_leaf 指向要填充的cacheinfo结构体的指针。
*/
static void cache_associativity(struct cacheinfo *this_leaf)
{
unsigned int line_size = this_leaf->coherency_line_size; //!< 获取行大小。
unsigned int nr_sets = this_leaf->number_of_sets; //!< 获取组数。
unsigned int size = this_leaf->size; //!< 获取总大小。
/*
* 如果缓存是全关联的(组数为1),则无需计算。
* 同时检查其他属性是否有效(大于0)以避免除零错误。
*/
if (!(nr_sets == 1) && (nr_sets > 0 && size > 0 && line_size > 0))
this_leaf->ways_of_associativity = (size / nr_sets) / line_size; //!< 计算关联度并赋值。
}
/**
* @brief 检查设备树节点是否具有 "cache-unified" 属性。
* @param[in] this_leaf 指向cacheinfo结构体的指针 (当前未使用)。
* @param[in] np 指向设备树中缓存节点的指针。
* @return 如果 "cache-unified" 属性存在且为真,则返回true;否则返回false。
*/
static bool cache_node_is_unified(struct cacheinfo *this_leaf,
struct device_node *np)
{
return of_property_read_bool(np, "cache-unified"); //!< 读取布尔属性 "cache-unified"。
}
/**
* @brief 检查一个给定的缓存节点是否属于一个CPU的缓存层级链。
* @param[in] cpu 指向CPU设备节点的指针。
* @param[in] cache_node 指向要匹配的缓存节点的指针。
* @return 如果cache_node在cpu的缓存层级链中,则返回true;否则返回false。
*/
static bool match_cache_node(struct device_node *cpu,
const struct device_node *cache_node)
{
struct device_node *prev, *cache = of_find_next_cache_node(cpu); //!< 从CPU节点开始查找第一个缓存节点。
while (cache) { //!< 循环遍历缓存层级链。
if (cache == cache_node) { //!< 如果找到了匹配的节点。
of_node_put(cache); //!< 释放当前节点的引用计数。
return true; //!< 返回true。
}
prev = cache; //!< 保存当前节点。
cache = of_find_next_cache_node(cache); //!< 查找下一个缓存节点。
of_node_put(prev); //!< 释放上一个节点的引用计数。
}
return false; //!< 遍历完整个链都未找到,返回false。
}
#ifndef arch_compact_of_hwid
/**
* @brief 体系结构相关的宏,用于压缩OF硬件ID,如果未定义则为空操作。
*/
#define arch_compact_of_hwid(_x) (_x)
#endif
/**
* @brief 为一个缓存条目设置唯一的ID。
* @param[in,out] this_leaf 指向要设置ID的cacheinfo结构体的指针。
* @param[in] cache_node 指向设备树中该缓存节点的指针。
*/
static void cache_of_set_id(struct cacheinfo *this_leaf,
struct device_node *cache_node)
{
struct device_node *cpu; //!< 用于遍历CPU节点的指针。
u32 min_id = ~0; //!< 初始化最小ID为一个最大值。
for_each_of_cpu_node(cpu) { //!< 遍历系统中的所有CPU节点。
u64 id = of_get_cpu_hwid(cpu, 0); //!< 获取CPU的硬件ID。
id = arch_compact_of_hwid(id); //!< 对硬件ID进行可能的压缩。
if (FIELD_GET(GENMASK_ULL(63, 32), id)) { //!< 检查ID的高32位是否非零,若非零则此方案不支持。
of_node_put(cpu); //!< 释放CPU节点引用。
return; //!< 直接返回。
}
if (match_cache_node(cpu, cache_node)) //!< 检查当前CPU是否共享此缓存节点。
min_id = min(min_id, (u32)id); //!< 如果共享,则更新最小ID。
}
if (min_id != ~0) { //!< 如果找到了至少一个共享此缓存的CPU。
this_leaf->id = min_id; //!< 将最小的CPU硬件ID作为此缓存的ID。
this_leaf->attributes |= CACHE_ID; //!< 设置CACHE_ID属性标志。
}
}
/**
* @brief 为一个缓存条目设置其所有从设备树读取的属性。
* @param[in,out] this_leaf 指向要填充的cacheinfo结构体的指针。
* @param[in] np 指向设备树中缓存节点的指针。
*/
static void cache_of_set_props(struct cacheinfo *this_leaf,
struct device_node *np)
{
/*
* init_cache_level 必须正确设置缓存级别,
* 如果此时类型仍是 NOCACHE,且节点是统一缓存,则将其设置为统一缓存类型。
*/
if (this_leaf->type == CACHE_TYPE_NOCACHE &&
cache_node_is_unified(this_leaf, np))
this_leaf->type = CACHE_TYPE_UNIFIED;
cache_size(this_leaf, np); //!< 设置缓存大小。
cache_get_line_size(this_leaf, np); //!< 设置缓存行大小。
cache_nr_sets(this_leaf, np); //!< 设置缓存组数。
cache_associativity(this_leaf); //!< 计算并设置关联度。
cache_of_set_id(this_leaf, np); //!< 设置缓存ID。
}
/**
* @brief 为指定CPU设置并填充所有来自设备树的缓存信息。
* @param[in] cpu CPU的逻辑编号。
* @return 成功返回0,失败返回错误码。
*/
static int cache_setup_of_node(unsigned int cpu)
{
struct cacheinfo *this_leaf; //!< 指向当前处理的缓存条目。
unsigned int index = 0; //!< 缓存条目的索引。
struct device_node *np __free(device_node) = of_cpu_device_node_get(cpu); //!< 获取CPU的设备节点,__free会自动释放np。
if (!np) { //!< 如果找不到节点。
pr_err("未能找到 cpu%d 的设备节点\n", cpu);
return -ENOENT; //!< 返回"无此实体"错误。
}
if (!of_check_cache_nodes(np)) { //!< 检查是否存在任何缓存信息。
return -ENOENT; //!< 如果没有,返回错误。
}
while (index < cache_leaves(cpu)) { //!< 循环处理该CPU的所有缓存条目。
this_leaf = per_cpu_cacheinfo_idx(cpu, index); //!< 获取当前索引的缓存条目结构体。
if (this_leaf->level != 1) { //!< 如果处理的不是L1缓存。
struct device_node *prev __free(device_node) = np; //!< 保存当前节点指针,并设置自动释放。
np = of_find_next_cache_node(np); //!< 寻找下一个级别的缓存节点。
if (!np) //!< 如果找不到下一个节点。
break; //!< 退出循环。
}
cache_of_set_props(this_leaf, np); //!< 为当前缓存条目填充属性。
this_leaf->fw_token = np; //!< 将设备树节点指针存为固件令牌,用于共享判断。
index++; //!< 索引递增。
}
if (index != cache_leaves(cpu)) /* 如果没有成功填充所有预期的缓存条目 */
return -ENOENT; //!< 返回错误。
return 0; //!< 成功返回0。
}
/**
* @brief 检查一个CPU设备节点及其后续节点是否包含任何缓存定义属性。
* @param[in] np 指向CPU设备节点的指针。
* @return 如果找到任何缓存相关属性,则返回true;否则返回false。
*/
static bool of_check_cache_nodes(struct device_node *np)
{
if (of_property_present(np, "cache-size") || //!< 检查统一缓存大小属性。
of_property_present(np, "i-cache-size") || //!< 检查指令缓存大小属性。
of_property_present(np, "d-cache-size") || //!< 检查数据缓存大小属性。
of_property_present(np, "cache-unified")) //!< 检查统一缓存标志属性。
return true; //!< 如果存在任何一个,返回true。
struct device_node *next __free(device_node) = of_find_next_cache_node(np); //!< 查找下一级缓存节点。
if (next) { //!< 如果存在下一级缓存节点。
return true; //!< 返回true。
}
return false; //!< 否则返回false。
}
/**
* @brief 计算一个设备树节点所描述的缓存条目(leaves)数量。
* @param[in] np 指向设备树缓存节点的指针。
* @return 该节点描述的缓存条目数量。
*/
static int of_count_cache_leaves(struct device_node *np)
{
unsigned int leaves = 0; //!< 初始化条目计数器。
if (of_property_present(np, "cache-size")) //!< 如果存在统一缓存大小属性。
++leaves;
if (of_property_present(np, "i-cache-size")) //!< 如果存在指令缓存大小属性。
++leaves;
if (of_property_present(np, "d-cache-size")) //!< 如果存在数据缓存大小属性。
++leaves;
if (!leaves) { //!< 如果没有找到任何大小属性。
/* 'cache-size' 类的属性是必需的,但如果缺失,则回退到'cache-unified'属性。 */
if (of_property_read_bool(np, "cache-unified")) //!< 如果是统一缓存。
return 1; //!< 算作1个条目。
else
return 2; //!< 否则默认认为是分离的指令和数据缓存,算作2个条目。
}
return leaves; //!< 返回统计到的条目数。
}
/**
* @brief 初始化指定CPU的缓存级别数和缓存条目总数。
* @param[in] cpu CPU的逻辑编号。
* @return 成功返回0,失败返回错误码。
*/
int init_of_cache_level(unsigned int cpu)
{
struct cpu_cacheinfo *this_cpu_ci = get_cpu_cacheinfo(cpu); //!< 获取该CPU的缓存信息结构体。
struct device_node *np __free(device_node) = of_cpu_device_node_get(cpu); //!< 获取CPU的设备节点。
unsigned int levels = 0, leaves, level; //!< levels:级别数, leaves:条目数, level:临时变量。
if (!of_check_cache_nodes(np)) { //!< 检查是否存在缓存信息。
return -ENOENT; //!< 若无,则返回错误。
}
leaves = of_count_cache_leaves(np); //!< 计算第一级(通常是L1)的缓存条目数。
if (leaves > 0)
levels = 1; //!< 如果有条目,那么至少有1级缓存。
while (1) { //!< 循环遍历所有更高级别的缓存。
struct device_node *prev __free(device_node) = np; //!< 保存当前节点。
np = of_find_next_cache_node(np); //!< 查找下一个缓存节点。
if (!np) //!< 如果没有更多缓存节点。
break; //!< 退出循环。
if (!of_device_is_compatible(np, "cache")) //!< 检查节点兼容性是否为"cache"。
return -EINVAL; //!< 若否,返回无效参数错误。
if (of_property_read_u32(np, "cache-level", &level)) //!< 读取"cache-level"属性。
return -EINVAL; //!< 若读取失败,返回错误。
if (level <= levels) //!< 缓存级别必须是递增的。
return -EINVAL; //!< 若否,返回错误。
leaves += of_count_cache_leaves(np); //!< 累加该级别的缓存条目数。
levels = level; //!< 更新当前已知的最高缓存级别。
}
this_cpu_ci->num_levels = levels; //!< 将最终统计的级别数存入结构体。
this_cpu_ci->num_leaves = leaves; //!< 将最终统计的条目总数存入结构体。
return 0; //!< 成功返回0。
}
#else
static inline int cache_setup_of_node(unsigned int cpu) { return 0; }
int init_of_cache_level(unsigned int cpu) { return 0; }
#endif
缓存信息获取与填充总控 (fetch_cache_info, detect_cache_attributes)
核心功能
这组函数是 cacheinfo 子系统的核心调度器。fetch_cache_info 的主要任务是在系统启动早期或CPU热插拔的初始阶段,通过查询固件(ACPI或设备树)或架构特定的方法,来确定一个CPU拥有多少缓存级别(num_levels)和缓存条目(num_leaves),并为此分配核心数据结构 cacheinfo 数组的内存。detect_cache_attributes 则是更完整的初始化入口,它不仅确保了内存的分配,还进一步调用架构相关的函数(populate_cache_leaves)和通用的固件解析函数(cache_shared_cpu_map_setup -> cache_setup_of_node)来彻底填充所有缓存的详细属性,并建立CPU间的共享关系图。
实现原理分析
-
分阶段初始化 : 缓存信息的获取被设计成一个分阶段的过程,以适应不同的系统启动阶段和硬件发现机制。
fetch_cache_info通常在早期被调用,它只做最基础的两件事:确定缓存数量并分配内存。而detect_cache_attributes则在稍晚的阶段(当CPU完全上线时)被调用,进行完整和详细的属性填充。这种设计分离了内存分配和属性填充,增强了系统的灵活性。 -
信息来源的优先级 :
fetch_cache_info函数体现了信息来源的优先级策略。它首先尝试ACPI,如果ACPI被禁用或失败,它会调用init_of_cache_level尝试从设备树获取信息。如果两者都失败,它会调用early_cache_level作为一个备选方案,这通常是一个由具体架构(如ARM64, x86)提供的、可能基于寄存器读取的早期探测函数。这种多重后备(fallback)机制确保了在不同配置的系统上都能最大可能地获取到缓存信息。 -
幂等性与热插拔支持 :
init_level_allocate_ci函数的设计体现了对幂等性(重复调用无副作用)和CPU热插拔的支持。它首先检查per_cpu_cacheinfo(cpu)是否已经分配了内存。如果已分配,并且不是由"早期"方法分配的,它会直接返回,避免重复工作。这个检查对于CPU下线再上线(hotplug)的场景至关重要,保证了初始化只执行一次。如果发现之前由early_cache_level分配的信息可能不准确,它会调用init_cache_level给予架构代码一个修正的机会,并可能重新分配内存,这增强了系统的健壮性。
特定场景分析 (单核、无MMU的 STM32H750)
对于STM32H750平台,这些总控函数的执行路径是清晰且确定的。
-
启动流程 : 当Linux内核在STM32H750上启动并初始化CPU0时,
cacheinfo_cpu_online(后续分析) 回调函数会被触发。此函数会调用detect_cache_attributes(0)作为主入口。 -
函数调用链:
detect_cache_attributes(0)首先调用init_level_allocate_ci(0)。- 在
init_level_allocate_ci(0)内部,由于per_cpu_cacheinfo(0)此时为NULL,它会调用init_cache_level(0)。在通用的ARM平台上,这通常是一个空的弱符号函数,直接返回。 - 然后,最重要的,它会调用
init_of_cache_level(0)(上一节已分析),该函数将从设备树中读取并计算出num_levels和num_leaves。 - 计算出数量后,
allocate_cache_info(0)被调用,使用kcalloc分配cache_leaves(0)个cacheinfo结构体所需的内存。 - 回到
detect_cache_attributes(0),它接着调用populate_cache_leaves(0)。这同样是一个架构相关的弱符号函数,在STM32H750这种依赖设备树的平台上,它可能只做一些基本的类型和级别填充,具体的属性依赖后续的设备树解析。 - 最后,
detect_cache_attributes(0)调用cache_shared_cpu_map_setup(0)。此函数会发现LLC信息尚未有效,于是调用cache_setup_properties(0),由于of_have_populated_dt()为真,最终会调用cache_setup_of_node(0)(上一节已分析)。正是这一步,真正地从设备树中读取了所有缓存的大小、行大小、组数等详细信息,并填充到刚刚分配的内存中。
总而言之,在STM32H750上,detect_cache_attributes 启动了一个清晰的链式反应:确定数量 -> 分配内存 -> 填充详细属性,而所有信息的最终来源都是设备树。
源码及逐行注释
c
/**
* @brief 分配用于存储 cacheinfo 结构体数组的内存。
* @param[in] cpu CPU的逻辑编号。
* @return 成功返回0,内存不足则返回-ENOMEM。
*/
static inline int allocate_cache_info(int cpu)
{
// 使用 kcalloc 分配内存,能保证内存被清零。
per_cpu_cacheinfo(cpu) = kcalloc(cache_leaves(cpu), sizeof(struct cacheinfo), GFP_ATOMIC);
if (!per_cpu_cacheinfo(cpu)) { //!< 如果内存分配失败。
cache_leaves(cpu) = 0; //!< 将缓存条目数重置为0,以表示状态无效。
return -ENOMEM; //!< 返回内存不足错误。
}
return 0; //!< 成功返回0。
}
/**
* @brief 在系统早期获取缓存信息,确定缓存层级和条目数,并分配内存。
* @param[in] cpu CPU的逻辑编号。
* @return 成功返回0,失败返回错误码。
*/
int fetch_cache_info(unsigned int cpu)
{
struct cpu_cacheinfo *this_cpu_ci = get_cpu_cacheinfo(cpu); //!< 获取该CPU的缓存信息结构体。
unsigned int levels = 0, split_levels = 0; //!< 用于ACPI路径的变量。
int ret; //!< 存储函数返回值。
if (acpi_disabled) { //!< 如果ACPI被禁用 (在STM32H750上通常是这种情况)。
ret = init_of_cache_level(cpu); //!< 调用基于设备树的函数来确定缓存层级和条目数。
} else {
// 尝试通过ACPI获取缓存信息。
ret = acpi_get_cache_info(cpu, &levels, &split_levels);
if (!ret) { //!< 如果ACPI成功返回。
this_cpu_ci->num_levels = levels; //!< 设置缓存级别数。
/*
* 这里假设分离式缓存(指令/数据)之上不会有统一缓存,
* 并且指令/数据缓存总是成对出现。
*/
this_cpu_ci->num_leaves = levels + split_levels; //!< 计算总的缓存条目数。
}
}
if (ret || !cache_leaves(cpu)) { //!< 如果从固件(DT/ACPI)获取信息失败,或者没有发现任何缓存条目。
ret = early_cache_level(cpu); //!< 调用架构相关的早期探测函数作为备选方案。
if (ret) //!< 如果早期探测也失败。
return ret; //!< 返回错误。
if (!cache_leaves(cpu)) //!< 如果仍然没有任何缓存条目。
return -ENOENT; //!< 返回"无此实体"错误。
this_cpu_ci->early_ci_levels = true; //!< 标记此信息是通过早期方法获取的,可能不完整。
}
return allocate_cache_info(cpu); //!< 为缓存信息结构体数组分配内存。
}
/**
* @brief 初始化缓存级别并确保为cacheinfo结构分配了内存。
* 此函数支持CPU热插拔,具有幂等性。
* @param[in] cpu CPU的逻辑编号。
* @return 成功返回0,失败返回错误码。
*/
static inline int init_level_allocate_ci(unsigned int cpu)
{
unsigned int early_leaves = cache_leaves(cpu); //!< 保存调用前的缓存条目数,可能为0或早期探测值。
/*
* 由于此函数可能在CPU热插拔时被多次调用,
* 如果内存已经分配,并且不是由可能不准确的 "early" 方法分配的,
* 那么就直接返回,避免重复工作。
*/
if (per_cpu_cacheinfo(cpu) && !ci_cacheinfo(cpu)->early_ci_levels)
return 0;
// 调用架构相关的`init_cache_level`,它有机会覆盖或修正从固件获取的信息。
if (init_cache_level(cpu) || !cache_leaves(cpu))
return -ENOENT; //!< 如果执行失败或没有缓存条目,返回错误。
/*
* 标记 early_ci_levels 为 false,因为此时已通过更可靠的方法初始化,
* 防止下次调用时重复执行。
*/
ci_cacheinfo(cpu)->early_ci_levels = false;
/*
* 某些架构(如x86)不使用早期初始化。
* 如果当前计算的条目数比之前多,或者之前内存就未分配,则需要(重新)分配内存。
*/
if (cache_leaves(cpu) <= early_leaves && per_cpu_cacheinfo(cpu))
return 0; //!< 如果条目数未增加且内存已存在,则无需操作。
kfree(per_cpu_cacheinfo(cpu)); //!< 释放可能存在的旧的(不准确的)内存区域。
return allocate_cache_info(cpu); //!< 分配新的、大小正确的内存。
}
/**
* @brief 检测并填充指定CPU的所有缓存属性。这是初始化的主要入口点。
* @param[in] cpu CPU的逻辑编号。
* @return 成功返回0,失败返回错误码。
*/
int detect_cache_attributes(unsigned int cpu)
{
int ret; //!< 存储函数返回值。
ret = init_level_allocate_ci(cpu); //!< 首先,确保缓存级别已知并且内存已分配。
if (ret) //!< 如果失败。
return ret; //!< 直接返回错误。
/*
* 如果末级缓存(LLC)信息已经有效,说明缓存条目之前已被填充,
* 只需更新CPU共享映射即可。
*/
if (!last_level_cache_is_valid(cpu)) {
/*
* 调用架构相关的 `populate_cache_leaves` 函数。
* 此函数负责填充缓存条目的基本信息(如类型、级别等),
* 通常通过读取CPU内部寄存器完成。
*/
ret = populate_cache_leaves(cpu);
if (ret) //!< 如果填充失败。
goto free_ci; //!< 跳转到清理逻辑。
}
/*
* 对于使用设备树的系统,在这里设置 fw_token 和 shared_cpu_map。
* 这个函数会最终调用 cache_setup_of_node 来完成设备树的解析和属性填充。
*/
ret = cache_shared_cpu_map_setup(cpu);
if (ret) { //!< 如果设置失败。
pr_warn("无法检测CPU %d的缓存层级结构\n", cpu);
goto free_ci; //!< 跳转到清理逻辑。
}
return 0; //!< 所有操作成功,返回0。
free_ci:
free_cache_attributes(cpu); //!< 释放为该CPU分配的资源。
return ret; //!< 返回错误码。
}
缓存共享关系建立与维护 (cache_shared_cpu_map_setup, cache_shared_cpu_map_remove)
核心功能
这组函数的核心任务是构建和拆除每个缓存条目(leaf)的 shared_cpu_map。cache_shared_cpu_map_setup 负责为一个刚刚上线的CPU,遍历其所有的缓存层级,并与系统中其他所有已在线的CPU进行比较。如果发现两个不同CPU的某个缓存条目实际上是同一个物理缓存(通过 cache_leaves_are_shared 判断),它就会在各自的 shared_cpu_map 中记录下对方的存在。cache_shared_cpu_map_remove 则在CPU下线时执行相反的操作,即从所有曾经与它共享缓存的兄弟CPU的映射表中,将自己移除,以保持系统拓扑信息的一致性。
实现原理分析
-
双重迭代发现机制 :
cache_shared_cpu_map_setup的核心是一个双重迭代循环。外层循环 (index) 遍历当前CPU (cpu) 的所有缓存条目。内层循环 (for_each_online_cpu(i)) 遍历系统中所有其他在线的CPU。对于每一对CPU,它会再次遍历对方CPU的缓存条目 (sib_index)。 -
高效剪枝 : 在比较之前,代码使用
if (sib_leaf->level != this_leaf->level || sib_leaf->type != this_leaf->type)进行了一次关键的"剪枝"操作。它确保只有在缓存级别和类型都完全相同的情况下才进行后续的共享判断。这极大地减少了不必要的比较次数,例如,一个CPU的L1指令缓存永远不会和一个兄弟CPU的L2统一缓存进行共享比较。 -
对称更新 : 当通过
cache_leaves_are_shared确认两个缓存条目是共享的时,代码会执行对称的更新操作:cpumask_set_cpu(cpu, &sib_leaf->shared_cpu_map);和cpumask_set_cpu(i, &this_leaf->shared_cpu_map);。这意味着不仅在兄弟CPU (i) 的缓存条目中记录了当前CPU (cpu),也在当前CPU的条目中记录了兄弟CPU。这种双向记录确保了无论从哪个CPU的角度查询共享关系,都能得到一致的结果。 -
全局参数跟踪 : 在建立共享关系的同时,
cache_shared_cpu_map_setup还顺带完成了一个任务:if (this_leaf->coherency_line_size > coherency_max_size)。它会持续跟踪并更新全局变量coherency_max_size,以记录整个系统中所有缓存中最大的缓存行大小。这个值对于DMA操作和内存一致性维护非常重要。
特定场景分析 (单核、无MMU的 STM32H750)
在STM32H750这个单核平台上,这部分用于处理多核共享的复杂逻辑被极大地简化了,其行为变得非常简单和确定。
-
循环的退化 : 核心的
for_each_online_cpu(i)循环只会迭代一次,即i等于cpu(逻辑编号为0)。当进入循环体内部时,if (i == cpu ...)这个条件会立即成立并执行continue。因此,整个用于寻找兄弟核心(sibling CPU)的复杂循环实际上不会执行任何有效的比较操作。 -
最终的
shared_cpu_map: 由于找不到任何"兄弟"核心,唯一对shared_cpu_map起作用的代码行是循环开始前的cpumask_set_cpu(cpu, &this_leaf->shared_cpu_map);。其结果是,对于CPU0的每一个缓存层级(L1i, L1d等),其对应的shared_cpu_map中将只有一个比特位被设置,那就是代表CPU0自己的那一位。 -
cache_shared_cpu_map_remove的行为 : 当系统关机,CPU0下线时,cache_shared_cpu_map_remove会被调用。同样,for_each_cpu(sibling, &this_leaf->shared_cpu_map)循环也只会找到CPU0自己,if (sibling == cpu ...)条件会为真,因此不会执行任何清除兄弟映射表的操作。函数的主要作用退化为简单地将this_cpu_ci->cpu_map_populated标志位复位。
综上所述,在单核环境下,这部分代码的功能正确地退化为:为每个缓存层级创建一个只包含其自身的共享映射表,这完全符合单核系统的物理现实。
源码及逐行注释
c
/**
* @var coherency_max_size
* @brief 存储系统中所有缓存中最大的缓存行大小。
*/
unsigned int coherency_max_size;
/**
* @brief 决定从哪个来源(DT或ACPI)获取缓存属性。
* @param[in] cpu CPU的逻辑编号。
* @return 成功返回0,失败返回错误码。
*/
static int cache_setup_properties(unsigned int cpu)
{
int ret = 0; //!< 初始化返回值为0。
if (of_have_populated_dt()) //!< 检查设备树(DT)是否已被解析和填充。
ret = cache_setup_of_node(cpu); //!< 如果是,则从设备树设置缓存属性。
else if (!acpi_disabled) //!< 否则,如果ACPI没有被禁用。
ret = cache_setup_acpi(cpu); //!< 尝试从ACPI设置缓存属性。
// 如果从DT/ACPI获取信息失败,并且架构允许使用备选信息。
if (ret && use_arch_cache_info())
use_arch_info = true; //!< 设置标志,表示后续将依赖架构特定的信息。
return ret; //!< 返回执行结果。
}
/**
* @brief 为指定CPU的所有缓存层级建立共享CPU映射表。
* @param[in] cpu CPU的逻辑编号。
* @return 成功返回0,失败返回错误码。
*/
static int cache_shared_cpu_map_setup(unsigned int cpu)
{
struct cpu_cacheinfo *this_cpu_ci = get_cpu_cacheinfo(cpu); //!< 获取该CPU的缓存信息主结构。
struct cacheinfo *this_leaf, *sib_leaf; //!< 定义指向当前CPU缓存条目和兄弟CPU缓存条目的指针。
unsigned int index, sib_index; //!< 定义用于循环的索引变量。
int ret = 0; //!< 初始化返回值为0。
if (this_cpu_ci->cpu_map_populated) //!< 如果该CPU的共享映射表已经填充过。
return 0; //!< 直接返回成功,避免重复工作。
/*
* 如果末级缓存(LLC)信息无效,或者我们被强制使用架构信息,
* 那么需要先调用函数来填充缓存的详细属性。
*/
if (!last_level_cache_is_valid(cpu) && !use_arch_info) {
ret = cache_setup_properties(cpu); //!< 从固件(DT/ACPI)填充属性。
if (ret) //!< 如果填充失败。
return ret; //!< 返回错误。
}
for (index = 0; index < cache_leaves(cpu); index++) { //!< 遍历当前CPU的所有缓存条目。
unsigned int i; //!< 用于遍历所有在线CPU的变量。
this_leaf = per_cpu_cacheinfo_idx(cpu, index); //!< 获取当前缓存条目的指针。
cpumask_set_cpu(cpu, &this_leaf->shared_cpu_map); //!< 首先,在共享映射中将自己加入。
for_each_online_cpu(i) { //!< 遍历系统中所有在线的CPU。
if (i == cpu || !per_cpu_cacheinfo(i)) //!< 如果是自己,或者对方没有缓存信息,则跳过。
continue;
for (sib_index = 0; sib_index < cache_leaves(i); sib_index++) { //!< 遍历兄弟CPU的所有缓存条目。
sib_leaf = per_cpu_cacheinfo_idx(i, sib_index); //!< 获取兄弟CPU当前缓存条目的指针。
/*
* 只有当两个缓存条目的级别和类型都相同时,
* 比较它们的ID才有意义。否则跳过。
*/
if (sib_leaf->level != this_leaf->level ||
sib_leaf->type != this_leaf->type)
continue;
if (cache_leaves_are_shared(this_leaf, sib_leaf)) { //!< 判断这两个缓存条目是否共享同一个物理缓存。
cpumask_set_cpu(cpu, &sib_leaf->shared_cpu_map); //!< 在兄弟CPU的映射中加入当前CPU。
cpumask_set_cpu(i, &this_leaf->shared_cpu_map); //!< 在当前CPU的映射中加入兄弟CPU。
break; //!< 找到共享关系后,无需再比较该兄弟CPU的其他条目,跳出内层循环。
}
}
}
/* 记录系统中观察到的最大缓存行大小 */
if (this_leaf->coherency_line_size > coherency_max_size)
coherency_max_size = this_leaf->coherency_line_size;
}
/* 标记该CPU的共享CPU映射表已经成功填充 */
this_cpu_ci->cpu_map_populated = true;
return 0; //!< 返回成功。
}
/**
* @brief 当一个CPU下线时,从其兄弟CPU的共享映射表中移除该CPU。
* @param[in] cpu 即将下线的CPU的逻辑编号。
*/
static void cache_shared_cpu_map_remove(unsigned int cpu)
{
struct cpu_cacheinfo *this_cpu_ci = get_cpu_cacheinfo(cpu); //!< 获取该CPU的缓存信息主结构。
struct cacheinfo *this_leaf, *sib_leaf; //!< 定义缓存条目指针。
unsigned int sibling, index, sib_index; //!< 定义循环变量。
for (index = 0; index < cache_leaves(cpu); index++) { //!< 遍历即将下线的CPU的所有缓存条目。
this_leaf = per_cpu_cacheinfo_idx(cpu, index); //!< 获取当前缓存条目。
for_each_cpu(sibling, &this_leaf->shared_cpu_map) { //!< 遍历共享此缓存的所有CPU。
if (sibling == cpu || !per_cpu_cacheinfo(sibling)) //!< 如果是自己或对方无缓存信息,则跳过。
continue;
for (sib_index = 0; sib_index < cache_leaves(sibling); sib_index++) { //!< 遍历兄弟CPU的所有缓存条目。
sib_leaf = per_cpu_cacheinfo_idx(sibling, sib_index); //!< 获取兄弟CPU的当前缓存条目。
/* 同样,只比较级别和类型都相同的缓存 */
if (sib_leaf->level != this_leaf->level ||
sib_leaf->type != this_leaf->type)
continue;
if (cache_leaves_are_shared(this_leaf, sib_leaf)) { //!< 确认它们是共享的。
cpumask_clear_cpu(cpu, &sib_leaf->shared_cpu_map); //!< 从兄弟CPU的映射中移除当前CPU。
// 注意:这里没有必要清除 this_leaf->shared_cpu_map 中的 sibling,
// 因为与 cpu 相关的整个 cacheinfo 将被视为无效。
break; //!< 找到后即可跳出内层循环。
}
}
}
}
/* 标记该CPU的共享映射表不再有效 */
this_cpu_ci->cpu_map_populated = false;
}
/**
* @brief 释放与指定CPU缓存属性相关的资源,主要在CPU下线时调用。
* @param[in] cpu CPU的逻辑编号。
*/
static void free_cache_attributes(unsigned int cpu)
{
if (!per_cpu_cacheinfo(cpu)) //!< 如果该CPU的缓存信息不存在。
return; //!< 直接返回。
cache_shared_cpu_map_remove(cpu); //!< 调用函数,从共享拓扑中移除该CPU。
// 注意:这里没有 kfree(per_cpu_cacheinfo(cpu)),因为缓存信息内存一旦分配,
// 通常不会在CPU下线时释放,以便于快速再次上线。
}
Sysfs 接口创建与 CPU 热插拔管理 (cacheinfo_sysfs_init, cacheinfo_cpu_online, cacheinfo_cpu_pre_down)
核心功能
cacheinfo_sysfs_init 是整个 cacheinfo sysfs 功能的入口,它通过 cpuhp_setup_state 函数向内核的CPU热插拔框架注册了两个回调函数:cacheinfo_cpu_online 和 cacheinfo_cpu_pre_down。
当一个CPU成功上线后,cacheinfo_cpu_online 回调函数会被触发。它的职责是:
- 调用
detect_cache_attributes来全面探测并填充该CPU的缓存信息。 - 调用
cache_add_dev在sysfs中为该CPU创建相应的目录结构,如/sys/devices/system/cpu/cpuX/cache/,并在其下为每个缓存条目(leaf)创建子目录indexY。 - 在每个
indexY目录下,创建一系列属性文件(如level,size,type,shared_cpu_map等),将内核中cacheinfo结构体的数据暴露出来。
当一个CPU即将下线时,cacheinfo_cpu_pre_down 回调函数会被触发。它负责执行清理工作:
- 调用
cpu_cache_sysfs_exit来移除sysfs中与该CPU相关的所有缓存目录和文件。 - 调用
free_cache_attributes来更新缓存共享拓扑,将该CPU从中移除。
实现原理分析
-
设备模型与
sysfs的集成 : 代码利用了Linux的设备模型(device model)来创建sysfs条目。cpu_device_create是一个辅助函数,它可以在一个父设备(这里是CPU设备,如cpu0)下创建一个新的子设备。cache_add_dev首先创建了一个顶层的cache目录,然后在其下为每个cacheinfo条目(leaf)创建了一个以indexY命名的设备。 -
属性文件的动态创建 :
DEVICE_ATTR_RO宏是一个强大的工具,它能以一种简洁的方式定义一个只读的sysfs属性。例如,static DEVICE_ATTR_RO(level);这一行代码会自动生成一个名为dev_attr_level的device_attribute结构体,并隐式地关联一个名为level_show的函数。当用户cat这个属性文件时,内核会调用对应的_show函数。 -
_show函数的实现 : 像level_show这样的函数(通过show_one宏统一定义)的实现模式非常清晰:a. 通过
dev_get_drvdata(dev)从设备结构体中获取与之关联的cacheinfo结构体指针。这个关联是在cpu_device_create时建立的。b. 访问
cacheinfo结构体中对应的成员(如this_leaf->level)。c. 使用
sysfs_emit(一个sprintf的安全封装) 将数据格式化成字符串,并写入用户提供的缓冲区。 -
属性的按需可见性 :
cache_default_attrs_is_visible函数是一个非常精巧的设计。它使得sysfs属性文件的创建是动态和条件性的。例如,如果一个缓存在固件中没有提供id,那么this_leaf->attributes & CACHE_ID就为假,is_visible回调就会返回0,内核因此就不会在sysfs中创建id这个文件。这避免了sysfs中出现内容为空或无意义的文件,使得接口更加整洁和健壮。 -
与热插拔框架的集成 :
cpuhp_setup_state是将模块功能挂接到CPU生命周期事件中的标准方式。通过注册ONLINE和PRE_DOWN两个状态的回调,cacheinfo子系统实现了其功能的完全动态化,完美支持多核系统中的CPU热插拔操作。
特定场景分析 (单核、无MMU的 STM32H750)
在STM32H750单核平台上,热插拔的动态特性虽然不存在,但这个框架依然是CPU初始化流程的标准组成部分。
-
启动时的调用 : 内核启动过程中,在初始化CPU0时,热插拔框架会模拟一次"上线"事件。这会触发
cacheinfo_cpu_online(0)的调用。 -
sysfs的创建 :cacheinfo_cpu_online(0)会执行完整的缓存探测和sysfs创建流程。假设STM32H750的Cortex-M7内核有分离的32KB L1指令缓存和32KB L1数据缓存,那么在sysfs中将会创建如下结构:/sys/devices/system/cpu/cpu0/cache/ |-- index0/ | |-- level (内容: 1) | |-- type (内容: Instruction) | |-- size (内容: 32K) | |-- coherency_line_size (例如: 64) | |-- ways_of_associativity (例如: 4) | |-- number_of_sets (例如: 128) | `-- shared_cpu_list (内容: 0) `-- index1/ |-- level (内容: 1) |-- type (内容: Data) |-- size (内容: 32K) |-- coherency_line_size (例如: 64) |-- ways_of_associativity (例如: 4) |-- number_of_sets (例如: 128) `-- shared_cpu_list (内容: 0)shared_cpu_list(或_map) 的内容将只包含CPU 0,这准确地反映了单核系统的拓扑结构。 -
关机时的调用 : 在系统关闭(shutdown)流程中,内核会模拟CPU的"下线"事件,这将触发
cacheinfo_cpu_pre_down(0)。该函数会负责将上面创建的所有sysfs目录和文件清理干净。
因此,即使在没有真正热插拔的单核嵌入式系统上,这个基于热插拔框架的设计依然提供了一个标准、健壮且模块化的方式来管理硬件资源的发现和展现。
源码及逐行注释
c
/* ... (前面的 show_one 宏和 _show 函数定义) ... */
/**
* @brief 在sysfs中为CPU缓存添加设备和属性文件。
* @param[in] cpu CPU的逻辑编号。
* @return 成功返回0,失败返回错误码。
*/
static int cache_add_dev(unsigned int cpu)
{
unsigned int i;
int rc;
struct device *ci_dev, *parent;
struct cacheinfo *this_leaf;
const struct attribute_group **cache_groups;
rc = cpu_cache_sysfs_init(cpu); //!< 初始化该CPU的sysfs缓存顶层目录。
if (unlikely(rc < 0)) //!< 如果初始化失败。
return rc; //!< 返回错误。
parent = per_cpu_cache_dev(cpu); //!< 获取刚创建的父设备 (如 .../cpuX/cache)。
for (i = 0; i < cache_leaves(cpu); i++) { //!< 遍历该CPU的所有缓存条目。
this_leaf = per_cpu_cacheinfo_idx(cpu, i); //!< 获取当前缓存条目的信息。
if (this_leaf->disable_sysfs) //!< 如果此条目被标记为不在sysfs中显示。
continue; //!< 则跳过。
if (this_leaf->type == CACHE_TYPE_NOCACHE) //!< 如果缓存类型无效。
break; //!< 则停止创建后续条目。
cache_groups = cache_get_attribute_groups(this_leaf); //!< 获取此条目应有的属性组。
// 创建一个子设备 (如 .../cache/indexY),并将this_leaf指针作为其私有数据。
ci_dev = cpu_device_create(parent, this_leaf, cache_groups,
"index%1u", i);
if (IS_ERR(ci_dev)) { //!< 如果设备创建失败。
rc = PTR_ERR(ci_dev); //!< 获取错误码。
goto err; //!< 跳转到错误处理。
}
per_cache_index_dev(cpu, i) = ci_dev; //!< 保存创建的设备指针,以便后续移除。
}
cpumask_set_cpu(cpu, &cache_dev_map); //!< 在全局位图中标记该CPU的缓存sysfs已创建。
return 0; //!< 成功返回。
err:
cpu_cache_sysfs_exit(cpu); //!< 发生错误,清理已创建的sysfs条目。
return rc; //!< 返回错误码。
}
/* ... (update_per_cpu_data_slice_size 相关函数) ... */
/**
* @brief CPU上线时的热插拔回调函数。
* @param[in] cpu 上线的CPU的逻辑编号。
* @return 成功返回0,失败返回错误码。
*/
static int cacheinfo_cpu_online(unsigned int cpu)
{
int rc = detect_cache_attributes(cpu); //!< 首先,完整地探测并填充该CPU的缓存属性。
cpumask_t *cpu_map;
if (rc) //!< 如果探测失败。
return rc; //!< 返回错误。
rc = cache_add_dev(cpu); //!< 在sysfs中为该CPU创建缓存设备和属性。
if (rc) //!< 如果创建失败。
goto err; //!< 跳转到错误处理。
// 更新与此CPU共享LLC的所有CPU的per-cpu数据切片大小。
if (cpu_map_shared_cache(true, cpu, &cpu_map))
update_per_cpu_data_slice_size(true, cpu, cpu_map);
return 0; //!< 成功返回。
err:
free_cache_attributes(cpu); //!< 释放为该CPU分配的资源。
return rc; //!< 返回错误码。
}
/**
* @brief CPU下线前的热插拔回调函数。
* @param[in] cpu 即将下线的CPU的逻辑编号。
* @return 总是返回0。
*/
static int cacheinfo_cpu_pre_down(unsigned int cpu)
{
cpumask_t *cpu_map;
unsigned int nr_shared;
nr_shared = cpu_map_shared_cache(false, cpu, &cpu_map); //!< 获取与此CPU共享缓存的其他CPU信息。
// 测试并清除标志位,确保exit函数只被调用一次。
if (cpumask_test_and_clear_cpu(cpu, &cache_dev_map))
cpu_cache_sysfs_exit(cpu); //!< 从sysfs中移除该CPU的缓存相关目录和文件。
free_cache_attributes(cpu); //!< 从缓存共享拓扑中移除该CPU。
if (nr_shared > 1) //!< 如果之前有其他CPU与它共享缓存。
update_per_cpu_data_slice_size(false, cpu, cpu_map); //!< 更新那些兄弟CPU的数据切片大小。
return 0; //!< 返回成功。
}
/**
* @brief cacheinfo sysfs接口的初始化函数。
* @return 注册热插拔回调函数的结果。
*/
static int __init cacheinfo_sysfs_init(void)
{
// 向CPU热插拔框架注册上线和下线回调函数。
return cpuhp_setup_state(CPUHP_AP_BASE_CACHEINFO_ONLINE,
"base/cacheinfo:online",
cacheinfo_cpu_online, cacheinfo_cpu_pre_down);
}
// 将cacheinfo_sysfs_init注册为一个设备初始化函数,它将在内核启动过程的适当阶段被调用。
device_initcall(cacheinfo_sysfs_init);