|
| 1 | +From 8386520a291966d3febe01bfb529d9d91d574d80 Mon Sep 17 00:00:00 2001 |
| 2 | +From: Steven Noonan <steven@uplinklabs.net> |
| 3 | +Date: Tue, 19 May 2026 18:35:49 -0700 |
| 4 | +Subject: [PATCH 1/8] xen/xenbus: expose host NUMA node of a mapped ring |
| 5 | + |
| 6 | +Xenbus backends in PVH dom0 today have no visibility into which host |
| 7 | +NUMA node the foreign frame backing a grant-mapped ring lives on. |
| 8 | +xen_alloc_unpopulated_pages() registers its pool with NUMA_NO_NODE, |
| 9 | +so page_to_nid() of any grant-mapped page reports NUMA_NO_NODE and |
| 10 | +backends have nothing to drive kthread or IRQ placement off of. The |
| 11 | +result is that every backend kthread for every queue piles onto one |
| 12 | +host node regardless of the guest's vNUMA layout. |
| 13 | + |
| 14 | +Add xenbus_ring_host_node(dev, vaddr) returning the Linux node id of |
| 15 | +the host node hosting the first ring page, or NUMA_NO_NODE when the |
| 16 | +information is unavailable. The value is resolved once at map time |
| 17 | +using the new XENMEM_get_mfn_pxms hypercall and cached on the |
| 18 | +xenbus_map_node, so the public helper is a cheap list lookup. |
| 19 | + |
| 20 | +Three NUMA identifier namespaces are involved in this code path: host |
| 21 | +PXM (firmware), Xen-internal nid (assigned in SRAT scan order), and |
| 22 | +dom0 Linux node id. The hypercall returns host PXM, matching what |
| 23 | +dom0's own SRAT already uses; pxm_to_node() converts to a Linux node |
| 24 | +id, which is what backends will feed to cpumask_of_node() and |
| 25 | +kthread_create_on_node(). Keeping the Xen-side ABI in PXM-space lets |
| 26 | +callers translate with one standard lookup instead of maintaining |
| 27 | +their own Xen-nid -> Linux-node table. |
| 28 | + |
| 29 | +Older or non-Edera hypervisors lack the new hypercall. The first |
| 30 | +call there returns -ENOSYS, which latches a global "unsupported" |
| 31 | +flag; every subsequent xenbus_ring_host_node() returns NUMA_NO_NODE |
| 32 | +without issuing further hypercalls. Backend patches that key off the |
| 33 | +helper see NUMA_NO_NODE and silently fall back to today's |
| 34 | +NUMA-oblivious behaviour. This makes the kernel-side change safe to |
| 35 | +ship without a lockstep hypervisor update. |
| 36 | + |
| 37 | +Gated by CONFIG_XEN_BACKEND_NUMA_AFFINITY (depends on XEN_BACKEND, |
| 38 | +NUMA, ACPI_NUMA; default y). Disabling the option compiles the |
| 39 | +query out and the public helper becomes a stub returning |
| 40 | +NUMA_NO_NODE. |
| 41 | + |
| 42 | +PV dom0 is not a NUMA-affinity target: the PV map path explicitly |
| 43 | +sets host_node = NUMA_NO_NODE and the helper handles the |
| 44 | +pv.area->addr lookup path symmetrically with hvm.addr, returning |
| 45 | +NUMA_NO_NODE in both cases when no match is found. |
| 46 | + |
| 47 | +The foreign MFN is sourced from dev_bus_addr in the gnttab map |
| 48 | +result. Xen sets that field unconditionally for host_map operations |
| 49 | +(see grant_table.c in the hypervisor), so it is reliable in PVH dom0 |
| 50 | +without IOMMU isolation. Sampling only the first ring page's MFN is |
| 51 | +sufficient for the common case where a ring is contiguous on one |
| 52 | +host node; multi-node huge-page grants are a future concern. |
| 53 | + |
| 54 | +Signed-off-by: Steven Noonan <steven@uplinklabs.net> |
| 55 | +--- |
| 56 | + drivers/xen/Kconfig | 15 ++++ |
| 57 | + drivers/xen/xenbus/xenbus_client.c | 111 +++++++++++++++++++++++++++++ |
| 58 | + include/xen/interface/memory.h | 26 +++++++ |
| 59 | + include/xen/xenbus.h | 13 ++++ |
| 60 | + 4 files changed, 165 insertions(+) |
| 61 | + |
| 62 | +diff --git a/drivers/xen/Kconfig b/drivers/xen/Kconfig |
| 63 | +index f9a35ed266ec..ec1752a66b3e 100644 |
| 64 | +--- a/drivers/xen/Kconfig |
| 65 | ++++ b/drivers/xen/Kconfig |
| 66 | +@@ -96,6 +96,21 @@ config XEN_BACKEND |
| 67 | + Support for backend device drivers that provide I/O services |
| 68 | + to other virtual machines. |
| 69 | + |
| 70 | ++config XEN_BACKEND_NUMA_AFFINITY |
| 71 | ++ bool "NUMA affinity for Xen backend drivers" |
| 72 | ++ depends on XEN_BACKEND && NUMA && ACPI_NUMA |
| 73 | ++ default y |
| 74 | ++ help |
| 75 | ++ Allow Xen backend drivers (netback, blkback, gntdev consumers) |
| 76 | ++ to discover the host NUMA node that hosts a grant-mapped ring |
| 77 | ++ page, and to place their service threads and IRQs on that node. |
| 78 | ++ |
| 79 | ++ Requires hypervisor support for the XENMEM_get_mfn_pxms |
| 80 | ++ hypercall. Without that support the feature is silently a |
| 81 | ++ no-op, equivalent to NUMA-oblivious behaviour. |
| 82 | ++ |
| 83 | ++ If unsure, say Y. |
| 84 | ++ |
| 85 | + config XENFS |
| 86 | + tristate "Xen filesystem" |
| 87 | + select XEN_PRIVCMD |
| 88 | +diff --git a/drivers/xen/xenbus/xenbus_client.c b/drivers/xen/xenbus/xenbus_client.c |
| 89 | +index 2dc874fb5506..8e0695ba39a3 100644 |
| 90 | +--- a/drivers/xen/xenbus/xenbus_client.c |
| 91 | ++++ b/drivers/xen/xenbus/xenbus_client.c |
| 92 | +@@ -31,15 +31,18 @@ |
| 93 | + */ |
| 94 | + |
| 95 | + #include <linux/mm.h> |
| 96 | ++#include <linux/numa.h> |
| 97 | + #include <linux/slab.h> |
| 98 | + #include <linux/types.h> |
| 99 | + #include <linux/spinlock.h> |
| 100 | + #include <linux/vmalloc.h> |
| 101 | + #include <linux/export.h> |
| 102 | ++#include <asm/xen/hypercall.h> |
| 103 | + #include <asm/xen/hypervisor.h> |
| 104 | + #include <xen/page.h> |
| 105 | + #include <xen/interface/xen.h> |
| 106 | + #include <xen/interface/event_channel.h> |
| 107 | ++#include <xen/interface/memory.h> |
| 108 | + #include <xen/balloon.h> |
| 109 | + #include <xen/events.h> |
| 110 | + #include <xen/grant_table.h> |
| 111 | +@@ -47,6 +50,10 @@ |
| 112 | + #include <xen/xen.h> |
| 113 | + #include <xen/features.h> |
| 114 | + |
| 115 | ++#ifdef CONFIG_XEN_BACKEND_NUMA_AFFINITY |
| 116 | ++#include <acpi/acpi_numa.h> |
| 117 | ++#endif |
| 118 | ++ |
| 119 | + #include "xenbus.h" |
| 120 | + |
| 121 | + #define XENBUS_PAGES(_grants) (DIV_ROUND_UP(_grants, XEN_PFN_PER_PAGE)) |
| 122 | +@@ -67,6 +74,7 @@ struct xenbus_map_node { |
| 123 | + }; |
| 124 | + grant_handle_t handles[XENBUS_MAX_RING_GRANTS]; |
| 125 | + unsigned int nr_handles; |
| 126 | ++ int host_node; /* Linux node id of foreign frame, or NUMA_NO_NODE */ |
| 127 | + }; |
| 128 | + |
| 129 | + struct map_ring_valloc { |
| 130 | +@@ -85,6 +93,72 @@ struct map_ring_valloc { |
| 131 | + static DEFINE_SPINLOCK(xenbus_valloc_lock); |
| 132 | + static LIST_HEAD(xenbus_valloc_pages); |
| 133 | + |
| 134 | ++#ifdef CONFIG_XEN_BACKEND_NUMA_AFFINITY |
| 135 | ++/* |
| 136 | ++ * Tri-state cache for XENMEM_get_mfn_pxms availability. -ENOSYS from |
| 137 | ++ * the first attempt latches "unsupported", short-circuiting future |
| 138 | ++ * calls. Any positive ACK (including the legitimate XEN_INVALID_NUMA_ID |
| 139 | ++ * answer for an MFN Xen does not know about) latches "supported". |
| 140 | ++ * |
| 141 | ++ * Lock-free: at most one transition each direction, and "unsupported" |
| 142 | ++ * is a stable terminal state once entered. A racing reader might |
| 143 | ++ * issue one redundant hypercall before observing the cached state, |
| 144 | ++ * which is harmless. |
| 145 | ++ */ |
| 146 | ++#define XEN_MFN_PXM_UNKNOWN 0 |
| 147 | ++#define XEN_MFN_PXM_SUPPORTED 1 |
| 148 | ++#define XEN_MFN_PXM_UNSUPPORTED 2 |
| 149 | ++ |
| 150 | ++static int xen_mfn_pxm_state = XEN_MFN_PXM_UNKNOWN; |
| 151 | ++ |
| 152 | ++/* |
| 153 | ++ * Resolve one foreign MFN to a Linux node id. Returns NUMA_NO_NODE |
| 154 | ++ * for any failure mode: hypercall unsupported, MFN unknown to Xen, |
| 155 | ++ * PXM not registered with the dom0 ACPI namespace. |
| 156 | ++ * |
| 157 | ++ * Three NUMA identifier namespaces are involved here. Xen returns |
| 158 | ++ * host PXM (firmware-supplied). pxm_to_node() translates to a Linux |
| 159 | ++ * dom0 node id. Callers then use the result against Linux helpers |
| 160 | ++ * like cpumask_of_node() and kthread_create_on_node(). |
| 161 | ++ */ |
| 162 | ++static int xenbus_query_mfn_node(unsigned long mfn) |
| 163 | ++{ |
| 164 | ++ struct xen_get_mfn_pxms req; |
| 165 | ++ xen_pfn_t mfn_arg = mfn; |
| 166 | ++ uint32_t pxm = XEN_INVALID_NUMA_ID; |
| 167 | ++ int rc; |
| 168 | ++ |
| 169 | ++ if (READ_ONCE(xen_mfn_pxm_state) == XEN_MFN_PXM_UNSUPPORTED) |
| 170 | ++ return NUMA_NO_NODE; |
| 171 | ++ |
| 172 | ++ memset(&req, 0, sizeof(req)); |
| 173 | ++ set_xen_guest_handle(req.mfns, &mfn_arg); |
| 174 | ++ set_xen_guest_handle(req.pxms, &pxm); |
| 175 | ++ req.nr_mfns = 1; |
| 176 | ++ |
| 177 | ++ rc = HYPERVISOR_memory_op(XENMEM_get_mfn_pxms, &req); |
| 178 | ++ if (rc < 0) { |
| 179 | ++ if (rc == -ENOSYS) { |
| 180 | ++ WRITE_ONCE(xen_mfn_pxm_state, XEN_MFN_PXM_UNSUPPORTED); |
| 181 | ++ pr_info("xenbus: hypervisor lacks XENMEM_get_mfn_pxms, backend NUMA affinity disabled\n"); |
| 182 | ++ } |
| 183 | ++ return NUMA_NO_NODE; |
| 184 | ++ } |
| 185 | ++ |
| 186 | ++ WRITE_ONCE(xen_mfn_pxm_state, XEN_MFN_PXM_SUPPORTED); |
| 187 | ++ |
| 188 | ++ if (pxm == XEN_INVALID_NUMA_ID) |
| 189 | ++ return NUMA_NO_NODE; |
| 190 | ++ |
| 191 | ++ return pxm_to_node(pxm); |
| 192 | ++} |
| 193 | ++#else |
| 194 | ++static int xenbus_query_mfn_node(unsigned long mfn) |
| 195 | ++{ |
| 196 | ++ return NUMA_NO_NODE; |
| 197 | ++} |
| 198 | ++#endif /* CONFIG_XEN_BACKEND_NUMA_AFFINITY */ |
| 199 | ++ |
| 200 | + struct xenbus_ring_ops { |
| 201 | + int (*map)(struct xenbus_device *dev, struct map_ring_valloc *info, |
| 202 | + grant_ref_t *gnt_refs, unsigned int nr_grefs, |
| 203 | +@@ -678,6 +752,8 @@ static int xenbus_map_ring_hvm(struct xenbus_device *dev, |
| 204 | + bool leaked = false; |
| 205 | + unsigned int nr_pages = XENBUS_PAGES(nr_grefs); |
| 206 | + |
| 207 | ++ node->host_node = NUMA_NO_NODE; |
| 208 | ++ |
| 209 | + err = xen_alloc_unpopulated_pages(nr_pages, node->hvm.pages); |
| 210 | + if (err) |
| 211 | + goto out_err; |
| 212 | +@@ -693,6 +769,17 @@ static int xenbus_map_ring_hvm(struct xenbus_device *dev, |
| 213 | + if (err) |
| 214 | + goto out_free_ballooned_pages; |
| 215 | + |
| 216 | ++ /* |
| 217 | ++ * Xen unconditionally fills dev_bus_addr with the foreign frame's |
| 218 | ++ * machine address on a successful host_map (see grant_table.c in |
| 219 | ++ * the hypervisor). Pick up the first ring page's MFN and resolve |
| 220 | ++ * it now while we still have the map info; the result is cached on |
| 221 | ++ * the xenbus_map_node so backends can look it up cheaply later. |
| 222 | ++ */ |
| 223 | ++ if (nr_grefs > 0) |
| 224 | ++ node->host_node = xenbus_query_mfn_node( |
| 225 | ++ PFN_DOWN(info->map[0].dev_bus_addr)); |
| 226 | ++ |
| 227 | + addr = vmap(node->hvm.pages, nr_pages, VM_MAP | VM_IOREMAP, |
| 228 | + PAGE_KERNEL); |
| 229 | + if (!addr) { |
| 230 | +@@ -743,6 +830,27 @@ int xenbus_unmap_ring_vfree(struct xenbus_device *dev, void *vaddr) |
| 231 | + } |
| 232 | + EXPORT_SYMBOL_GPL(xenbus_unmap_ring_vfree); |
| 233 | + |
| 234 | ++int xenbus_ring_host_node(struct xenbus_device *dev, void *vaddr) |
| 235 | ++{ |
| 236 | ++ struct xenbus_map_node *node; |
| 237 | ++ int node_id = NUMA_NO_NODE; |
| 238 | ++ |
| 239 | ++ spin_lock(&xenbus_valloc_lock); |
| 240 | ++ list_for_each_entry(node, &xenbus_valloc_pages, next) { |
| 241 | ++ void *addr = xen_pv_domain() ? node->pv.area->addr |
| 242 | ++ : node->hvm.addr; |
| 243 | ++ |
| 244 | ++ if (addr == vaddr) { |
| 245 | ++ node_id = node->host_node; |
| 246 | ++ break; |
| 247 | ++ } |
| 248 | ++ } |
| 249 | ++ spin_unlock(&xenbus_valloc_lock); |
| 250 | ++ |
| 251 | ++ return node_id; |
| 252 | ++} |
| 253 | ++EXPORT_SYMBOL_GPL(xenbus_ring_host_node); |
| 254 | ++ |
| 255 | + #ifdef CONFIG_XEN_PV |
| 256 | + static int map_ring_apply(pte_t *pte, unsigned long addr, void *data) |
| 257 | + { |
| 258 | +@@ -763,6 +871,9 @@ static int xenbus_map_ring_pv(struct xenbus_device *dev, |
| 259 | + bool leaked = false; |
| 260 | + int err = -ENOMEM; |
| 261 | + |
| 262 | ++ /* PV dom0 is not a NUMA-affinity target; leave the value unset. */ |
| 263 | ++ node->host_node = NUMA_NO_NODE; |
| 264 | ++ |
| 265 | + area = get_vm_area(XEN_PAGE_SIZE * nr_grefs, VM_IOREMAP); |
| 266 | + if (!area) |
| 267 | + return -ENOMEM; |
| 268 | +diff --git a/include/xen/interface/memory.h b/include/xen/interface/memory.h |
| 269 | +index 1a371a825c55..1998a12a9465 100644 |
| 270 | +--- a/include/xen/interface/memory.h |
| 271 | ++++ b/include/xen/interface/memory.h |
| 272 | +@@ -325,4 +325,30 @@ struct xen_mem_acquire_resource { |
| 273 | + }; |
| 274 | + DEFINE_GUEST_HANDLE_STRUCT(xen_mem_acquire_resource); |
| 275 | + |
| 276 | ++/* |
| 277 | ++ * XENMEM_get_mfn_pxms: resolve a batch of host MFNs to their firmware |
| 278 | ++ * proximity-domain identifiers (host PXM on x86 ACPI). |
| 279 | ++ * |
| 280 | ++ * Returned values are in the host PXM namespace (the same value space |
| 281 | ++ * dom0's own SRAT uses), not Xen's internal node id. Callers convert |
| 282 | ++ * to a Linux node id with pxm_to_node(). Slots Xen has no node info |
| 283 | ++ * for receive XEN_INVALID_NUMA_ID rather than failing the whole batch. |
| 284 | ++ * |
| 285 | ++ * Restricted to the hardware domain. On hypervisors that do not |
| 286 | ++ * provide this op (older or non-Edera Xen, or builds without |
| 287 | ++ * CONFIG_NUMA), the hypercall returns -ENOSYS; callers treat that as |
| 288 | ++ * "feature unavailable" and fall back to NUMA-oblivious behaviour. |
| 289 | ++ */ |
| 290 | ++#define XENMEM_get_mfn_pxms 40 |
| 291 | ++ |
| 292 | ++#define XEN_INVALID_NUMA_ID (~(uint32_t)0) |
| 293 | ++ |
| 294 | ++struct xen_get_mfn_pxms { |
| 295 | ++ GUEST_HANDLE(xen_pfn_t) mfns; |
| 296 | ++ GUEST_HANDLE(uint32_t) pxms; |
| 297 | ++ uint32_t nr_mfns; |
| 298 | ++ uint32_t flags; |
| 299 | ++}; |
| 300 | ++DEFINE_GUEST_HANDLE_STRUCT(xen_get_mfn_pxms); |
| 301 | ++ |
| 302 | + #endif /* __XEN_PUBLIC_MEMORY_H__ */ |
| 303 | +diff --git a/include/xen/xenbus.h b/include/xen/xenbus.h |
| 304 | +index 7dab04cf4a36..18b902bf79ef 100644 |
| 305 | +--- a/include/xen/xenbus.h |
| 306 | ++++ b/include/xen/xenbus.h |
| 307 | +@@ -225,6 +225,19 @@ int xenbus_map_ring_valloc(struct xenbus_device *dev, grant_ref_t *gnt_refs, |
| 308 | + |
| 309 | + int xenbus_unmap_ring_vfree(struct xenbus_device *dev, void *vaddr); |
| 310 | + |
| 311 | ++/* |
| 312 | ++ * Return the host NUMA node (Linux node id) of the foreign frame |
| 313 | ++ * backing the first page of a mapping previously established by |
| 314 | ++ * xenbus_map_ring_valloc(). Returns NUMA_NO_NODE if the hypervisor |
| 315 | ++ * cannot provide the information, the mapping is not found, or the |
| 316 | ++ * kernel was built without CONFIG_XEN_BACKEND_NUMA_AFFINITY. |
| 317 | ++ * |
| 318 | ++ * Intended for backends placing their service threads and IRQs on |
| 319 | ++ * the node hosting the ring. The value is resolved at map time and |
| 320 | ++ * cached on the mapping, so this call is a cheap lookup. |
| 321 | ++ */ |
| 322 | ++int xenbus_ring_host_node(struct xenbus_device *dev, void *vaddr); |
| 323 | ++ |
| 324 | + int xenbus_alloc_evtchn(struct xenbus_device *dev, evtchn_port_t *port); |
| 325 | + int xenbus_free_evtchn(struct xenbus_device *dev, evtchn_port_t port); |
| 326 | + |
| 327 | +-- |
| 328 | +2.54.0 |
| 329 | + |
0 commit comments