Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,7 @@ endif
objects += linux.o
objects += core/commands.o
objects += core/sched.o
objects += core/numa.o
objects += core/mmio.o
objects += core/kprintf.o
ifeq ($(conf_tracepoints),1)
Expand Down
72 changes: 72 additions & 0 deletions core/mempool.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#endif
#include <atomic>
#include <osv/mmu.hh>
#include <osv/numa.hh>
#include <osv/trace.hh>
#include <lockfree/ring.hh>
#include <osv/percpu-worker.hh>
Expand Down Expand Up @@ -597,6 +598,11 @@ class page_range_allocator {
page_range* alloc(size_t size, bool contiguous = true);
page_range* alloc_aligned(size_t size, size_t offset, size_t alignment,
bool fill = false);
// Carve a single page from a free range that lies on NUMA node `node`,
// returning it, or nullptr if no free memory on that node is available.
// Used by the node-preferring allocator; the caller falls back to a normal
// allocation when this returns nullptr.
page_range* alloc_page_from_node(int node);
void free(page_range* pr);

void initial_add(page_range* pr);
Expand Down Expand Up @@ -796,6 +802,38 @@ page_range* page_range_allocator::alloc(size_t size, bool contiguous)
return &pr;
}

page_range* page_range_allocator::alloc_page_from_node(int node)
{
// Find a free range whose memory lies on the requested NUMA node, then
// carve a single page from its front (mirroring alloc()). This is a
// best-effort preference: a linear scan over the free lists, returning the
// first range on the node. Returns nullptr if none is found, so the caller
// can fall back to a node-agnostic allocation.
page_range* found = nullptr;
for_each(0, [&] (page_range& pr) {
auto phys = mmu::virt_to_phys(static_cast<void*>(&pr));
if (numa::node_of_phys(phys) == node) {
found = &pr;
return false; // stop iterating
}
return true;
});
if (!found) {
return nullptr;
}

auto& pr = *found;
remove(pr);
if (pr.size > page_size) {
auto& np = *new (static_cast<void*>(&pr) + page_size)
page_range(pr.size - page_size);
insert(np);
pr.size = page_size;
}
set_bits(pr, false);
return &pr;
}

page_range* page_range_allocator::alloc_aligned(size_t size, size_t offset,
size_t alignment, bool fill)
{
Expand Down Expand Up @@ -1789,6 +1827,40 @@ void* alloc_page()
return p;
}

// Allocate one page, preferring memory local to NUMA node `node`. Falls back
// to a node-agnostic allocation when the node has no free memory local to it
// in the global page-range allocator (or NUMA is not available).
//
// This is a best-effort node-preferring hint that the scheduler and
// mbind/set_mempolicy can build on. It only sees memory still held by the
// global page-range allocator: OSv drains much of physical memory into per-CPU
// L1/L2 pools that are not (yet) node-partitioned, so once a node's global
// ranges are exhausted this falls back. Full per-node pools are future work
// (roadmap B2.2-full); this step gives correct placement while node-local
// global memory is available and never fails or misplaces.
void* alloc_page_on_node(int node)
{
if (node < 0 || !numa::available() || (unsigned)node >= numa::nr_nodes()) {
return alloc_page();
}
void* ret = nullptr;
WITH_LOCK(free_page_ranges_lock) {
page_range* pr = free_page_ranges.alloc_page_from_node(node);
if (pr) {
on_alloc(page_size);
ret = static_cast<void*>(pr);
}
}
if (!ret) {
// No free memory on that node: fall back rather than fail.
return alloc_page();
}
#if CONF_memory_tracker
tracker_remember(ret, page_size);
#endif
return ret;
}

