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/Makefile.am b/Makefile.am index 52134df64..1d1733a92 100644 --- a/Makefile.am +++ b/Makefile.am @@ -188,8 +188,10 @@ linux_platform_headers = \ generic/gettime.h \ generic/hostname.h \ generic/uname.h \ + linux/CGroupMem.h \ linux/CGroupUtils.h \ linux/Compat.h \ + linux/ContainerMeter.h \ linux/GPU.h \ linux/HugePageMeter.h \ linux/IOPriority.h \ @@ -216,8 +218,10 @@ linux_platform_sources = \ generic/gettime.c \ generic/hostname.c \ generic/uname.c \ + linux/CGroupMem.c \ linux/CGroupUtils.c \ linux/Compat.c \ + linux/ContainerMeter.c \ linux/GPU.c \ linux/HugePageMeter.c \ linux/IOPriorityPanel.c \ @@ -527,6 +531,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/Compat.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/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/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/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/CGroupMem.c b/linux/CGroupMem.c new file mode 100644 index 000000000..e7f5f63a8 --- /dev/null +++ b/linux/CGroupMem.c @@ -0,0 +1,231 @@ +/* +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 +#include +#include +#include + +#include "Macros.h" +#include "XUtils.h" + +#include "linux/Compat.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; +} + +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 new file mode 100644 index 000000000..2d56f74e4 --- /dev/null +++ b/linux/CGroupMem.h @@ -0,0 +1,51 @@ +#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 + + +/* 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 + "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); + +/* 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 new file mode 100644 index 000000000..77fb3ff10 --- /dev/null +++ b/linux/CGroupMemTest.c @@ -0,0 +1,147 @@ +/* +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 +#include + + +/* XUtils' allocation-failure path references CRT_done(); this test links XUtils.o + 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) { } + +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); +} + +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); + return 1; + } + printf("All %d checks passed\n", checks); + return 0; +} 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/LinuxMachine.c b/linux/LinuxMachine.c index 916a02a61..2476bdc2e 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) { @@ -826,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) 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 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);