Skip to content

Commit 248f3b7

Browse files
committed
numa: report real topology from getcpu and get_mempolicy
With NUMA topology now discovered (numa:: module), make the topology-query syscalls report it instead of a hardcoded single node: - sys_getcpu() now fills the node-out argument with numa::node_of_cpu() for the calling CPU rather than always 0. - get_mempolicy(): - MPOL_F_NODE returns the calling CPU's node. - MPOL_F_NODE | MPOL_F_ADDR returns the node backing the given address, resolved via a page-table walk (virt_to_phys_pt) and numa::node_of_phys(). - the allowed-nodes mask now sets a bit for every discovered node (not just node 0), and rejects a maxnode smaller than the node count with EINVAL. set_mempolicy() stays a no-op: the topology is known but the physical allocator is not yet node-aware, so a placement policy cannot be enforced. Its comment is updated to say so; enforcement will come with the node-aware allocator. Adds numa::node_of_phys() to map a physical address to its node. On a machine with no SRAT everything degrades to the previous single-node-0 behavior. Add tests/tst-numa-mempolicy.cc checking getcpu's node is in range and matches numa::node_of_cpu, get_mempolicy's MPOL_F_NODE and allowed-mask (bit count == node count, maxnode-too-small EINVAL), and the MPOL_F_ADDR path. Verified on OSv under KVM single-node and with a QEMU 2-node -numa config. Depends on the "numa: discover NUMA topology from ACPI SRAT/SLIT" change.
1 parent fd8ce35 commit 248f3b7

5 files changed

Lines changed: 138 additions & 12 deletions

File tree

core/numa.cc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ unsigned node_of_cpu(unsigned cpu_id)
4444
return it == s_cpu_to_node.end() ? 0 : it->second;
4545
}
4646

