From 9316e0ff1870a29b12885d986fb993d56a98d363 Mon Sep 17 00:00:00 2001 From: Konstantinos Samaras-Tsakiris <5130925+Oblynx@users.noreply.github.com> Date: Thu, 25 Jun 2026 09:00:12 +0000 Subject: [PATCH 1/4] linux: add cgroup v2 memory file parsers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce linux/CGroupMem with pure, string-input parsers for the cgroup v2 files htop needs to discover and read its own memory node: - parseLimit — memory.max / memory.swap.max ("max" or bytes -> kB) - parseUsage — memory.current / memory.swap.current (bytes -> kB) - parseStat — a named field (e.g. "file") from memory.stat, exact-token - parseSelfCgroup — the v2 ("0::") path from /proc/self/cgroup - parseMountinfo — the cgroup2 mount point from /proc/self/mountinfo Values use uint64_t and the parsers build on htop's String_startsWith, String_strchrnul and String_safeStrncpy helpers rather than open-coding prefix/scan/copy logic. They take strings rather than touching the filesystem, so they are covered by a standalone unit test (linux/CGroupMemTest) wired into check_PROGRAMS/TESTS. No behaviour change yet: nothing calls them. Assisted-by: Claude Signed-off-by: Konstantinos Samaras-Tsakiris <5130925+Oblynx@users.noreply.github.com> --- Makefile.am | 7 +++ linux/CGroupMem.c | 134 ++++++++++++++++++++++++++++++++++++++++++ linux/CGroupMem.h | 33 +++++++++++ linux/CGroupMemTest.c | 103 ++++++++++++++++++++++++++++++++ 4 files changed, 277 insertions(+) create mode 100644 linux/CGroupMem.c create mode 100644 linux/CGroupMem.h create mode 100644 linux/CGroupMemTest.c diff --git a/Makefile.am b/Makefile.am index 52134df64..7fd62798c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -188,6 +188,7 @@ linux_platform_headers = \ generic/gettime.h \ generic/hostname.h \ generic/uname.h \ + linux/CGroupMem.h \ linux/CGroupUtils.h \ linux/Compat.h \ linux/GPU.h \ @@ -216,6 +217,7 @@ linux_platform_sources = \ generic/gettime.c \ generic/hostname.c \ generic/uname.c \ + linux/CGroupMem.c \ linux/CGroupUtils.c \ linux/Compat.c \ linux/GPU.c \ @@ -527,6 +529,11 @@ coverage: cppcheck: cppcheck -q -v . --enable=all -DHAVE_OPENVZ +check_PROGRAMS = linux/CGroupMemTest +TESTS = $(check_PROGRAMS) +linux_CGroupMemTest_SOURCES = linux/CGroupMemTest.c linux/CGroupMem.c linux/CGroupMem.h XUtils.c +linux_CGroupMemTest_CPPFLAGS = $(AM_CPPFLAGS) -D_DEFAULT_SOURCE + dist-hook: $(top_distdir)/configure @if test "x$$FORCE_MAKE_DIST" != x || \ grep 'pkg_m4_included' '$(top_distdir)/configure' >/dev/null; then :; \ diff --git a/linux/CGroupMem.c b/linux/CGroupMem.c new file mode 100644 index 000000000..a176bdcf1 --- /dev/null +++ b/linux/CGroupMem.c @@ -0,0 +1,134 @@ +/* +htop - CGroupMem.c +(C) 2026 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "config.h" // IWYU pragma: keep + +#include "linux/CGroupMem.h" + +#include +#include +#include + +#include "Macros.h" +#include "XUtils.h" + + +bool CGroupMem_parseUsage(const char* content, uint64_t* outKB) { + if (!content) + return false; + + char* end; + unsigned long long bytes = strtoull(content, &end, 10); + if (end == content) + return false; + + *outKB = (uint64_t)(bytes / 1024); + return true; +} + +bool CGroupMem_parseLimit(const char* content, uint64_t* outKB) { + if (!content) + return false; + + while (*content == ' ' || *content == '\t') + content++; + + /* "max" means unlimited; otherwise it is a plain byte count like a usage file. */ + if (String_startsWith(content, "max")) + return false; + + return CGroupMem_parseUsage(content, outKB); +} + +bool CGroupMem_parseStat(const char* content, const char* key, uint64_t* outKB) { + if (!content || !key) + return false; + + size_t keyLen = strlen(key); + const char* p = content; + while (*p) { + const char* eol = String_strchrnul(p, '\n'); + + /* match "key " at the start of the line (exact token, space-delimited) */ + if (String_startsWith(p, key) && p[keyLen] == ' ') { + char* end; + unsigned long long bytes = strtoull(p + keyLen + 1, &end, 10); + if (end != p + keyLen + 1) { + *outKB = (uint64_t)(bytes / 1024); + return true; + } + } + + if (*eol == '\0') + break; + p = eol + 1; + } + return false; +} + +bool CGroupMem_parseMountinfo(const char* content, char* mountBuf, size_t mountSize) { + if (!content || mountSize == 0) + return false; + + const char* p = content; + while (*p) { + const char* eol = String_strchrnul(p, '\n'); + + /* the filesystem type is the first token after the " - " separator */ + const char* sep = strstr(p, " - "); + if (sep && sep < eol) { + const char* fstype = sep + 3; + if (String_startsWith(fstype, "cgroup2") && (fstype[7] == ' ' || fstype[7] == '\t')) { + /* mount point is the 5th space-separated field (index 4) before the separator */ + const char* q = p; + int field = 0; + while (field < 4 && q < sep) { + q = String_strchrnul(q, ' '); + if (q >= sep) + break; + q++; + field++; + } + if (field == 4 && q < sep) { + const char* mpEnd = String_strchrnul(q, ' '); + if (mpEnd <= sep) { + size_t mpLen = (size_t)(mpEnd - q); + String_safeStrncpy(mountBuf, q, MINIMUM(mpLen + 1, mountSize)); + return true; + } + } + } + } + + if (*eol == '\0') + break; + p = eol + 1; + } + return false; +} + +bool CGroupMem_parseSelfCgroup(const char* content, char* pathBuf, size_t bufSize) { + if (!content || bufSize == 0) + return false; + + const char* p = content; + while (*p) { + const char* eol = String_strchrnul(p, '\n'); + + if (String_startsWith(p, "0::")) { + const char* path = p + 3; + size_t pathLen = (size_t)(eol - path); + String_safeStrncpy(pathBuf, path, MINIMUM(pathLen + 1, bufSize)); + return true; + } + + if (*eol == '\0') + break; + p = eol + 1; + } + return false; +} diff --git a/linux/CGroupMem.h b/linux/CGroupMem.h new file mode 100644 index 000000000..d3bb8519f --- /dev/null +++ b/linux/CGroupMem.h @@ -0,0 +1,33 @@ +#ifndef HEADER_CGroupMem +#define HEADER_CGroupMem +/* +htop - CGroupMem.h +(C) 2026 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include +#include +#include + + +/* Pure parsers (testable with string inputs). */ + +/* Parse a cgroup limit file body ("max\n" or "\n"). Returns false for + "max"/empty/invalid (unlimited or unreadable); true with *outKB set otherwise. */ +bool CGroupMem_parseLimit(const char* content, uint64_t* outKB); + +/* Parse a cgroup usage file body ("\n"). false if not a number. */ +bool CGroupMem_parseUsage(const char* content, uint64_t* outKB); + +/* Parse memory.stat body for `key` (e.g. "file"); value is bytes → kB. false if key absent. */ +bool CGroupMem_parseStat(const char* content, const char* key, uint64_t* outKB); + +/* Parse /proc/self/cgroup body; copy the v2 ("0::") path into pathBuf. false if no 0:: line. */ +bool CGroupMem_parseSelfCgroup(const char* content, char* pathBuf, size_t bufSize); + +/* Parse /proc/self/mountinfo body; copy the cgroup2 mount point into mountBuf. false if none. */ +bool CGroupMem_parseMountinfo(const char* content, char* mountBuf, size_t mountSize); + +#endif /* HEADER_CGroupMem */ diff --git a/linux/CGroupMemTest.c b/linux/CGroupMemTest.c new file mode 100644 index 000000000..db2a03cc3 --- /dev/null +++ b/linux/CGroupMemTest.c @@ -0,0 +1,103 @@ +/* +htop - CGroupMemTest.c +(C) 2026 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "linux/CGroupMem.h" + +#include +#include +#include + + +/* XUtils' allocation-failure path references CRT_done(); this test links XUtils.o + but never triggers OOM, so a no-op stub satisfies the linker without pulling in + CRT and the whole TUI. */ +void CRT_done(void); +void CRT_done(void) { } + +static int checks = 0; +static int failures = 0; + +#define CHECK(cond) \ + do { \ + checks++; \ + if (!(cond)) { \ + failures++; \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + } \ + } while (0) + + +static void test_parseLimit(void) { + uint64_t kb = 999; + CHECK(CGroupMem_parseLimit("max\n", &kb) == false); + CHECK(CGroupMem_parseLimit("", &kb) == false); + CHECK(CGroupMem_parseLimit("4294967296\n", &kb) == true); + CHECK(kb == 4194304ULL); /* 4 GiB / 1024 */ +} + +static void test_parseUsage(void) { + uint64_t kb = 0; + CHECK(CGroupMem_parseUsage("2202009600\n", &kb) == true); + CHECK(kb == 2150400ULL); /* 2202009600 / 1024 */ + CHECK(CGroupMem_parseUsage("garbage\n", &kb) == false); +} + +static void test_parseStat(void) { + const char* stat = + "anon 1048576\n" + "file 524288000\n" + "kernel_stack 16384\n" + "file_mapped 100\n"; + uint64_t kb = 0; + CHECK(CGroupMem_parseStat(stat, "file", &kb) == true); + CHECK(kb == 512000ULL); /* 524288000 / 1024 */ + CHECK(CGroupMem_parseStat(stat, "anon", &kb) == true); + CHECK(kb == 1024ULL); + /* must not match the "file_mapped" prefix when asked for "file" — exact token only */ + CHECK(CGroupMem_parseStat(stat, "slab", &kb) == false); +} + +static void test_parseSelfCgroup(void) { + char buf[256]; + /* namespaced container: path is the namespace root */ + CHECK(CGroupMem_parseSelfCgroup("0::/\n", buf, sizeof(buf)) == true); + CHECK(buf[0] == '/' && buf[1] == '\0'); + /* hybrid: v1 controller lines then a v2 line */ + CHECK(CGroupMem_parseSelfCgroup("4:memory:/foo\n0::/bar/baz\n", buf, sizeof(buf)) == true); + CHECK(CGroupMem_parseSelfCgroup("0::/bar/baz\n", buf, sizeof(buf)) == true); + /* compare the second result */ + CHECK(buf[0] == '/' && buf[1] == 'b' && buf[2] == 'a' && buf[3] == 'r'); + /* pure v1: no 0:: line */ + CHECK(CGroupMem_parseSelfCgroup("4:memory:/foo\n", buf, sizeof(buf)) == false); +} + +static void test_parseMountinfo(void) { + char buf[256]; + const char* mi = + "23 28 0:22 / /proc rw,nosuid - proc proc rw\n" + "29 23 0:25 / /sys/fs/cgroup ro,nosuid,nodev,noexec - cgroup2 cgroup2 rw,nsdelegate\n"; + CHECK(CGroupMem_parseMountinfo(mi, buf, sizeof(buf)) == true); + CHECK(strcmp(buf, "/sys/fs/cgroup") == 0); + /* no cgroup2 mount → false */ + const char* none = "23 28 0:22 / /proc rw - proc proc rw\n"; + CHECK(CGroupMem_parseMountinfo(none, buf, sizeof(buf)) == false); +} + +int main(void) { + test_parseLimit(); + test_parseUsage(); + test_parseStat(); + test_parseSelfCgroup(); + test_parseMountinfo(); + + if (failures) { + fprintf(stderr, "%d/%d checks FAILED\n", failures, checks); + return 1; + } + printf("All %d checks passed\n", checks); + return 0; +} From 62023349c28a353664807f7488fbb55473139886 Mon Sep 17 00:00:00 2001 From: Konstantinos Samaras-Tsakiris <5130925+Oblynx@users.noreply.github.com> Date: Thu, 25 Jun 2026 09:00:25 +0000 Subject: [PATCH 2/4] linux: collect cgroup v2 memory stats each refresh Build the collection layer on top of the parsers and feed it from the Linux memory scan: - CGroupMemData holds the effective limit/usage/file-cache and the swap equivalents (all uint64_t kB), plus active/swapActive flags. - CGroupMem_readNode opens the node directory once and reads memory.{max,current,stat} and the swap files through Compat_openat / Compat_readfileat, keeping stdio out of this memory-sensitive path. A node is "active" only when a finite memory.max is below the host total; swap is only entered when memory itself is limited, so the Memory and Swap meters stay consistent. - CGroupMem_scan resolves our own v2 node once (cgroup2 mount from /proc/self/mountinfo + the "0::" path from /proc/self/cgroup, the namespace root inside a container) and reads it on every refresh. - LinuxMachine_scanMemoryInfo now calls CGroupMem_scan into a cgroupMem field, gated on the host total. readNode is unit-tested against a temp directory. Still no UI change: the data is gathered but not yet displayed. Assisted-by: Claude Signed-off-by: Konstantinos Samaras-Tsakiris <5130925+Oblynx@users.noreply.github.com> --- Makefile.am | 2 +- linux/CGroupMem.c | 97 +++++++++++++++++++++++++++++++++++++++++++ linux/CGroupMem.h | 18 ++++++++ linux/CGroupMemTest.c | 48 ++++++++++++++++++++- linux/LinuxMachine.c | 4 ++ linux/LinuxMachine.h | 2 + 6 files changed, 168 insertions(+), 3 deletions(-) diff --git a/Makefile.am b/Makefile.am index 7fd62798c..a54fc692c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -531,7 +531,7 @@ cppcheck: check_PROGRAMS = linux/CGroupMemTest TESTS = $(check_PROGRAMS) -linux_CGroupMemTest_SOURCES = linux/CGroupMemTest.c linux/CGroupMem.c linux/CGroupMem.h XUtils.c +linux_CGroupMemTest_SOURCES = linux/CGroupMemTest.c linux/CGroupMem.c linux/CGroupMem.h XUtils.c linux/Compat.c linux_CGroupMemTest_CPPFLAGS = $(AM_CPPFLAGS) -D_DEFAULT_SOURCE dist-hook: $(top_distdir)/configure diff --git a/linux/CGroupMem.c b/linux/CGroupMem.c index a176bdcf1..e7f5f63a8 100644 --- a/linux/CGroupMem.c +++ b/linux/CGroupMem.c @@ -9,13 +9,19 @@ in the source distribution for its full text. #include "linux/CGroupMem.h" +#include +#include #include +#include #include #include +#include #include "Macros.h" #include "XUtils.h" +#include "linux/Compat.h" + bool CGroupMem_parseUsage(const char* content, uint64_t* outKB) { if (!content) @@ -132,3 +138,94 @@ bool CGroupMem_parseSelfCgroup(const char* content, char* pathBuf, size_t bufSiz } return false; } + +void CGroupMem_readNode(const char* nodeDir, uint64_t hostTotalMemKB, CGroupMemData* data) { + char buf[4096]; + + data->active = false; + data->swapActive = false; + data->limit = data->current = data->file = 0; + data->swapLimit = data->swapCurrent = 0; + + int dirFd = Compat_openat(AT_FDCWD, nodeDir, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (dirFd < 0) + return; + + if (Compat_readfileat(dirFd, "memory.max", buf, sizeof(buf)) > 0) { + uint64_t kb; + if (CGroupMem_parseLimit(buf, &kb) && kb > 0 && kb < hostTotalMemKB) { + data->limit = kb; + data->active = true; + } + } + + if (data->active) { + if (Compat_readfileat(dirFd, "memory.current", buf, sizeof(buf)) > 0) + CGroupMem_parseUsage(buf, &data->current); + if (Compat_readfileat(dirFd, "memory.stat", buf, sizeof(buf)) > 0) + CGroupMem_parseStat(buf, "file", &data->file); + } + + /* Only enter swap cgroup mode when memory is also limited, so the Memory and + Swap meters stay consistent (a finite memory.swap.max with memory.max=max + must not flip Swap to cgroup mode while Memory stays on host totals). */ + if (data->active && Compat_readfileat(dirFd, "memory.swap.max", buf, sizeof(buf)) > 0) { + uint64_t kb; + if (CGroupMem_parseLimit(buf, &kb)) { + data->swapLimit = kb; + data->swapActive = true; + if (Compat_readfileat(dirFd, "memory.swap.current", buf, sizeof(buf)) > 0) + CGroupMem_parseUsage(buf, &data->swapCurrent); + } + } + + close(dirFd); +} + +/* Resolve our own cgroup v2 node directory: the cgroup2 mount point joined with the + "0::" path from /proc/self/cgroup ("/" = the mount point itself). false if not v2. */ +static bool CGroupMem_resolveNodeDir(char* nodeDir, size_t size) { + char fileBuf[8192]; + char mountBuf[PATH_MAX]; + char cgPath[PATH_MAX]; + + int selfFd = Compat_openat(AT_FDCWD, "/proc/self", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (selfFd < 0) + return false; + + bool ok = false; + if (Compat_readfileat(selfFd, "mountinfo", fileBuf, sizeof(fileBuf)) > 0 && + CGroupMem_parseMountinfo(fileBuf, mountBuf, sizeof(mountBuf)) && + Compat_readfileat(selfFd, "cgroup", fileBuf, sizeof(fileBuf)) > 0 && + CGroupMem_parseSelfCgroup(fileBuf, cgPath, sizeof(cgPath))) { + /* node dir = mount point + cgroup path; "0::/" → the mount point itself. Plain + snprintf (not xSnprintf) so an over-long cgroup path falls back to host mode + gracefully instead of aborting htop. */ + int n = (cgPath[0] == '/' && cgPath[1] == '\0') + ? snprintf(nodeDir, size, "%s", mountBuf) + : snprintf(nodeDir, size, "%s%s", mountBuf, cgPath); + ok = (n > 0 && (size_t)n < size); + } + + close(selfFd); + return ok; +} + +void CGroupMem_scan(uint64_t hostTotalMemKB, CGroupMemData* data) { + static bool resolved = false; + static bool available = false; + static char nodeDir[PATH_MAX]; + + if (!resolved) { + resolved = true; + available = CGroupMem_resolveNodeDir(nodeDir, sizeof(nodeDir)); + } + + if (!available) { + data->active = false; + data->swapActive = false; + return; + } + + CGroupMem_readNode(nodeDir, hostTotalMemKB, data); +} diff --git a/linux/CGroupMem.h b/linux/CGroupMem.h index d3bb8519f..2d56f74e4 100644 --- a/linux/CGroupMem.h +++ b/linux/CGroupMem.h @@ -12,6 +12,17 @@ in the source distribution for its full text. #include +/* All byte counts are stored in kB (bytes / 1024) to match htop's memory_t convention. */ +typedef struct CGroupMemData_ { + bool active; /* a finite memory.max < host total was found */ + bool swapActive; /* a finite memory.swap.max was found */ + uint64_t limit; /* memory.max, kB */ + uint64_t current; /* memory.current, kB */ + uint64_t file; /* memory.stat 'file', kB */ + uint64_t swapLimit; /* memory.swap.max, kB (may be 0 = swap disabled) */ + uint64_t swapCurrent; /* memory.swap.current, kB */ +} CGroupMemData; + /* Pure parsers (testable with string inputs). */ /* Parse a cgroup limit file body ("max\n" or "\n"). Returns false for @@ -30,4 +41,11 @@ bool CGroupMem_parseSelfCgroup(const char* content, char* pathBuf, size_t bufSiz /* Parse /proc/self/mountinfo body; copy the cgroup2 mount point into mountBuf. false if none. */ bool CGroupMem_parseMountinfo(const char* content, char* mountBuf, size_t mountSize); +/* Read the cgroup files under nodeDir and fill `data`. hostTotalMemKB gates activation + (active only when a finite limit < host total). Tolerant of missing files. */ +void CGroupMem_readNode(const char* nodeDir, uint64_t hostTotalMemKB, CGroupMemData* data); + +/* Resolve (and cache) our cgroup v2 node, then read it. Fills data->active = false on any failure. */ +void CGroupMem_scan(uint64_t hostTotalMemKB, CGroupMemData* data); + #endif /* HEADER_CGroupMem */ diff --git a/linux/CGroupMemTest.c b/linux/CGroupMemTest.c index db2a03cc3..77fb3ff10 100644 --- a/linux/CGroupMemTest.c +++ b/linux/CGroupMemTest.c @@ -9,12 +9,13 @@ in the source distribution for its full text. #include #include +#include #include /* XUtils' allocation-failure path references CRT_done(); this test links XUtils.o - but never triggers OOM, so a no-op stub satisfies the linker without pulling in - CRT and the whole TUI. */ + and Compat.o but never triggers OOM, so a no-op stub satisfies the linker without + pulling in CRT and the whole TUI. */ void CRT_done(void); void CRT_done(void) { } @@ -87,12 +88,55 @@ static void test_parseMountinfo(void) { CHECK(CGroupMem_parseMountinfo(none, buf, sizeof(buf)) == false); } +static void writeFile(const char* dir, const char* name, const char* body) { + char path[1024]; + snprintf(path, sizeof(path), "%s/%s", dir, name); + FILE* fp = fopen(path, "w"); + if (fp) { + fputs(body, fp); + fclose(fp); + } +} + +static void test_readNode(void) { + char dir[] = "/tmp/cgmtestXXXXXX"; + if (!mkdtemp(dir)) { + CHECK(0 && "mkdtemp failed"); + return; + } + + writeFile(dir, "memory.max", "4294967296\n"); /* 4 GiB */ + writeFile(dir, "memory.current", "2202009600\n"); /* ~2.05 GiB */ + writeFile(dir, "memory.stat", "anon 1048576\nfile 524288000\n"); + writeFile(dir, "memory.swap.max", "1073741824\n"); /* 1 GiB */ + writeFile(dir, "memory.swap.current", "10485760\n"); /* 10 MiB */ + + CGroupMemData data = {0}; + /* host has 64 GiB → limit (4 GiB) < host → active */ + CGroupMem_readNode(dir, 64ULL * 1024 * 1024, &data); + CHECK(data.active == true); + CHECK(data.limit == 4194304ULL); + CHECK(data.current == 2150400ULL); + CHECK(data.file == 512000ULL); + CHECK(data.swapActive == true); + CHECK(data.swapLimit == 1048576ULL); + CHECK(data.swapCurrent == 10240ULL); + + /* unlimited memory.max → inactive; swap must not flip to cgroup mode either */ + writeFile(dir, "memory.max", "max\n"); + CGroupMemData data2 = {0}; + CGroupMem_readNode(dir, 64ULL * 1024 * 1024, &data2); + CHECK(data2.active == false); + CHECK(data2.swapActive == false); +} + int main(void) { test_parseLimit(); test_parseUsage(); test_parseStat(); test_parseSelfCgroup(); test_parseMountinfo(); + test_readNode(); if (failures) { fprintf(stderr, "%d/%d checks FAILED\n", failures, checks); diff --git a/linux/LinuxMachine.c b/linux/LinuxMachine.c index 916a02a61..9108ac3ad 100644 --- a/linux/LinuxMachine.c +++ b/linux/LinuxMachine.c @@ -30,6 +30,7 @@ in the source distribution for its full text. #include "Settings.h" #include "UsersTable.h" +#include "linux/CGroupMem.h" #include "linux/Compat.h" #include "linux/Platform.h" // needed for GNU/hurd to get PATH_MAX // IWYU pragma: keep @@ -216,6 +217,9 @@ static void LinuxMachine_scanMemoryInfo(LinuxMachine* this) { host->cachedSwap = swapCacheMem; this->zswap.usedZswapComp = zswapCompMem; this->zswap.usedZswapOrig = zswapOrigMem; + + /* host->totalMem is in kB; CGroupMem works in kB. */ + CGroupMem_scan(host->totalMem, &this->cgroupMem); } static void LinuxMachine_scanHugePages(LinuxMachine* this) { diff --git a/linux/LinuxMachine.h b/linux/LinuxMachine.h index efcf8f86f..688f1b636 100644 --- a/linux/LinuxMachine.h +++ b/linux/LinuxMachine.h @@ -11,6 +11,7 @@ in the source distribution for its full text. #include #include "Machine.h" +#include "linux/CGroupMem.h" #include "linux/ZramStats.h" #include "linux/ZswapStats.h" #include "zfs/ZfsArcStats.h" @@ -99,6 +100,7 @@ typedef struct LinuxMachine_ { ZfsArcStats zfs; ZramStats zram; ZswapStats zswap; + CGroupMemData cgroupMem; } LinuxMachine; #ifndef PROCDIR From 2ac359c1d936bf4ae73d5b335221e0f38354f693 Mon Sep 17 00:00:00 2001 From: Konstantinos Samaras-Tsakiris <5130925+Oblynx@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:35:04 +0000 Subject: [PATCH 3/4] linux: add cgroup v2-aware Container Memory and Swap meters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two new meters, ContainerMemory ("Cmem") and ContainerSwap ("Cswp"), that present the container's cgroup v2 limits (collected in the previous commit) and fall back to the host figures when no cgroup limit is in effect. They are plain additional meters — the classic Memory/Swap meters are untouched — so host and container figures can be shown side by side. - linux/ContainerMeter.{c,h}: the two MeterClasses, reusing the classic rendering (MemoryMeter_display / SwapMeter_display + attribute tables). - MemoryMeter/SwapMeter: factor the value-update body into MemoryMeter_updateValuesWith() / SwapMeter_updateValuesWith(), which take the Platform value-setter as a parameter, so the Container meters reuse it instead of duplicating it. The attribute arrays and display functions are exported for that reuse. - linux/Platform.c: Platform_setCGroupMemoryValues / Platform_setCGroupSwapValues fill the meter from LinuxMachine.cgroupMem when a limit is active (memory: total=limit, used=current-file, cache=file, available=limit-current; swap: total=swapLimit, used=swapCurrent) and delegate to the host setters otherwise. Both meters are registered in Platform_meterTypes[]. Assisted-by: Claude Signed-off-by: Konstantinos Samaras-Tsakiris <5130925+Oblynx@users.noreply.github.com> --- Makefile.am | 2 ++ MemoryMeter.c | 12 ++++++--- MemoryMeter.h | 9 +++++++ SwapMeter.c | 12 ++++++--- SwapMeter.h | 9 +++++++ linux/ContainerMeter.c | 61 ++++++++++++++++++++++++++++++++++++++++++ linux/ContainerMeter.h | 17 ++++++++++++ linux/Platform.c | 45 +++++++++++++++++++++++++++++++ linux/Platform.h | 4 +++ 9 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 linux/ContainerMeter.c create mode 100644 linux/ContainerMeter.h diff --git a/Makefile.am b/Makefile.am index a54fc692c..1d1733a92 100644 --- a/Makefile.am +++ b/Makefile.am @@ -191,6 +191,7 @@ linux_platform_headers = \ linux/CGroupMem.h \ linux/CGroupUtils.h \ linux/Compat.h \ + linux/ContainerMeter.h \ linux/GPU.h \ linux/HugePageMeter.h \ linux/IOPriority.h \ @@ -220,6 +221,7 @@ linux_platform_sources = \ linux/CGroupMem.c \ linux/CGroupUtils.c \ linux/Compat.c \ + linux/ContainerMeter.c \ linux/GPU.c \ linux/HugePageMeter.c \ linux/IOPriorityPanel.c \ diff --git a/MemoryMeter.c b/MemoryMeter.c index 296966cc1..e65ba0d92 100644 --- a/MemoryMeter.c +++ b/MemoryMeter.c @@ -21,7 +21,7 @@ in the source distribution for its full text. #include "RichString.h" -static const int MemoryMeter_attributes[] = { +const int MemoryMeter_attributes[] = { MEMORY_1, MEMORY_2, MEMORY_3, @@ -30,7 +30,7 @@ static const int MemoryMeter_attributes[] = { MEMORY_6 }; -static void MemoryMeter_updateValues(Meter* this) { +void MemoryMeter_updateValuesWith(Meter* this, void (*setValues)(Meter*)) { char* buffer = this->txtBuffer; size_t size = sizeof(this->txtBuffer); int written; @@ -42,7 +42,7 @@ static void MemoryMeter_updateValues(Meter* this) { this->values[memoryClassIdx] = NAN; } - Platform_setMemoryValues(this); + setValues(this); this->curItems = (uint8_t) Platform_numberOfMemoryClasses; /* compute the used memory */ @@ -70,7 +70,11 @@ static void MemoryMeter_updateValues(Meter* this) { Meter_humanUnit(buffer, this->total, size); } -static void MemoryMeter_display(const Object* cast, RichString* out) { +static void MemoryMeter_updateValues(Meter* this) { + MemoryMeter_updateValuesWith(this, Platform_setMemoryValues); +} + +void MemoryMeter_display(const Object* cast, RichString* out) { char buffer[50]; const Meter* this = (const Meter*)cast; const Settings* settings = this->host->settings; diff --git a/MemoryMeter.h b/MemoryMeter.h index 3a8b967bb..5f033af14 100644 --- a/MemoryMeter.h +++ b/MemoryMeter.h @@ -9,6 +9,9 @@ in the source distribution for its full text. */ #include "Meter.h" +#include "Object.h" +#include "RichString.h" + typedef struct MemoryClass_s { const char *label; // e.g. "wired", "shared", "compressed" - platform-specific memory classe names @@ -19,4 +22,10 @@ typedef struct MemoryClass_s { extern const MeterClass MemoryMeter_class; +extern const int MemoryMeter_attributes[]; + +void MemoryMeter_display(const Object* cast, RichString* out); + +void MemoryMeter_updateValuesWith(Meter* this, void (*setValues)(Meter*)); + #endif diff --git a/SwapMeter.c b/SwapMeter.c index faa194556..9bebc3ace 100644 --- a/SwapMeter.c +++ b/SwapMeter.c @@ -19,20 +19,20 @@ in the source distribution for its full text. #include "RichString.h" -static const int SwapMeter_attributes[] = { +const int SwapMeter_attributes[] = { SWAP, SWAP_CACHE, SWAP_FRONTSWAP, }; -static void SwapMeter_updateValues(Meter* this) { +void SwapMeter_updateValuesWith(Meter* this, void (*setValues)(Meter*)) { char* buffer = this->txtBuffer; size_t size = sizeof(this->txtBuffer); int written; this->values[SWAP_METER_CACHE] = NAN; /* 'cached' not present on all platforms */ this->values[SWAP_METER_FRONTSWAP] = NAN; /* 'frontswap' not present on all platforms */ - Platform_setSwapValues(this); + setValues(this); written = Meter_humanUnit(buffer, this->values[SWAP_METER_USED], size); METER_BUFFER_CHECK(buffer, size, written); @@ -42,7 +42,11 @@ static void SwapMeter_updateValues(Meter* this) { Meter_humanUnit(buffer, this->total, size); } -static void SwapMeter_display(const Object* cast, RichString* out) { +static void SwapMeter_updateValues(Meter* this) { + SwapMeter_updateValuesWith(this, Platform_setSwapValues); +} + +void SwapMeter_display(const Object* cast, RichString* out) { char buffer[50]; const Meter* this = (const Meter*)cast; RichString_writeAscii(out, CRT_colors[METER_TEXT], ":"); diff --git a/SwapMeter.h b/SwapMeter.h index 94b8dc859..d6e1ca961 100644 --- a/SwapMeter.h +++ b/SwapMeter.h @@ -8,6 +8,9 @@ in the source distribution for its full text. */ #include "Meter.h" +#include "Object.h" +#include "RichString.h" + typedef enum { SWAP_METER_USED = 0, @@ -18,4 +21,10 @@ typedef enum { extern const MeterClass SwapMeter_class; +extern const int SwapMeter_attributes[]; + +void SwapMeter_display(const Object* cast, RichString* out); + +void SwapMeter_updateValuesWith(Meter* this, void (*setValues)(Meter*)); + #endif diff --git a/linux/ContainerMeter.c b/linux/ContainerMeter.c new file mode 100644 index 000000000..5a91656f7 --- /dev/null +++ b/linux/ContainerMeter.c @@ -0,0 +1,61 @@ +/* +htop - linux/ContainerMeter.c +(C) 2025 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "config.h" // IWYU pragma: keep + +#include "linux/ContainerMeter.h" + +#include "MemoryMeter.h" +#include "Meter.h" +#include "Object.h" +#include "Platform.h" +#include "SwapMeter.h" + + +static void ContainerMemoryMeter_updateValues(Meter* this) { + MemoryMeter_updateValuesWith(this, Platform_setCGroupMemoryValues); +} + +const MeterClass ContainerMemoryMeter_class = { + .super = { + .extends = Class(Meter), + .delete = Meter_delete, + .display = MemoryMeter_display, + }, + .updateValues = ContainerMemoryMeter_updateValues, + .defaultMode = BAR_METERMODE, + .supportedModes = METERMODE_DEFAULT_SUPPORTED, + .maxItems = 6, // maximum of MEMORY_N settings + .isPercentChart = true, + .total = 100.0, + .attributes = MemoryMeter_attributes, + .name = "ContainerMemory", + .uiName = "Container memory", + .caption = "Mem" +}; + +static void ContainerSwapMeter_updateValues(Meter* this) { + SwapMeter_updateValuesWith(this, Platform_setCGroupSwapValues); +} + +const MeterClass ContainerSwapMeter_class = { + .super = { + .extends = Class(Meter), + .delete = Meter_delete, + .display = SwapMeter_display, + }, + .updateValues = ContainerSwapMeter_updateValues, + .defaultMode = BAR_METERMODE, + .supportedModes = METERMODE_DEFAULT_SUPPORTED, + .maxItems = SWAP_METER_ITEMCOUNT, + .isPercentChart = true, + .total = 100.0, + .attributes = SwapMeter_attributes, + .name = "ContainerSwap", + .uiName = "Container swap", + .caption = "Swp" +}; diff --git a/linux/ContainerMeter.h b/linux/ContainerMeter.h new file mode 100644 index 000000000..2b75eb35a --- /dev/null +++ b/linux/ContainerMeter.h @@ -0,0 +1,17 @@ +#ifndef HEADER_ContainerMeter +#define HEADER_ContainerMeter +/* +htop - linux/ContainerMeter.h +(C) 2025 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "Meter.h" + + +extern const MeterClass ContainerMemoryMeter_class; + +extern const MeterClass ContainerSwapMeter_class; + +#endif /* HEADER_ContainerMeter */ diff --git a/linux/Platform.c b/linux/Platform.c index cecaf3b5d..c1a204be6 100644 --- a/linux/Platform.c +++ b/linux/Platform.c @@ -49,6 +49,7 @@ in the source distribution for its full text. #include "TasksMeter.h" #include "UptimeMeter.h" #include "linux/Compat.h" +#include "linux/ContainerMeter.h" #include "linux/IOPriority.h" #include "linux/IOPriorityPanel.h" #include "linux/LinuxMachine.h" @@ -236,6 +237,8 @@ const MeterClass* const Platform_meterTypes[] = { &LoadMeter_class, &MemoryMeter_class, &SwapMeter_class, + &ContainerMemoryMeter_class, + &ContainerSwapMeter_class, &MemorySwapMeter_class, &SysArchMeter_class, &HugePageMeter_class, @@ -496,6 +499,48 @@ void Platform_setSwapValues(Meter* this) { } } +void Platform_setCGroupMemoryValues(Meter* this) { + const Machine* host = this->host; + const LinuxMachine* lhost = (const LinuxMachine*) host; + + if (!lhost->cgroupMem.active) { + /* No cgroup v2 limit detected: fall back to the classic host numbers. */ + Platform_setMemoryValues(this); + return; + } + + /* cgroup v2 mode: total = limit; used = current - file (anon+kernel); cache = file. */ + uint64_t limit = lhost->cgroupMem.limit; + uint64_t current = lhost->cgroupMem.current; + uint64_t file = lhost->cgroupMem.file; + uint64_t used = (current > file) ? (current - file) : 0; + + this->total = limit; + this->values[MEMORY_CLASS_USED] = used; + this->values[MEMORY_CLASS_SHARED] = 0; + this->values[MEMORY_CLASS_COMPRESSED] = 0; + this->values[MEMORY_CLASS_BUFFERS] = 0; + this->values[MEMORY_CLASS_CACHE] = file; + this->values[MEMORY_CLASS_AVAILABLE] = (limit > current) ? (limit - current) : 0; +} + +void Platform_setCGroupSwapValues(Meter* this) { + const Machine* host = this->host; + const LinuxMachine* lhost = (const LinuxMachine*) host; + + if (!lhost->cgroupMem.swapActive) { + /* No cgroup v2 swap limit detected: fall back to the classic host numbers. */ + Platform_setSwapValues(this); + return; + } + + /* cgroup v2 swap: total = memory.swap.max (may be 0 = disabled -> empty bar). */ + this->total = lhost->cgroupMem.swapLimit; + this->values[SWAP_METER_USED] = lhost->cgroupMem.swapCurrent; + this->values[SWAP_METER_CACHE] = NAN; /* not represented per-cgroup */ + this->values[SWAP_METER_FRONTSWAP] = NAN; +} + void Platform_setZramValues(Meter* this) { const LinuxMachine* lhost = (const LinuxMachine*) this->host; diff --git a/linux/Platform.h b/linux/Platform.h index d91856342..8e69fd3b2 100644 --- a/linux/Platform.h +++ b/linux/Platform.h @@ -74,6 +74,10 @@ void Platform_setMemoryValues(Meter* this); void Platform_setSwapValues(Meter* this); +void Platform_setCGroupMemoryValues(Meter* this); + +void Platform_setCGroupSwapValues(Meter* this); + void Platform_setZramValues(Meter* this); void Platform_setZfsArcValues(Meter* this); From d19ae8bff54aa90b6c55bb7f0f1484c7551b4beb Mon Sep 17 00:00:00 2001 From: Konstantinos Samaras-Tsakiris <5130925+Oblynx@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:35:04 +0000 Subject: [PATCH 4/4] Default to the Container meters when running containerized MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When htop starts without a configuration and detects it is running inside a container, seed the default header layout with the ContainerMemory and ContainerSwap meters instead of the classic Memory/Swap ones, so a containerized user sees the limits that actually apply with no setup and no extra setting. Containerization is exposed generically as Machine.containerized (set from the Linux Running_containerized probe in LinuxMachine_new; false on every other platform), so the cross-platform Settings_defaultMeters can pick the meter names without depending on Linux-only symbols — the Container* names are only ever selected where those meters are registered. The ordering is safe: Platform_init() detects containerization before Machine_new() copies the flag and Settings_new() seeds the defaults. Assisted-by: Claude Signed-off-by: Konstantinos Samaras-Tsakiris <5130925+Oblynx@users.noreply.github.com> --- Machine.c | 2 ++ Machine.h | 2 ++ Settings.c | 9 +++++++-- linux/LinuxMachine.c | 3 +++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Machine.c b/Machine.c index cf824b499..43ddcdbcd 100644 --- a/Machine.c +++ b/Machine.c @@ -25,6 +25,8 @@ void Machine_init(Machine* this, UsersTable* usersTable, uid_t userId) { this->htopUserId = getuid(); + this->containerized = false; + // discover fixed column width limits Row_setPidColumnWidth(Platform_getMaxPid()); diff --git a/Machine.h b/Machine.h index d7c71b94b..2d2e47a21 100644 --- a/Machine.h +++ b/Machine.h @@ -59,6 +59,8 @@ typedef struct Machine_ { unsigned int activeCPUs; unsigned int existingCPUs; + bool containerized; /* whether htop is running inside a container (Linux only; always false elsewhere) */ + UsersTable* usersTable; uid_t htopUserId; uid_t maxUserId; /* recently observed */ diff --git a/Settings.c b/Settings.c index 683abac34..997a28f79 100644 --- a/Settings.c +++ b/Settings.c @@ -166,9 +166,14 @@ static void Settings_defaultMeters(Settings* this, const Machine* host) { this->hColumns[0].names[0] = xStrdup("AllCPUs"); this->hColumns[0].modes[0] = BAR_METERMODE; } - this->hColumns[0].names[1] = xStrdup("Memory"); + // When containerized, default to the cgroup-aware meters so the limits + // imposed on the container are visible. The "Container*" meters fall back + // to the host figures when no cgroup limit is in effect. + // host->containerized is always false off Linux, so the literal meter + // names below are only ever used on a platform that registers them. + this->hColumns[0].names[1] = xStrdup(host->containerized ? "ContainerMemory" : "Memory"); this->hColumns[0].modes[1] = BAR_METERMODE; - this->hColumns[0].names[2] = xStrdup("Swap"); + this->hColumns[0].names[2] = xStrdup(host->containerized ? "ContainerSwap" : "Swap"); this->hColumns[0].modes[2] = BAR_METERMODE; this->hColumns[1].names[r] = xStrdup("Tasks"); this->hColumns[1].modes[r++] = TEXT_METERMODE; diff --git a/linux/LinuxMachine.c b/linux/LinuxMachine.c index 9108ac3ad..2476bdc2e 100644 --- a/linux/LinuxMachine.c +++ b/linux/LinuxMachine.c @@ -830,6 +830,9 @@ Machine* Machine_new(UsersTable* usersTable, uid_t userId) { Machine_init(super, usersTable, userId); + // Platform_init() has already probed containerization by this point + super->containerized = Running_containerized; + // Initialize page size long pageSize = sysconf(_SC_PAGESIZE); if (pageSize <= 0)