From f78e7a0949182420918aeb2dd1d44cb9ca534436 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 13 May 2026 14:58:21 +0200 Subject: [PATCH 1/4] cpu-set-util: add cpu_set_count() helper Let's add a minor, simplifying helper for getting number of CPUs in a mask. --- src/shared/cpu-set-util.h | 8 ++++++++ src/shared/numa-util.c | 2 +- src/test/test-cpu-set-util.c | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/shared/cpu-set-util.h b/src/shared/cpu-set-util.h index a74e264e12..751e9add27 100644 --- a/src/shared/cpu-set-util.h +++ b/src/shared/cpu-set-util.h @@ -38,3 +38,11 @@ int cpu_set_to_dbus(const CPUSet *c, uint8_t **ret, size_t *ret_size); int cpu_set_from_dbus(const uint8_t *bits, size_t size, CPUSet *ret); int cpus_in_affinity_mask(void); + +static inline size_t cpu_set_count(const CPUSet *c) { + if (!c) + return 0; + if (c->allocated <= 0) + return 0; + return CPU_COUNT_S(c->allocated, c->set); +} diff --git a/src/shared/numa-util.c b/src/shared/numa-util.c index c011c4381d..228ea7ad2d 100644 --- a/src/shared/numa-util.c +++ b/src/shared/numa-util.c @@ -26,7 +26,7 @@ bool numa_policy_is_valid(const NUMAPolicy *policy) { if (policy->nodes.set && numa_policy_get_type(policy) == MPOL_PREFERRED && - CPU_COUNT_S(policy->nodes.allocated, policy->nodes.set) != 1) + cpu_set_count(&policy->nodes) != 1) return false; return true; diff --git a/src/test/test-cpu-set-util.c b/src/test/test-cpu-set-util.c index bd22e4ddb2..672451ff35 100644 --- a/src/test/test-cpu-set-util.c +++ b/src/test/test-cpu-set-util.c @@ -11,7 +11,8 @@ #define ASSERT_CPUSET_COUNT(c, n) \ ASSERT_NOT_NULL(c.set); \ ASSERT_GE(c.allocated, CPU_ALLOC_SIZE(n)); \ - ASSERT_EQ(CPU_COUNT_S(c.allocated, c.set), (n)) + ASSERT_EQ(CPU_COUNT_S(c.allocated, c.set), (n)); \ + ASSERT_EQ(cpu_set_count(&c), (size_t) (n)) #define ASSERT_CPUSET_ISSET(c, i) \ ASSERT_TRUE(CPU_ISSET_S(i, c.allocated, c.set)); From 94ef901b1ce107551d2ea23700536a04d0b778f7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 13 May 2026 14:59:35 +0200 Subject: [PATCH 2/4] cpu-set-util: introduce cpus_online(). Add a helper that tries to determine the number of installed CPUs. This borrows heavily from physical_memory(), i.e. uses the physical number, but caps by per-container cpuset. --- src/modules-load/modules-load.c | 15 ++++++----- src/shared/cpu-set-util.c | 48 +++++++++++++++++++++++++++++++++ src/shared/cpu-set-util.h | 2 ++ src/test/test-cpu-set-util.c | 7 +++++ 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/modules-load/modules-load.c b/src/modules-load/modules-load.c index 0917f800a1..eb58b70c7b 100644 --- a/src/modules-load/modules-load.c +++ b/src/modules-load/modules-load.c @@ -11,6 +11,7 @@ #include "build.h" #include "conf-files.h" #include "constants.h" +#include "cpu-set-util.h" #include "errno-util.h" #include "fd-util.h" #include "fileio.h" @@ -314,13 +315,13 @@ static unsigned determine_num_worker_threads(unsigned n_modules) { if (n_threads == UINT_MAX) { /* By default, use a number of worker threads equal the number of online CPUs, * but clamp it to avoid a probing storm on machines with many CPUs. */ - long ncpus = sysconf(_SC_NPROCESSORS_ONLN); - if (ncpus < 0) { - log_warning_errno(errno, "Failed to get number of online CPUs, ignoring: %m"); - ncpus = 1; - } - - n_threads = CLAMP((unsigned)ncpus, 1U, 16U); + unsigned n_cpus; + r = cpus_online(&n_cpus); + if (r < 0) { + log_warning_errno(r, "Failed to get number of online CPUs, ignoring: %m"); + n_threads = 1; + } else + n_threads = CLAMP(n_cpus, 1U, 16U); } /* There's no reason to spawn more threads than the modules that need to be loaded */ diff --git a/src/shared/cpu-set-util.c b/src/shared/cpu-set-util.c index 9211dbe47e..85d0e11402 100644 --- a/src/shared/cpu-set-util.c +++ b/src/shared/cpu-set-util.c @@ -6,6 +6,7 @@ #include "alloc-util.h" #include "bitfield.h" +#include "cgroup-util.h" #include "cpu-set-util.h" #include "extract-word.h" #include "log.h" @@ -400,3 +401,50 @@ int cpu_set_from_dbus(const uint8_t *bits, size_t size, CPUSet *ret) { *ret = TAKE_STRUCT(c); return 0; } + +static int cgroup_cpus_effective(unsigned *ret) { + int r; + + assert(ret); + + _cleanup_free_ char *root = NULL; + r = cg_get_root_path(&root); + if (r < 0) + return log_debug_errno(r, "Failed to determine root cgroup: %m"); + + _cleanup_free_ char *value = NULL; + r = cg_get_attribute(root, "cpuset.cpus.effective", &value); + if (r < 0) + return log_debug_errno(r, "Failed to read cpuset.cpus.effective cgroup attribute: %m"); + + _cleanup_(cpu_set_done) CPUSet cpus = {}; + r = parse_cpu_set(value, &cpus); + if (r < 0) + return log_debug_errno(r, "Failed to parse cpuset.cpus.effective cgroup attribute: %m"); + + *ret = (unsigned) MIN(cpu_set_count(&cpus), UINT_MAX); + return 0; +} + +int cpus_online(unsigned *ret) { + int r; + + assert(ret); + + /* In order to support containers nicely that have a configured cpuset we'll take the minimum of the + * physically reported amount of CPUs and the limit configured for the root cgroup, if there is + * any. */ + + long sc = sysconf(_SC_NPROCESSORS_ONLN); + if (sc < 0) + return log_debug_errno(errno, "sysconf(_SC_NPROCESSORS_ONLN) failed: %m"); + + unsigned cg, lc = (unsigned) CLAMP((unsigned long) sc, 1U, UINT_MAX); + r = cgroup_cpus_effective(&cg); + if (r < 0) + *ret = lc; + else + *ret = CLAMP(cg, 1U, lc); + + return 0; +} diff --git a/src/shared/cpu-set-util.h b/src/shared/cpu-set-util.h index 751e9add27..1f89ce2020 100644 --- a/src/shared/cpu-set-util.h +++ b/src/shared/cpu-set-util.h @@ -46,3 +46,5 @@ static inline size_t cpu_set_count(const CPUSet *c) { return 0; return CPU_COUNT_S(c->allocated, c->set); } + +int cpus_online(unsigned *ret); diff --git a/src/test/test-cpu-set-util.c b/src/test/test-cpu-set-util.c index 672451ff35..027e0ca2d4 100644 --- a/src/test/test-cpu-set-util.c +++ b/src/test/test-cpu-set-util.c @@ -284,4 +284,11 @@ TEST(cpu_set_add_range) { ASSERT_OK(cpu_set_add_range(&c, 0, 8191)); } +TEST(cpus_online) { + unsigned n_cpus = 0; + ASSERT_OK(cpus_online(&n_cpus)); + ASSERT_GE(n_cpus, 1U); + log_info("Number of CPUs currently online: %u", n_cpus); +} + DEFINE_TEST_MAIN(LOG_DEBUG); From e5f27e51f9f57f50944821fea31a8673154dfb7f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 13 May 2026 15:00:08 +0200 Subject: [PATCH 3/4] report-basic: export PhysicalMemorybytes + CPUsOnline metrics --- src/report/report-basic.c | 45 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/report/report-basic.c b/src/report/report-basic.c index 772a5d9f74..43c9cb9774 100644 --- a/src/report/report-basic.c +++ b/src/report/report-basic.c @@ -8,7 +8,9 @@ #include "alloc-util.h" #include "architecture.h" +#include "cpu-set-util.h" #include "hostname-setup.h" +#include "limits-util.h" #include "log.h" #include "metrics.h" #include "os-util.h" @@ -100,6 +102,37 @@ static int machine_id_generate(const MetricFamily *mf, sd_varlink *link, void *u /* fields= */ NULL); } +static int physical_memory_generate(const MetricFamily *mf, sd_varlink *link, void *userdata) { + assert(mf && mf->name); + assert(link); + + return metric_build_send_unsigned( + mf, + link, + /* object= */ NULL, + physical_memory(), + /* fields= */ NULL); +} + +static int cpus_online_generate(const MetricFamily *mf, sd_varlink *link, void *userdata) { + int r; + + assert(mf && mf->name); + assert(link); + + unsigned n_cpus; + r = cpus_online(&n_cpus); + if (r < 0) + return r; + + return metric_build_send_unsigned( + mf, + link, + /* object= */ NULL, + n_cpus, + /* fields= */ NULL); +} + enum { FIELD_PRETTY_NAME, FIELD_NAME, @@ -203,6 +236,12 @@ static const MetricFamily metric_family_table[] = { METRIC_FAMILY_TYPE_STRING, .generate = boot_id_generate, }, + { + METRIC_IO_SYSTEMD_BASIC_PREFIX "CPUsOnline", + "Number of CPUs currently online", + METRIC_FAMILY_TYPE_GAUGE, + .generate = cpus_online_generate, + }, { METRIC_IO_SYSTEMD_BASIC_PREFIX "Hostname", "System hostname", @@ -239,6 +278,12 @@ static const MetricFamily metric_family_table[] = { OS_RELEASE_STANDARD_FIELD("SYSEXT_LEVEL"), OS_RELEASE_STANDARD_FIELD("CONFEXT_LEVEL"), /* Keep those ↑ in sync with os_release_generate(). */ + { + METRIC_IO_SYSTEMD_BASIC_PREFIX "PhysicalMemoryBytes", + "Installed physical memory in bytes", + METRIC_FAMILY_TYPE_GAUGE, + .generate = physical_memory_generate, + }, { METRIC_IO_SYSTEMD_BASIC_PREFIX "Virtualization", "Virtualization type", From 29fbad2b7f806afcaf13b20b681d120a5516b1c9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 13 May 2026 15:06:57 +0200 Subject: [PATCH 4/4] update TODO --- TODO.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.md b/TODO.md index 422145e25b..024e2bcfb0 100644 --- a/TODO.md +++ b/TODO.md @@ -136,6 +136,8 @@ SPDX-License-Identifier: LGPL-2.1-or-later "nothing"/"invalid"/"zero"/"please-suppress". Then use that to reduce noise in systemd-report output. +- cgroup-metrics: add per-cgroup PSI metrics + - sysupdate: offer reading transfer files/components/features optionally from some JSON fragment rather than transfer files, so that we can update it independently from any DDI, and it needs no activation cycle. Why? so that