47+
int node_of_phys(uint64_t phys)
48+
{
49+
for (auto& r : s_mem_ranges) {
50+
if (phys >= r.base && phys < r.base + r.length) {
51+
return (int)r.node;
52+
}
53+
}
54+
return -1;
55+
}
56+
4757
unsigned distance(unsigned from, unsigned to)
4858
{
4959
if (from == to) {

include/osv/numa.hh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ bool available();
5050
// The node a CPU (by sched cpu id) belongs to, or 0 if unknown.
5151
unsigned node_of_cpu(unsigned cpu_id);
5252

53+
// The node that owns a physical address, or -1 if it falls in no known range.
54+
// Only meaningful when available() is true.
55+
int node_of_phys(uint64_t phys);
56+
5357
// The SLIT distance from node `from` to node `to`. Linux/ACPI convention:
5458
// 10 == local (same node), higher == farther. Returns 10 for the diagonal and
5559
// a default (10 local / 20 remote) when no SLIT is present.

linux.cc

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
#include <osv/debug.hh>
1212
#include <osv/sched.hh>
13+
#include <osv/numa.hh>
14+
#include <osv/mmu.hh>
1315
#include <osv/mutex.h>
1416
#include <osv/waitqueue.hh>
1517
#include <osv/stubbing.hh>
@@ -176,23 +178,44 @@ int futex(int *uaddr, int op, int val, const struct timespec *timeout,
176178
static long get_mempolicy(int *policy, unsigned long *nmask,
177179
unsigned long maxnode, void *addr, int flags)
178180
{
179-
// As OSv has no support for NUMA nodes, we do here the minimum possible,
180-
// which is basically to return the same policy (MPOL_DEFAULT) and list
181-
// of nodes (just node 0) no matter if the caller asked for the default
182-
// policy, the allowed policy, or the policy for a specific address.
181+
// Report the topology discovered from ACPI SRAT/SLIT (numa::). When NUMA is
182+
// not available (no SRAT, the common single-node VM) this degrades to the
183+
// old single-node-0 behavior. We still apply MPOL_DEFAULT everywhere: the
184+
// allocator is not yet node-aware, so we cannot honor a real policy, but we
185+
// can at least answer topology queries truthfully.
183186
if ((flags & MPOL_F_NODE)) {
184-
*policy = 0; // in this case, store a node id, not a policy
187+
// Store a node id in *policy, not a policy value.
188+
if (!policy) {
189+
errno = EINVAL;
190+
return -1;
191+
}
192+
if (flags & MPOL_F_ADDR) {
193+
// The node backing the given address, if we can resolve it.
194+
int node = -1;
195+
if (numa::available() && addr) {
196+
auto phys = mmu::virt_to_phys_pt(addr);
197+
node = numa::node_of_phys(phys);
198+
}
199+
*policy = node < 0 ? 0 : node;
200+
} else {
201+
// The node the calling CPU runs on.
202+
*policy = numa::node_of_cpu(sched::cpu::current()->id);
203+
}
185204
return 0;
186205
}
187206
if (policy) {
188207
*policy = MPOL_DEFAULT;
189208
}
190209
if (nmask) {
191-
if (maxnode < 1) {
210+
if (maxnode < numa::nr_nodes()) {
192211
errno = EINVAL;
193212
return -1;
194213
}
195-
nmask[0] |= 1;
214+
// Set a bit for every node that exists.
215+
for (unsigned n = 0; n < numa::nr_nodes(); n++) {
216+
nmask[n / (8 * sizeof(unsigned long))] |=
217+
1UL << (n % (8 * sizeof(unsigned long)));
218+
}
196219
}
197220
return 0;
198221
}
@@ -202,9 +225,11 @@ static long get_mempolicy(int *policy, unsigned long *nmask,
202225
static long set_mempolicy(int policy, unsigned long *nmask,
203226
unsigned long maxnode)
204227
{
205-
// OSv has very minimal support for NUMA - merely exposes
206-
// all cpus as a single node0 and cannot really apply any meaningful policy
207-
// Therefore we implement this as noop, ignore all arguments and return success
228+
// The topology is now discovered (numa::), but the physical allocator is
229+
// not yet node-aware, so we cannot actually enforce a placement policy.
230+
// Accept the call as a no-op (ignoring the requested policy) rather than
231+
// failing, so NUMA-aware programs run unmodified. Enforcement will come
232+
// when the allocator learns about nodes.
208233
return 0;
209234
}
210235
#endif
@@ -441,12 +466,13 @@ static long sys_getcwd(char *buf, unsigned long size)
441466
#define __NR_sys_getcpu __NR_getcpu
442467
static long sys_getcpu(unsigned int *cpu, unsigned int *node, void *tcache)
443468
{
469+
auto *c = sched::cpu::current();
444470
if (cpu) {
445-
*cpu = sched::cpu::current()->id;
471+
*cpu = c->id;
446472
}
447473

448474
if (node) {
449-
*node = 0;
475+
*node = numa::node_of_cpu(c->id);
450476
}
451477

452478
return 0;

modules/tests/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ tests := tst-pthread.so misc-ramdisk.so tst-vblk.so tst-bsd-evh.so \
123123
tst-prctl.so \
124124
tst-mmap-file-cow.so \
125125
tst-numa.so \
126+
tst-numa.so tst-numa-mempolicy.so \
126127
tst-elf-permissions.so misc-mutex.so misc-sockets.so tst-condvar.so \
127128
tst-queue-mpsc.so tst-af-local.so tst-pipe.so tst-yield.so \
128129
misc-ctxsw.so tst-read.so tst-symlink.so tst-openat.so \

tests/tst-numa-mempolicy.cc

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright (C) 2026 Waldemar Kozaczuk
3+
*
4+
* This work is open source software, licensed under the terms of the
5+
* BSD license as described in the LICENSE file in the top-level directory.
6+
*/
7+
8+
// Verifies that getcpu and get_mempolicy report the NUMA topology discovered by
9+
// the numa:: module (rather than a hardcoded single node). Boot with QEMU's
10+
// -numa options to exercise more than one node. Built and run as part of the
11+
// OSv test image.
12+
13+
#include <osv/numa.hh>
14+
15+
#include <sched.h>
16+
#include <sys/syscall.h>
17+
#include <unistd.h>
18+
#include <errno.h>
19+
#include <stdlib.h>
20+
21+
#include <cassert>
22+
#include <iostream>
23+
24+
#define MPOL_F_NODE (1 << 0)
25+
#define MPOL_F_ADDR (1 << 1)
26+
27+
static long get_mempolicy_(int *policy, unsigned long *nmask,
28+
unsigned long maxnode, void *addr, int flags)
29+
{
30+
return syscall(SYS_get_mempolicy, policy, nmask, maxnode, addr, flags);
31+
}
32+
33+
int main()
34+
{
35+
std::cerr << "Running numa-mempolicy tests\n";
36+
unsigned nodes = numa::nr_nodes();
37+
38+
// getcpu (via sched_getcpu, which passes a node-out pointer) must report a
39+
// node within range and consistent with numa::node_of_cpu.
40+
unsigned cpu = 0, node = 0;
41+
long r = syscall(SYS_getcpu, &cpu, &node, nullptr);
42+
assert(r == 0);
43+
assert(node < nodes);
44+
assert(node == numa::node_of_cpu(cpu));
45+
46+
// get_mempolicy with MPOL_F_NODE reports the current CPU's node.
47+
int cur_node = -1;
48+
assert(get_mempolicy_(&cur_node, nullptr, 0, nullptr, MPOL_F_NODE) == 0);
49+
assert(cur_node >= 0 && (unsigned)cur_node < nodes);
50+
51+
// The allowed-nodes mask must have exactly `nodes` bits set at minimum, and
52+
// maxnode too small must fail with EINVAL.
53+
unsigned long mask[16] = {};
54+
int policy = -1;
55+
assert(get_mempolicy_(&policy, mask, sizeof(mask) * 8, nullptr, 0) == 0);
56+
int bits = 0;
57+
for (unsigned i = 0; i < sizeof(mask) * 8; i++) {
58+
if (mask[i / (8 * sizeof(unsigned long))] &
59+
(1UL << (i % (8 * sizeof(unsigned long))))) {
60+
bits++;
61+
}
62+
}
63+
assert((unsigned)bits == nodes);
64+
65+
if (nodes > 1) {
66+
errno = 0;
67+
assert(get_mempolicy_(&policy, mask, 1, nullptr, 0) == -1 &&
68+
errno == EINVAL);
69+
}
70+
71+
// MPOL_F_NODE|MPOL_F_ADDR reports the node backing a given address. On a
72+
// real NUMA machine this must be a valid node; the value depends on
73+
// placement so we only check the range.
74+
void *buf = malloc(4096);
75+
assert(buf);
76+
*(volatile char *)buf = 1; // fault it in
77+
int addr_node = -1;
78+
assert(get_mempolicy_(&addr_node, nullptr, 0, buf, MPOL_F_NODE | MPOL_F_ADDR) == 0);
79+
assert(addr_node >= 0 && (unsigned)addr_node < nodes);
80+
free(buf);
81+
82+
std::cerr << " nodes=" << nodes << " current_node=" << cur_node << "\n";
83+
std::cerr << "numa-mempolicy tests PASSED\n";
84+
return 0;
85+
}

0 commit comments

Comments
 (0)