Skip to content

Commit 5b04ad5

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 c1f3340 commit 5b04ad5

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>
@@ -177,23 +179,44 @@ int futex(int *uaddr, int op, int val, const struct timespec *timeout,
177179
static long get_mempolicy(int *policy, unsigned long *nmask,
178180
unsigned long maxnode, void *addr, int flags)
179181
{
180-
// As OSv has no support for NUMA nodes, we do here the minimum possible,
181-
// which is basically to return the same policy (MPOL_DEFAULT) and list
182-
// of nodes (just node 0) no matter if the caller asked for the default
183-
// policy, the allowed policy, or the policy for a specific address.
182+
// Report the topology discovered from ACPI SRAT/SLIT (numa::). When NUMA is
183+
// not available (no SRAT, the common single-node VM) this degrades to the
184+
// old single-node-0 behavior. We still apply MPOL_DEFAULT everywhere: the
185+
// allocator is not yet node-aware, so we cannot honor a real policy, but we
186+
// can at least answer topology queries truthfully.
184187
if ((flags & MPOL_F_NODE)) {
185-
*policy = 0; // in this case, store a node id, not a policy
188+
// Store a node id in *policy, not a policy value.
189+
if (!policy) {
190+
errno = EINVAL;
191+
return -1;
192+
}
193+
if (flags & MPOL_F_ADDR) {
194+
// The node backing the given address, if we can resolve it.
195+
int node = -1;
196+
if (numa::available() && addr) {
197+
auto phys = mmu::virt_to_phys_pt(addr);
198+
node = numa::node_of_phys(phys);
199+
}
200+
*policy = node < 0 ? 0 : node;
201+
} else {
202+
// The node the calling CPU runs on.
203+
*policy = numa::node_of_cpu(sched::cpu::current()->id);
204+
}
186205
return 0;
187206
}
188207
if (policy) {
189208
*policy = MPOL_DEFAULT;
190209
}
191210
if (nmask) {
192-
if (maxnode < 1) {
211+
if (maxnode < numa::nr_nodes()) {
193212
errno = EINVAL;
194213
return -1;
195214
}
196-
nmask[0] |= 1;
215+
// Set a bit for every node that exists.
216+
for (unsigned n = 0; n < numa::nr_nodes(); n++) {
217+
nmask[n / (8 * sizeof(unsigned long))] |=
218+
1UL << (n % (8 * sizeof(unsigned long)));
219+
}
197220
}
198221
return 0;
199222
}
@@ -203,9 +226,11 @@ static long get_mempolicy(int *policy, unsigned long *nmask,
203226
static long set_mempolicy(int policy, unsigned long *nmask,
204227
unsigned long maxnode)
205228
{
206-
// OSv has very minimal support for NUMA - merely exposes
207-
// all cpus as a single node0 and cannot really apply any meaningful policy
208-
// Therefore we implement this as noop, ignore all arguments and return success
229+
// The topology is now discovered (numa::), but the physical allocator is
230+
// not yet node-aware, so we cannot actually enforce a placement policy.
231+
// Accept the call as a no-op (ignoring the requested policy) rather than
232+
// failing, so NUMA-aware programs run unmodified. Enforcement will come
233+
// when the allocator learns about nodes.
209234
return 0;
210235
}
211236
#endif
@@ -445,12 +470,13 @@ static long sys_getcwd(char *buf, unsigned long size)
445470
#define __NR_sys_getcpu __NR_getcpu
446471
static long sys_getcpu(unsigned int *cpu, unsigned int *node, void *tcache)
447472
{
473+
auto *c = sched::cpu::current();
448474
if (cpu) {
449-
*cpu = sched::cpu::current()->id;
475+
*cpu = c->id;
450476
}
451477

452478
if (node) {
453-
*node = 0;
479+
*node = numa::node_of_cpu(c->id);
454480
}
455481

456482
return 0;

modules/tests/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ tests := tst-pthread.so misc-ramdisk.so tst-vblk.so tst-bsd-evh.so \
125125
tst-prctl.so \
126126
tst-mmap-file-cow.so \
127127
tst-numa.so \
128+
tst-numa.so tst-numa-mempolicy.so \
128129
tst-elf-permissions.so misc-mutex.so misc-sockets.so tst-condvar.so \
129130
tst-queue-mpsc.so tst-af-local.so tst-pipe.so tst-yield.so \
130131
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)