[PATCH v3 1/6] x86/resctrl: Parse ACPI ERDT table and map RMDD domains by L3 cache ID
From: Chen Yu
Date: Fri Jun 05 2026 - 22:42:58 EST
From: Anil S Keshavamurthy <anil.s.keshavamurthy@xxxxxxxxx>
ERDT(Enhanced RDT) introduces a new top-level ACPI structure
(the ERDT) that the kernel must parse before any enhanced
RDT feature can be used. The ERDT improves the existing RDT
by switching low-level register access from MSR-based to
MMIO-based, which is more efficient.
The ERDT structure may include several sub ACPI tables:
- Resource Management Domain Description Structure (RMDD)
- CPU Agent Collection Description Structure (CACD)
- Cache Monitoring Registers for CPU Agents Description Structure
(CMRC)
There is one ERDT table per platform.
Each RMDD substructure in ERDT represents one resource management
domain (RMD), also known as an L3 domain. Thus, the total number
of RMDDs equals the number of L3 domains on the platform.
Each RMDD contains information such as MMIO addresses. This address
is used to retrieve RDT metrics like L3 occupancy.
Add basic ERDT ACPI table and sub-table parsing, and store the
relevant tables for later processing.
Among these sub-tables, RMDD requires special handling. There is one
RMDD per domain, and the domain ID reuses the L3 cache ID. Many code
paths need to retrieve an RMDD efficiently by domain ID (L3 cache ID).
Because L3 cache IDs are derived from x2APIC IDs and are not
contiguous, using a plain array indexed by domain ID would waste
memory. As a trade-off, an xarray is used to store these tables, with
the L3 cache ID as the key.
Suggested-by: Tony Luck <tony.luck@xxxxxxxxx>
Co-developed-by: Chen Yu <yu.c.chen@xxxxxxxxx>
Signed-off-by: Chen Yu <yu.c.chen@xxxxxxxxx>
Signed-off-by: Anil S Keshavamurthy <anil.s.keshavamurthy@xxxxxxxxx>
---
v2->v3:
Wrap __resctrl_arch_late_init() to avoid the goto logic. (Thomas Gleixner)
Make the variables in struct erdt_domain_info tabular format (Thomas Gleixner)
Remove tail comments (Thomas Gleixner)
Make the name of erdt_enabled() and variable in it consistent and
comprehensible. (Thomas Gleixner)
Use topo_lookup_cpuid() to search the CPU id according to the x2apic id
(Thomas Gleixner)
Fix kernel doc comment format (Thomas Gleixner)
Use brackets for multiple lines "if" case. (Thomas Gleixner)
Let the parameter for cacd_init() to fully utilize 100 characters.
(Thomas Gleixner)
Variables are reordered in reverse fir-tree.(Thomas Gleixner)
Added a named constant and use it in the rmdd->flags check.
(Thomas Gleixner)
Introduce helper functions to make the code readable when iterating
the RMDD tables. (Thomas Gleixner)
v1->v2:
Add #include <linux/cleanup.h> to follow the "include-what-you-use" best.
(Tony Luck)
Remove the variable of cacd in struct erdt_domain_info as it will
never be used after initialization.(sashiko)
Invoke erdt_exit() to avoid resource leak if rdt_alloc_capable and
rdt_mon_capable are both false.(sashiko)
Refine comments.(sashiko)
---
arch/x86/Kconfig | 4 +-
arch/x86/include/asm/apic.h | 1 +
arch/x86/include/asm/resctrl.h | 2 +
arch/x86/kernel/cpu/resctrl/Makefile | 1 +
arch/x86/kernel/cpu/resctrl/core.c | 14 +-
arch/x86/kernel/cpu/resctrl/erdt.c | 304 +++++++++++++++++++++++++
arch/x86/kernel/cpu/resctrl/internal.h | 3 +
arch/x86/kernel/cpu/topology.c | 2 +-
8 files changed, 327 insertions(+), 4 deletions(-)
create mode 100644 arch/x86/kernel/cpu/resctrl/erdt.c
diff --git a/arch/x86/Kconfig b/arch/x86/Kconfig
index f3f7cb01d69d..97d210bd9bb5 100644
--- a/arch/x86/Kconfig
+++ b/arch/x86/Kconfig
@@ -515,7 +515,7 @@ config X86_MPPARSE
config X86_CPU_RESCTRL
bool "x86 CPU resource control support"
- depends on X86 && (CPU_SUP_INTEL || CPU_SUP_AMD)
+ depends on X86_64 && (CPU_SUP_INTEL || CPU_SUP_AMD)
depends on MISC_FILESYSTEMS
select ARCH_HAS_CPU_RESCTRL
select RESCTRL_FS
@@ -538,7 +538,7 @@ config X86_CPU_RESCTRL
config X86_CPU_RESCTRL_INTEL_AET
bool "Intel Application Energy Telemetry"
- depends on X86_64 && X86_CPU_RESCTRL && CPU_SUP_INTEL && INTEL_PMT_TELEMETRY=y && INTEL_TPMI=y
+ depends on X86_CPU_RESCTRL && CPU_SUP_INTEL && INTEL_PMT_TELEMETRY=y && INTEL_TPMI=y
help
Enable per-RMID telemetry events in resctrl.
diff --git a/arch/x86/include/asm/apic.h b/arch/x86/include/asm/apic.h
index 9cd493d467d4..bb84651b14bd 100644
--- a/arch/x86/include/asm/apic.h
+++ b/arch/x86/include/asm/apic.h
@@ -54,6 +54,7 @@ static inline void x86_32_probe_apic(void) { }
#endif
extern u32 cpuid_to_apicid[];
+int topo_lookup_cpuid(u32 apic_id);
#define CPU_ACPIID_INVALID U32_MAX
diff --git a/arch/x86/include/asm/resctrl.h b/arch/x86/include/asm/resctrl.h
index 575f8408a9e7..97c2f6bc7a5f 100644
--- a/arch/x86/include/asm/resctrl.h
+++ b/arch/x86/include/asm/resctrl.h
@@ -40,6 +40,8 @@ struct resctrl_pqr_state {
u32 default_closid;
};
+bool erdt_enabled(void);
+
DECLARE_PER_CPU(struct resctrl_pqr_state, pqr_state);
extern bool rdt_alloc_capable;
diff --git a/arch/x86/kernel/cpu/resctrl/Makefile b/arch/x86/kernel/cpu/resctrl/Makefile
index 273ddfa30836..2216ee084832 100644
--- a/arch/x86/kernel/cpu/resctrl/Makefile
+++ b/arch/x86/kernel/cpu/resctrl/Makefile
@@ -2,6 +2,7 @@
obj-$(CONFIG_X86_CPU_RESCTRL) += core.o rdtgroup.o monitor.o
obj-$(CONFIG_X86_CPU_RESCTRL) += ctrlmondata.o
obj-$(CONFIG_X86_CPU_RESCTRL_INTEL_AET) += intel_aet.o
+obj-$(CONFIG_X86_CPU_RESCTRL) += erdt.o
obj-$(CONFIG_RESCTRL_FS_PSEUDO_LOCK) += pseudo_lock.o
# To allow define_trace.h's recursive include:
diff --git a/arch/x86/kernel/cpu/resctrl/core.c b/arch/x86/kernel/cpu/resctrl/core.c
index 7667cf7c4e94..90730f0851fa 100644
--- a/arch/x86/kernel/cpu/resctrl/core.c
+++ b/arch/x86/kernel/cpu/resctrl/core.c
@@ -1012,6 +1012,7 @@ static __init void check_quirks(void)
static __init bool get_rdt_resources(void)
{
+ erdt_init();
rdt_alloc_capable = get_rdt_alloc_resources();
rdt_mon_capable = get_rdt_mon_resources();
@@ -1113,7 +1114,7 @@ void resctrl_cpu_detect(struct cpuinfo_x86 *c)
}
}
-static int __init resctrl_arch_late_init(void)
+static int __init __resctrl_arch_late_init(void)
{
struct rdt_resource *r;
int state, ret, i;
@@ -1156,6 +1157,15 @@ static int __init resctrl_arch_late_init(void)
return 0;
}
+static int __init resctrl_arch_late_init(void)
+{
+ int ret = __resctrl_arch_late_init();
+
+ if (ret)
+ erdt_exit();
+ return ret;
+}
+
late_initcall(resctrl_arch_late_init);
static void __exit resctrl_arch_exit(void)
@@ -1165,6 +1175,8 @@ static void __exit resctrl_arch_exit(void)
cpuhp_remove_state(rdt_online);
resctrl_exit();
+
+ erdt_exit();
}
__exitcall(resctrl_arch_exit);
diff --git a/arch/x86/kernel/cpu/resctrl/erdt.c b/arch/x86/kernel/cpu/resctrl/erdt.c
new file mode 100644
index 000000000000..51597a6e0058
--- /dev/null
+++ b/arch/x86/kernel/cpu/resctrl/erdt.c
@@ -0,0 +1,304 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Enhanced Resource Director Technology(ERDT)
+ *
+ * Copyright (C) 2026 Intel Corporation
+ *
+ */
+
+#define pr_fmt(fmt) "resctrl: " fmt
+
+#include <linux/cleanup.h>
+#include <linux/cpu.h>
+#include <linux/err.h>
+#include <linux/xarray.h>
+#include <linux/resctrl.h>
+#include <linux/acpi.h>
+#include <asm/apic.h>
+#include <asm/cpu_device_id.h>
+#include "internal.h"
+
+enum erdt_mmio_type {
+ ERDT_MMIO_RMDD_CREG,
+ ERDT_MMIO_CMRC_BASE,
+ ERDT_MMIO_MAX
+};
+
+struct erdt_domain_info {
+ void __iomem *base[ERDT_MMIO_MAX];
+ struct acpi_erdt_cmrc *cmrc;
+};
+
+static bool erdt_enabled_flag;
+
+static DEFINE_XARRAY(erdt_domain_xa);
+
+#define ERDT_VALID_VERSION 1
+#define RMDD_FLAG_CPU_DOMAIN BIT(0)
+
+static u32 valid_subtbl_mask;
+
+bool erdt_enabled(void)
+{
+ return erdt_enabled_flag;
+}
+
+/**
+ * get_l3_cache_id_from_cacd - Resolve L3 cache ID from CACD subtable
+ * @cacd: Pointer to the ACPI ERDT CACD structure
+ *
+ * Parses the X2APIC ID list in the given CACD subtable to
+ * identify an online logical CPU and uses it to query the associated
+ * L3 cache ID. The first valid CPU found is used for this lookup.
+ *
+ * The L3 cache ID is used as a unique domain key for ERDT domain
+ * registration and lookup.
+ *
+ * Return: L3 cache ID for the first matching CPU, or -1 on failure.
+ */
+static __init int get_l3_cache_id_from_cacd(struct acpi_erdt_cacd *cacd)
+{
+ int num_ids, cpu, online_cpu = -1, cache_id = -1, tmp;
+ struct cacheinfo *ci;
+
+ if (cacd->header.length < sizeof(*cacd) + sizeof(cacd->X2APICIDS[0])) {
+ pr_warn(FW_BUG "Invalid x2apicid CACD table\n");
+ return -1;
+ }
+
+ num_ids = (cacd->header.length - sizeof(*cacd)) / sizeof(cacd->X2APICIDS[0]);
+
+ guard(cpus_read_lock)();
+
+ for (int i = 0; i < num_ids; i++) {
+ cpu = topo_lookup_cpuid(cacd->X2APICIDS[i]);
+ if (cpu < 0) {
+ pr_warn(FW_BUG "Unknown x2apicid 0x%x\n", cacd->X2APICIDS[i]);
+
+ return -1;
+ }
+
+ if (!cpu_online(cpu))
+ continue;
+
+ tmp = get_cpu_cacheinfo_id(cpu, RESCTRL_L3_CACHE);
+ if (tmp == -1) {
+ pr_warn(FW_BUG "Can not find L3 cache id for CPU%d\n", cpu);
+ return -1;
+ }
+
+ if (cache_id == -1)
+ cache_id = tmp;
+
+ if (tmp != cache_id) {
+ pr_warn(FW_BUG "CACD references multiple L3 cache instances\n");
+ return -1;
+ }
+ online_cpu = cpu;
+ }
+
+ if (online_cpu == -1)
+ return -1;
+
+ /*
+ * Check if CACD lists all CPUs in the LLC domain.
+ */
+ ci = get_cpu_cacheinfo_level(online_cpu, RESCTRL_L3_CACHE);
+ if (!ci || num_ids != cpumask_weight(&ci->shared_cpu_map)) {
+ pr_warn(FW_BUG "CACD does not list all the CPUs in L3 domain\n");
+ return -1;
+ }
+
+ return cache_id;
+}
+
+static void __iomem *erdt_ioremap_checked(phys_addr_t base, u32 size, const char *desc)
+{
+ void __iomem *addr = ioremap(base, size << 12);
+
+ if (!addr) {
+ pr_err("ERDT: Failed to map %s at phys addr %#llx (size: %u pages)\n",
+ desc, (unsigned long long)base, size);
+ }
+ return addr;
+}
+
+static void erdt_iounmap_domain(struct erdt_domain_info *domain)
+{
+ for (int i = 0; i < ERDT_MMIO_MAX; i++) {
+ if (domain->base[i]) {
+ iounmap(domain->base[i]);
+ domain->base[i] = NULL;
+ }
+ }
+}
+
+static void cleanup_one_domain(struct erdt_domain_info *d)
+{
+ erdt_iounmap_domain(d);
+ kfree(d);
+}
+
+static __init bool cacd_init(struct erdt_domain_info *d, struct acpi_subtbl_hdr_16 *subtbl,
+ int *l3_cache_id)
+{
+ *l3_cache_id = get_l3_cache_id_from_cacd((struct acpi_erdt_cacd *)subtbl);
+
+ return *l3_cache_id != -1;
+}
+
+static inline struct acpi_subtbl_hdr_16 *rmdd_subtbl(struct acpi_erdt_rmdd *rmdd)
+{
+ return (void *)rmdd + sizeof(*rmdd);
+}
+
+static inline struct acpi_subtbl_hdr_16 *next_subtbl(struct acpi_subtbl_hdr_16 *subtbl)
+{
+ return (void *)subtbl + subtbl->length;
+}
+
+static inline bool subtbl_valid(struct acpi_erdt_rmdd *rmdd, struct acpi_subtbl_hdr_16 *subtbl)
+{
+ void *rmdd_end = (void *)rmdd + rmdd->header.length;
+
+ if (subtbl->length < sizeof(*subtbl))
+ return false;
+
+ if ((void *)subtbl + sizeof(*subtbl) > rmdd_end)
+ return false;
+
+ if ((void *)subtbl + subtbl->length > rmdd_end)
+ return false;
+
+ return true;
+}
+
+static __init bool parse_rmdd_entry(struct acpi_subtbl_hdr_16 *rmdd_hdr)
+{
+ struct erdt_domain_info *domain_info;
+ struct acpi_subtbl_hdr_16 *subtbl;
+ struct acpi_erdt_rmdd *rmdd;
+ int l3_cache_id = -1;
+ u32 subtbl_mask = 0;
+
+ if (rmdd_hdr->length < sizeof(*rmdd)) {
+ pr_info(FW_BUG "Invalid RMDD length %u\n", rmdd_hdr->length);
+ return false;
+ }
+
+ rmdd = (struct acpi_erdt_rmdd *)rmdd_hdr;
+
+ /* Quietly ignore non-CPU-based L3 domains */
+ if (!(rmdd->flags & RMDD_FLAG_CPU_DOMAIN))
+ return true;
+
+ domain_info = kzalloc(sizeof(*domain_info), GFP_KERNEL);
+ if (!domain_info)
+ return false;
+
+ domain_info->base[ERDT_MMIO_RMDD_CREG] =
+ erdt_ioremap_checked(rmdd->creg_base, rmdd->creg_size, "RMDD ctrl base");
+ if (!domain_info->base[ERDT_MMIO_RMDD_CREG])
+ goto cleanup;
+
+ for (subtbl = rmdd_subtbl(rmdd); subtbl_valid(rmdd, subtbl);
+ subtbl = next_subtbl(subtbl)) {
+ switch (subtbl->type) {
+ case ACPI_ERDT_TYPE_CACD:
+ if (cacd_init(domain_info, subtbl, &l3_cache_id))
+ subtbl_mask |= BIT(ACPI_ERDT_TYPE_CACD);
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (l3_cache_id == -1) {
+ pr_info("ERDT: Failed to resolve L3 cache ID for RMDD domain %d\n",
+ rmdd->domain_id);
+
+ goto cleanup;
+ }
+
+ /* Require all RMDDs to support same set of sub-tables */
+ if (!valid_subtbl_mask) {
+ valid_subtbl_mask = subtbl_mask;
+ } else if (subtbl_mask != valid_subtbl_mask) {
+ pr_info(FW_BUG "Mismatch domain\n");
+ goto cleanup;
+ }
+
+ if (xa_insert(&erdt_domain_xa, l3_cache_id, domain_info, GFP_KERNEL)) {
+ pr_info("ERDT: Failed to store domain info for RMDD domain %d\n",
+ rmdd->domain_id);
+ goto cleanup;
+ }
+
+ return true;
+
+cleanup:
+ cleanup_one_domain(domain_info);
+ return false;
+}
+
+static void erdt_cleanup(void)
+{
+ struct erdt_domain_info *d;
+ unsigned long index;
+
+ xa_for_each(&erdt_domain_xa, index, d)
+ cleanup_one_domain(d);
+ xa_destroy(&erdt_domain_xa);
+}
+
+static __init int enumerate_erdt_table(struct acpi_table_header *table_hdr)
+{
+ struct acpi_table_erdt *erdt = (struct acpi_table_erdt *)table_hdr;
+ struct acpi_subtbl_hdr_16 *subtbl;
+ void *table_end;
+
+ if (erdt->header.revision != ERDT_VALID_VERSION) {
+ pr_info("Unknown ERDT table revision %d\n", erdt->header.revision);
+ return -EINVAL;
+ }
+
+ if (erdt->header.length < sizeof(*erdt)) {
+ pr_info(FW_BUG "ERDT: Invalid table length %u\n", erdt->header.length);
+ return -EINVAL;
+ }
+
+ subtbl = (void *)erdt + sizeof(struct acpi_table_erdt);
+ table_end = (void *)erdt + erdt->header.length;
+
+ while ((void *)subtbl + sizeof(*subtbl) <= table_end) {
+ if (subtbl->length < sizeof(*subtbl) ||
+ (void *)subtbl + subtbl->length > table_end) {
+ pr_info("ERDT: Invalid subtable length\n");
+ goto cleanup;
+ }
+
+ if (subtbl->type == ACPI_ERDT_TYPE_RMDD)
+ if (!parse_rmdd_entry(subtbl))
+ goto cleanup;
+
+ subtbl = (void *)subtbl + subtbl->length;
+ }
+
+ erdt_enabled_flag = true;
+
+ return 0;
+
+cleanup:
+ erdt_cleanup();
+ return -EINVAL;
+}
+
+int __init erdt_init(void)
+{
+ return acpi_table_parse(ACPI_SIG_ERDT, enumerate_erdt_table);
+}
+
+void erdt_exit(void)
+{
+ erdt_cleanup();
+}
diff --git a/arch/x86/kernel/cpu/resctrl/internal.h b/arch/x86/kernel/cpu/resctrl/internal.h
index e3cfa0c10e92..9c59bd5e028e 100644
--- a/arch/x86/kernel/cpu/resctrl/internal.h
+++ b/arch/x86/kernel/cpu/resctrl/internal.h
@@ -253,4 +253,7 @@ static inline void intel_aet_mon_domain_setup(int cpu, int id, struct rdt_resour
static inline bool intel_handle_aet_option(bool force_off, char *tok) { return false; }
#endif
+int erdt_init(void);
+void erdt_exit(void);
+
#endif /* _ASM_X86_RESCTRL_INTERNAL_H */
diff --git a/arch/x86/kernel/cpu/topology.c b/arch/x86/kernel/cpu/topology.c
index 4913b64ec592..bcee70fb9277 100644
--- a/arch/x86/kernel/cpu/topology.c
+++ b/arch/x86/kernel/cpu/topology.c
@@ -92,7 +92,7 @@ static inline u32 topo_apicid(u32 apicid, enum x86_topology_domains dom)
return apicid & (UINT_MAX << x86_topo_system.dom_shifts[dom - 1]);
}
-static int topo_lookup_cpuid(u32 apic_id)
+int topo_lookup_cpuid(u32 apic_id)
{
int i;
--
2.25.1