static inline void untracked_free_page(void *v)
{
trace_memory_page_free(v);
Expand Down
188 changes: 188 additions & 0 deletions core/numa.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright (C) 2026 Waldemar Kozaczuk
*
* This work is open source software, licensed under the terms of the
* BSD license as described in the LICENSE file in the top-level directory.
*/

#include <osv/numa.hh>
#include <osv/sched.hh>
#include <osv/debug.hh>

#include <unordered_map>
#include <algorithm>

#include <osv/drivers_config.h>

#if CONF_drivers_acpi
extern "C" {
#include "acpi.h"
}
#include <boost/intrusive/parent_from_member.hpp>
#endif

namespace numa {

static bool s_available = false;
static unsigned s_nr_nodes = 1;
// Map from a raw APIC id to the NUMA node it belongs to (from SRAT).
static std::unordered_map<uint32_t, unsigned> s_apic_to_node;
// Map from a sched cpu id to its NUMA node (resolved via APIC id).
static std::unordered_map<unsigned, unsigned> s_cpu_to_node;
// SLIT distance matrix, row-major, s_nr_nodes x s_nr_nodes; empty if no SLIT.
static std::vector<uint8_t> s_distances;
static std::vector<mem_range> s_mem_ranges;
static bool s_initialized = false;

unsigned nr_nodes() { return s_nr_nodes; }
bool available() { return s_available; }
const std::vector<mem_range>& memory_ranges() { return s_mem_ranges; }

unsigned node_of_cpu(unsigned cpu_id)
{
auto it = s_cpu_to_node.find(cpu_id);
return it == s_cpu_to_node.end() ? 0 : it->second;
}

int node_of_phys(uint64_t phys)
{
for (auto& r : s_mem_ranges) {
if (phys >= r.base && phys < r.base + r.length) {
return (int)r.node;
}
}
return -1;
}

unsigned distance(unsigned from, unsigned to)
{
if (from == to) {
return 10; // ACPI convention: 10 == local.
}
if (!s_distances.empty() && from < s_nr_nodes && to < s_nr_nodes) {
return s_distances[from * s_nr_nodes + to];
}
return 20; // Default remote distance when no SLIT is present.
}

#if CONF_drivers_acpi
using boost::intrusive::get_parent_from_member;

// Record the (apic id -> node) mapping and track the highest node seen.
static void record_cpu_affinity(uint32_t apic_id, unsigned node, unsigned& max_node)
{
s_apic_to_node[apic_id] = node;
max_node = std::max(max_node, node);
}

static void parse_srat()
{
char sig[] = ACPI_SIG_SRAT;
ACPI_TABLE_HEADER* header;
if (AcpiGetTable(sig, 0, &header) != AE_OK) {
return; // No SRAT: leave the single-node fallback in place.
}
auto srat = get_parent_from_member(header, &ACPI_TABLE_SRAT::Header);
void* sub = srat + 1;
void* end = static_cast<void*>(srat) + srat->Header.Length;
unsigned max_node = 0;

while (sub < end) {
auto s = static_cast<ACPI_SUBTABLE_HEADER*>(sub);
if (s->Length == 0) {
break; // Guard against a malformed zero-length subtable.
}
switch (s->Type) {
case ACPI_SRAT_TYPE_CPU_AFFINITY: {
auto a = get_parent_from_member(s, &ACPI_SRAT_CPU_AFFINITY::Header);
if (a->Flags & ACPI_SRAT_CPU_ENABLED) {
unsigned node = a->ProximityDomainLo |
(a->ProximityDomainHi[0] << 8) |
(a->ProximityDomainHi[1] << 16) |
(a->ProximityDomainHi[2] << 24);
record_cpu_affinity(a->ApicId, node, max_node);
}
break;
}
case ACPI_SRAT_TYPE_X2APIC_CPU_AFFINITY: {
auto a = get_parent_from_member(s, &ACPI_SRAT_X2APIC_CPU_AFFINITY::Header);
if (a->Flags & ACPI_SRAT_CPU_ENABLED) {
record_cpu_affinity(a->ApicId, a->ProximityDomain, max_node);
}
break;
}
case ACPI_SRAT_TYPE_MEMORY_AFFINITY: {
auto m = get_parent_from_member(s, &ACPI_SRAT_MEM_AFFINITY::Header);
if (m->Flags & ACPI_SRAT_MEM_ENABLED) {
s_mem_ranges.push_back(mem_range{
m->BaseAddress, m->Length, m->ProximityDomain,
(m->Flags & ACPI_SRAT_MEM_HOT_PLUGGABLE) != 0});
max_node = std::max(max_node, (unsigned)m->ProximityDomain);
}
break;
}
default:
break;
}
sub = static_cast<void*>(sub) + s->Length;
}

if (!s_apic_to_node.empty() || !s_mem_ranges.empty()) {
s_available = true;
s_nr_nodes = max_node + 1;
}
}

static void parse_slit()
{
char sig[] = ACPI_SIG_SLIT;
ACPI_TABLE_HEADER* header;
if (AcpiGetTable(sig, 0, &header) != AE_OK) {
return;
}
auto slit = get_parent_from_member(header, &ACPI_TABLE_SLIT::Header);
uint64_t n = slit->LocalityCount;
// Only trust SLIT if it agrees with the node count we saw in SRAT.
if (n == 0 || n != s_nr_nodes) {
return;
}
s_distances.assign(slit->Entry, slit->Entry + n * n);
}

// Resolve the (apic id -> node) map into a (sched cpu id -> node) map.
static void resolve_cpus()
{
for (auto* c : sched::cpus) {
auto it = s_apic_to_node.find(c->arch.apic_id);
if (it != s_apic_to_node.end()) {
s_cpu_to_node[c->id] = it->second;
}
}
}
#endif

void init()
{
if (s_initialized) {
return;
}
s_initialized = true;

#if CONF_drivers_acpi
parse_srat();
if (s_available) {
parse_slit();
resolve_cpus();
}
#endif

if (s_available) {
debugf("NUMA: %u node(s), %zu CPU(s) mapped, %zu memory range(s)%s\n",
s_nr_nodes, s_cpu_to_node.size(), s_mem_ranges.size(),
s_distances.empty() ? ", no SLIT" : "");
} else {
debugf("NUMA: no SRAT, assuming a single flat node\n");
}
}

}
67 changes: 67 additions & 0 deletions include/osv/numa.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (C) 2026 Waldemar Kozaczuk
*
* This work is open source software, licensed under the terms of the
* BSD license as described in the LICENSE file in the top-level directory.
*/

#ifndef OSV_NUMA_HH
#define OSV_NUMA_HH

#include <cstdint>
#include <vector>
#include <cstddef>

// NUMA topology discovery.
//
// This module parses the ACPI SRAT (System Resource Affinity Table) and SLIT
// (System Locality Distance Information Table) to learn the machine's NUMA
// layout: which node each CPU belongs to, which physical memory ranges belong
// to each node, and the relative distances between nodes.
//
// This is discovery only: it does not change how memory is allocated or how
// threads are scheduled. It exposes the topology so later work (a node-aware
// allocator, scheduler affinity, mbind/get_mempolicy) can use it. On a machine
// with no SRAT (the common single-node virtual machine), the whole system is
// reported as one node (node 0) containing every CPU.

namespace numa {

// A contiguous physical memory range assigned to a NUMA node.
struct mem_range {
uint64_t base;
uint64_t length;
unsigned node;
bool hotpluggable;
};

// Discover the topology by parsing SRAT/SLIT. Safe to call once, after ACPI is
// initialized and the CPUs have been enumerated (so APIC ids are known). If no
// SRAT is present, initializes a single flat node. Idempotent.
void init();

// Number of NUMA nodes (>= 1).
unsigned nr_nodes();

// True if the topology came from a real SRAT (as opposed to the synthesized
// single-node fallback).
bool available();

// The node a CPU (by sched cpu id) belongs to, or 0 if unknown.
unsigned node_of_cpu(unsigned cpu_id);

// The node that owns a physical address, or -1 if it falls in no known range.
// Only meaningful when available() is true.
int node_of_phys(uint64_t phys);

// The SLIT distance from node `from` to node `to`. Linux/ACPI convention:
// 10 == local (same node), higher == farther. Returns 10 for the diagonal and
// a default (10 local / 20 remote) when no SLIT is present.
unsigned distance(unsigned from, unsigned to);

// The memory ranges discovered from SRAT (empty if none).
const std::vector<mem_range>& memory_ranges();

}

#endif
4 changes: 4 additions & 0 deletions include/osv/pagealloc.hh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
namespace memory {

void* alloc_page();
// Allocate one page preferring memory local to NUMA node `node`, falling back
// to a node-agnostic allocation when the node has no free memory or NUMA is
// unavailable.
void* alloc_page_on_node(int node);
void free_page(void* page);
void* alloc_huge_page(size_t bytes);
void free_huge_page(void *page, size_t bytes);
Expand Down
Loading