From dbadce041d69d8090da16af2255f57850a5bfb43 Mon Sep 17 00:00:00 2001 From: Chunsheng Luo Date: Wed, 3 Sep 2025 16:50:02 +0800 Subject: [PATCH 1/7] library: Add support for meminfo source type Added a stype field to the struct meminfo_info to explicitly store the source type of meminfo data. Additionally, extracted the meminfo buffer reading logic into a dedicated procps_meminfo_read_buf. Signed-off-by: Chunsheng Luo --- library/meminfo.c | 105 ++++++++++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 41 deletions(-) diff --git a/library/meminfo.c b/library/meminfo.c index 59d0e0b7..e215465d 100644 --- a/library/meminfo.c +++ b/library/meminfo.c @@ -130,6 +130,10 @@ struct stacks_extent { struct meminfo_stack **stacks; }; +enum meminfo_source_type { + PROC_MEMINFO_TYPE = 0, +}; + struct meminfo_info { int refcount; int meminfo_fd; @@ -140,6 +144,7 @@ struct meminfo_info { struct hsearch_data hashtab; struct meminfo_result get_this; time_t sav_secs; + enum meminfo_source_type stype; }; @@ -648,29 +653,8 @@ static int meminfo_make_hash_failed ( #undef htXTRA } // end: meminfo_make_hash_failed - -/* - * meminfo_read_failed(): - * - * Read the data out of /proc/meminfo putting the information - * into the supplied info structure - */ -static int meminfo_read_failed ( - struct meminfo_info *info) -{ - /* a 'memory history reference' macro for readability, - so we can focus the field names ... */ - #define mHr(f) info->hist.new. f - char buf[MEMINFO_BUFF]; - char *head, *tail; +static int procps_meminfo_read_buf(struct meminfo_info *info, char *buf, int len) { int size; - unsigned long *valptr; - signed long mem_used; - - // remember history from last time around - memcpy(&info->hist.old, &info->hist.new, sizeof(struct meminfo_data)); - // clear out the soon to be 'current' values - memset(&info->hist.new, 0, sizeof(struct meminfo_data)); if (-1 == info->meminfo_fd && (-1 == (info->meminfo_fd = open(MEMINFO_FILE, O_RDONLY)))) @@ -688,7 +672,7 @@ static int meminfo_read_failed ( } for (;;) { - if ((size = read(info->meminfo_fd, buf, sizeof(buf)-1)) < 0) { + if ((size = read(info->meminfo_fd, buf, len-1)) < 0) { if (errno == EINTR || errno == EAGAIN) continue; return 1; @@ -700,6 +684,42 @@ static int meminfo_read_failed ( return 1; } buf[size] = '\0'; + return 0; +} + +/* + * meminfo_read_failed(): + * + * Read the data out of /proc/meminfo putting the information + * into the supplied info structure + */ +static int meminfo_read_failed ( + struct meminfo_info *info) +{ + /* a 'memory history reference' macro for readability, + so we can focus the field names ... */ + #define mHr(f) info->hist.new. f + char buf[MEMINFO_BUFF]; + char *head, *tail; + int ret = -1; + enum meminfo_source_type stype = info->stype; + unsigned long *valptr; + signed long mem_used; + + if (stype != PROC_MEMINFO_TYPE) + return ret; + + // remember history from last time around + memcpy(&info->hist.old, &info->hist.new, sizeof(struct meminfo_data)); + // clear out the soon to be 'current' values + memset(&info->hist.new, 0, sizeof(struct meminfo_data)); + + if (stype == PROC_MEMINFO_TYPE) { + ret = procps_meminfo_read_buf(info, buf, sizeof(buf)); + } + + if (ret != 0) + return ret; head = buf; @@ -811,24 +831,8 @@ static struct stacks_extent *meminfo_stacks_alloc ( return p_blob; } // end: meminfo_stacks_alloc - -// ___ Public Functions ||||||||||||||||||||||||||||||||||||||||||||||||||||||| - -// --- standard required functions -------------------------------------------- - -/* - * procps_meminfo_new: - * - * Create a new container to hold the stat information - * - * The initial refcount is 1, and needs to be decremented - * to release the resources of the structure. - * - * Returns: < 0 on failure, 0 on success along with - * a pointer to a new context struct - */ -PROCPS_EXPORT int procps_meminfo_new ( - struct meminfo_info **info) +static int meminfo_new_internal (struct meminfo_info **info, + enum meminfo_source_type type) { struct meminfo_info *p; @@ -857,6 +861,7 @@ PROCPS_EXPORT int procps_meminfo_new ( return -errno; } + p->stype = type; /* do a priming read here for the following potential benefits: | 1) ensure there will be no problems with subsequent access | 2) make delta results potentially useful, even if 1st time | @@ -870,6 +875,24 @@ PROCPS_EXPORT int procps_meminfo_new ( return 0; } // end: procps_meminfo_new +// ___ Public Functions ||||||||||||||||||||||||||||||||||||||||||||||||||||||| + +// --- standard required functions -------------------------------------------- +/* + * procps_meminfo_new: + * + * Create a new container to hold the stat information + * + * The initial refcount is 1, and needs to be decremented + * to release the resources of the structure. + * + * Returns: < 0 on failure, 0 on success along with + * a pointer to a new context struct + */ +PROCPS_EXPORT int procps_meminfo_new ( + struct meminfo_info **info){ + return meminfo_new_internal(info, PROC_MEMINFO_TYPE); +} PROCPS_EXPORT int procps_meminfo_ref ( struct meminfo_info *info) From 3df863d2bf16c05558ff3d1da0a7bf123a404010 Mon Sep 17 00:00:00 2001 From: Chunsheng Luo Date: Wed, 3 Sep 2025 16:58:36 +0800 Subject: [PATCH 2/7] library: Add cgroup memory info support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces comprehensive cgroup memory information support, enabling the library to read memory limits, usage, swap, and cache statistics from cgroup v1 and v2 hierarchies. The implementation includes automatic cgroup version detection, path traversal to locate the memory controller, and parsing of memory statistics including active/inactive memory, slab information, and swap usage. The module provides a unified interface to access container memory metrics while maintaining compatibility with traditional /proc/meminfo format。 Signed-off-by: Chunsheng Luo --- Makefile.am | 1 + library/cgmeminfo.c | 820 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 821 insertions(+) create mode 100644 library/cgmeminfo.c diff --git a/Makefile.am b/Makefile.am index 508c7c9c..a573405e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -272,6 +272,7 @@ library_libproc2_la_LDFLAGS = \ library_libproc2_la_SOURCES = \ library/capname.c \ local/capnames.h \ + library/cgmeminfo.c \ library/devname.c \ library/include/devname.h \ library/diskstats.c \ diff --git a/library/cgmeminfo.c b/library/cgmeminfo.c new file mode 100644 index 00000000..82502989 --- /dev/null +++ b/library/cgmeminfo.c @@ -0,0 +1,820 @@ +/* + * cgmeminfo.c - cgroup memory information functions + * + * Copyright © 2025 Chunsheng Luo + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "include/meminfo.h" + + +/* Conversion constants */ +#define BYTES_TO_KB 1024 +#define CGMEMINFO_LEN 8192 + +/* Cgroup version types */ +#define CGROUP_TYPE_UNKNOWN (0) +#define CGROUP_TYPE_LEGACY (1 << 0) +#define CGROUP_TYPE_UNIFIED (1 << 1) +#define CGROUP_TYPE_HYBRID (CGROUP_TYPE_LEGACY | CGROUP_TYPE_UNIFIED) + +struct memory_stat { + unsigned long total_cache; + unsigned long total_rss; /* not used now */ + unsigned long total_rss_huge; + unsigned long total_shmem; + unsigned long total_mapped_file; + unsigned long total_dirty; + unsigned long total_writeback; + unsigned long total_inactive_anon; + unsigned long total_active_anon; + unsigned long total_inactive_file; + unsigned long total_active_file; + unsigned long total_unevictable; + unsigned long slab_reclaimable; + unsigned long slab_unreclaimable; + unsigned long slab; +}; + +struct memcg_data { + unsigned long memory_limit; /* v1: memory.limit_in_bytes, v2: memory.max */ + unsigned long memory_current; /* v1: memory.usage_in_bytes, v2: memory.current */ + unsigned long swap_limit; /* v1: memory.memsw.limit_in_bytes, v2: memory.swap.max */ + unsigned long swap_current; /* v1: memory.memsw.usage_in_bytes, v2: memory.swap.current */ + struct memory_stat memory_stat; /* v1: memory.stat, v2: memory.stat */ +}; + +struct memcg_meminfo { + int version; + int refcount; + char *cgmem_mount; + char *cgmem_path; + struct memcg_data cgmem_data; +}; + +/** + * Clean up memcg_meminfo structure resources + * @info: memcg_meminfo structure to clean up + */ +static void cleanup_memcg_info(struct memcg_meminfo *info) { + if (!info) return; + + if (info->cgmem_mount) { + free(info->cgmem_mount); + info->cgmem_mount = NULL; + } + + if (info->cgmem_path) { + free(info->cgmem_path); + info->cgmem_path = NULL; + } +} + +/** + * Read all content from a file into buffer + * @path: file path to read from + * @buf: buffer to store the content + * @buf_size: size of the buffer + * + * Returns: 0 on success, other on error + */ +static int read_from_file(const char *path, char *buf, size_t buf_size) { + FILE *fp = NULL; + size_t bytes_read; + int ret; + + if (!path || !buf || buf_size == 0) { + return EINVAL; + } + + fp = fopen(path, "r"); + if (!fp) { + return errno; + } + + memset(buf, 0, buf_size); + + bytes_read = fread(buf, 1, buf_size - 1, fp); + if (ferror(fp)) { + ret = errno ? errno : EIO; + goto cleanup; + } + + buf[bytes_read] = '\0'; + ret = 0; + +cleanup: + if (fp) + fclose(fp); + + return ret; +} + +static char *cgroup_mount(int version) { + FILE *fp; + char *ret = NULL; + struct mntent *mnt; + + if (!(fp = setmntent("/proc/mounts", "r"))) { + return NULL; + } + + while ((mnt = getmntent(fp)) != NULL) { + if (version == CGROUP_TYPE_UNIFIED + && strcmp(mnt->mnt_type, "cgroup2") == 0) { + if (!(ret = strdup(mnt->mnt_dir))) + break; + break; + } else if (version == CGROUP_TYPE_LEGACY + && strcmp(mnt->mnt_type, "cgroup") == 0 + && strstr(mnt->mnt_opts, "memory") != NULL) { + if (!(ret = strdup(mnt->mnt_dir))) + break; + break; + } + } + + endmntent(fp); + return ret; +} + +enum memcg_metric_type { + MEMCG_MEMORY_LIMIT = 0, + MEMCG_MEMORY_CURRENT = 1, + MEMCG_SWAP_LIMIT = 2, + MEMCG_SWAP_CURRENT = 3, + MEMCG_MEMORY_STAT = 4 +}; + +struct memcg_file_mapping { + const char *v1_file; + const char *v2_file; +}; + +/* Define file mappings for different memory metrics */ +static const struct memcg_file_mapping file_mappings[] = { + [MEMCG_MEMORY_LIMIT] = { "memory.limit_in_bytes", "memory.max" }, + [MEMCG_MEMORY_CURRENT] = { "memory.usage_in_bytes", "memory.current" }, + [MEMCG_SWAP_LIMIT] = { "memory.memsw.limit_in_bytes", "memory.swap.max" }, + [MEMCG_SWAP_CURRENT] = { "memory.memsw.usage_in_bytes", "memory.swap.current" }, + [MEMCG_MEMORY_STAT] = { "memory.stat", "memory.stat" } +}; + +static int memcg_parse_memory_stat(char *buf, struct memory_stat *stat, int cgroup_version) { + /* Initialize all fields to 0 */ + memset(stat, 0, sizeof(struct memory_stat)); + +#define sTv(f) &stat->f + /* Define mapping table for memory statistics */ + struct { + const char *v1_key; + const char *v2_key; + unsigned long *field; + } stat_mappings[] = { + { "total_cache", "file", sTv(total_cache) }, + { "total_rss_huge", NULL, sTv(total_rss_huge) }, /* v1 only */ + { "total_shmem", "shmem", sTv(total_shmem) }, + { "total_mapped_file", "file_mapped", sTv(total_mapped_file) }, + { "total_dirty", "file_dirty", sTv(total_dirty) }, + { "total_writeback", "file_writeback", sTv(total_writeback) }, + { "total_inactive_anon", "inactive_anon", sTv(total_inactive_anon) }, + { "total_active_anon", "active_anon", sTv(total_active_anon) }, + { "total_inactive_file", "inactive_file", sTv(total_inactive_file) }, + { "total_active_file", "active_file", sTv(total_active_file) }, + { "total_unevictable", "unevictable", sTv(total_unevictable) }, + { NULL, "slab_reclaimable", sTv(slab_reclaimable) }, /* v2 only */ + { NULL, "slab_unreclaimable", sTv(slab_unreclaimable) }, /* v2 only */ + { NULL, "slab", sTv(slab) }, /* v2 only */ + { NULL, NULL, NULL } /* End marker */ + }; + + char *line = buf; + char *next_line; + + while (line && *line) { + next_line = strchr(line, '\n'); + if (next_line) { + *next_line = '\0'; + next_line++; + } + + char stat_key[64]; + unsigned long stat_value; + if (sscanf(line, "%63s %lu", stat_key, &stat_value) == 2) { + for (int i = 0; stat_mappings[i].field != NULL; i++) { + const char *key = NULL; + + if (cgroup_version & CGROUP_TYPE_LEGACY) + key = stat_mappings[i].v1_key; + else + key = stat_mappings[i].v2_key; + + if (key && strcmp(stat_key, key) == 0) { + *(stat_mappings[i].field) = stat_value; + break; + } + } + } + + if (next_line && next_line > line) { + *(next_line - 1) = '\n'; + } + + line = next_line; + } + +#undef sTv + return 0; +} + +/** + * Common helper function to build cgroup metric file path + * @info: memcg_meminfo structure + * @metric_type: type of metric to read + * @path_buffer: buffer to store the constructed path + * @buffer_size: size of the path buffer + * + * Returns: 0 on success, other on error + */ +static inline int memcg_build_file_path(struct memcg_meminfo *info, + enum memcg_metric_type metric_type, + char *path_buffer, + size_t buffer_size) { + if (metric_type >= sizeof(file_mappings)/sizeof(file_mappings[0])) + return 1; + + const char *filename = (info->version & CGROUP_TYPE_LEGACY) + ? file_mappings[metric_type].v1_file + : file_mappings[metric_type].v2_file; + + if (!filename) + return 1; + + int ret = snprintf(path_buffer, buffer_size, "%s%s/%s", + info->cgmem_mount, info->cgmem_path, filename); + if (ret < 0 || ret >= (int)buffer_size) + return 1; + + return 0; +} + +/** + * Generic function to read memory metric from cgroup file + * @info: memcg_meminfo structure + * @metric_type: type of metric to read + * + * Returns: 0 on success, other on error + */ +static int memcg_read_metric(struct memcg_meminfo *info, + enum memcg_metric_type metric_type) { + char path[PATH_MAX]; + char *buf = NULL; + int ret; + unsigned long result = 0; + + if (memcg_build_file_path(info, metric_type, path, sizeof(path)) != 0) + return 1; + + buf = (char *)malloc(CGMEMINFO_LEN); + if (!buf) + return ENOMEM; + + ret = read_from_file(path, buf, CGMEMINFO_LEN); + if (ret != 0) { + goto out; + } + + /* For memory.stat, contents are key-value, so parsing is handled separately */ + if (metric_type != MEMCG_MEMORY_STAT) { + /* cgroup v2 memory.max can return "max", so we need to handle that. */ + if (sscanf(buf, "%lu", &result) != 1) { + result = ULLONG_MAX; + } + } + + ret = EINVAL; + switch (metric_type) { + case MEMCG_MEMORY_LIMIT: + info->cgmem_data.memory_limit = result; + break; + case MEMCG_MEMORY_CURRENT: + info->cgmem_data.memory_current = result; + break; + case MEMCG_SWAP_LIMIT: + if (info->version & CGROUP_TYPE_LEGACY) { + if (info->cgmem_data.memory_limit > result) { + result = 0; + } else { + result -= info->cgmem_data.memory_limit; + } + } + info->cgmem_data.swap_limit = result; + break; + case MEMCG_SWAP_CURRENT: + if (info->version & CGROUP_TYPE_LEGACY) { + if (info->cgmem_data.memory_current > result + || info->cgmem_data.swap_limit == 0) { + result = 0; + } else { + result -= info->cgmem_data.memory_current; + } + } + info->cgmem_data.swap_current = result; + break; + case MEMCG_MEMORY_STAT: + memcg_parse_memory_stat(buf, &info->cgmem_data.memory_stat, info->version); + break; + default: + goto out; + } + ret = 0; + +out: + free(buf); + return ret; +} + +static int memcg_get_memory_limit(struct memcg_meminfo *info) { + return memcg_read_metric(info, MEMCG_MEMORY_LIMIT); +} + +static int memcg_get_memory_current(struct memcg_meminfo *info) { + return memcg_read_metric(info, MEMCG_MEMORY_CURRENT); +} + +static int memcg_get_swap_limit(struct memcg_meminfo *info) { + return memcg_read_metric(info, MEMCG_SWAP_LIMIT); +} + +static int memcg_get_swap_current(struct memcg_meminfo *info) { + return memcg_read_metric(info, MEMCG_SWAP_CURRENT); +} + +static int memcg_get_memory_stat(struct memcg_meminfo *info) { + return memcg_read_metric(info, MEMCG_MEMORY_STAT); +} + +/* + * Check if current process exists in cgroup tasks + * @cgroup_version: cgroup version flags (CGROUP_TYPE_LEGACY or CGROUP_TYPE_UNIFIED) + * @cgroup_mount: mount point of the cgroup + * @cgroup_path: path to the cgroup + * + * Returns: true if current process exists in cgroup tasks, false otherwise + */ +static bool memcg_process_in_cgroup_tasks(int cgroup_type, + const char* cgmount, const char *path) { + pid_t current_pid = getpid(); + char tasks_file[PATH_MAX]; + FILE *fp; + pid_t task_pid; + bool found = false; + char *line = NULL; + size_t line_len = 0; + ssize_t rlen; + + if (cgroup_type & CGROUP_TYPE_UNIFIED) { + snprintf(tasks_file, sizeof(tasks_file), "%s%s/cgroup.procs", cgmount, path); + } else if (cgroup_type & CGROUP_TYPE_LEGACY) { + snprintf(tasks_file, sizeof(tasks_file), "%s%s/tasks", cgmount, path); + } + + fp = fopen(tasks_file, "r"); + if (!fp) + return false; + + /* Search for current process ID in the file */ + while ((rlen = getline(&line, &line_len, fp)) != -1) { + if (sscanf(line, "%d", &task_pid) == 1) { + if (task_pid == current_pid) { + found = true; + break; + } + } + } + free(line); + fclose(fp); + + return found; +} + +/** + * Remove the first layer of a path (e.g., "/a/b/c" -> "/b/c") + * @path: Input path to modify (non-NULL, and not a string constant) + + * Note: The input path string will be modified in-place. + * For root path ("/"), returns NULL as no layer can be removed. + * + * Returns: Pointer to the modified path, or NULL if path is "/" + */ +static char *remove_path_layer(char *path) { + if (strcmp(path, "/") == 0) { + return NULL; + } + + char *first_slash = strchr(path + 1, '/'); + if (first_slash) { + memmove(path, first_slash, strlen(first_slash) + 1); + } else { + strcpy(path, "/"); + } + return path; +} + +/* + * Traverse cgroup path and find real memory cgroup path + * @path: path to the cgroup directory + * @cgroup_type: cgroup version flags + * @info: pointer to memcg_meminfo structure to populate + * @found: pointer to bool to indicate if cgroup was found + * + * Returns: 0 on success, >0 on error + */ +static int traverse_cgroup_path(const char *path, int cgroup_type, + struct memcg_meminfo *info, bool *found) { + char *current_path = NULL; + char *cgmount = NULL; + int ret = 0; + + *found = false; + + if (!(current_path = strdup(path))) + return ENOMEM; + + cgmount = cgroup_mount(cgroup_type); + if (!cgmount) { + free(current_path); + return ENOENT; + } + + while (current_path && strlen(current_path) > 0) { + if (memcg_process_in_cgroup_tasks(cgroup_type, cgmount, current_path)) { + info->version = cgroup_type; + if (!(info->cgmem_path = strdup(current_path))) { + ret = ENOMEM; + goto out; + } + info->cgmem_mount = cgmount; + cgmount = NULL; + *found = true; + break; + } + current_path = remove_path_layer(current_path); + } + +out: + free(current_path); + free(cgmount); + + return ret; +} + +static int parse_thread_cgroup_paths(char **v1_path, char **v2_path) { + FILE *fp; + char *line = NULL; + size_t line_len = 0; + ssize_t rlen; + int ret = 0; + + /* Check cgroup version by reading /proc/self/cgroup */ + fp = fopen("/proc/self/cgroup", "r"); + if (!fp) { + return errno; + } + + while ((rlen = getline(&line, &line_len, fp)) != -1) { + char *newline = strchr(line, '\n'); + if (newline) *newline = '\0'; + + if (strncmp(line, "0::", 3) == 0) { + /* cgroup v2 format: "0::/user.slice/user-0.slice" */ + if (!(*v2_path = strdup(line + 3))) { + ret = ENOMEM; + goto cleanup; + } + } else { + /* cgroup v1 format: "13:memory:/user.slice/user-0.slice/" */ + char *first_colon = strchr(line, ':'); + if (first_colon) { + char *second_colon = strchr(first_colon + 1, ':'); + if (second_colon) { + *second_colon = '\0'; + /* Check if subsystems contain "memory" */ + if (strstr(first_colon + 1, "memory")) { + char *path_start = second_colon + 1; + char *newline = strchr(path_start, '\n'); + if (newline) { + *newline = '\0'; + } + if (!(*v1_path = strdup(path_start))) { + ret = ENOMEM; + goto cleanup; + } + } + + *second_colon = ':'; + } + } + } + } + +cleanup: + if (line) + free(line); + + if (fp) + fclose(fp); + + return ret; +} + +static int memcg_get_memory_info(struct memcg_meminfo *info) { + char *v1_path = NULL; + char *v2_path = NULL; + bool found = false; + int ret = ENOENT; + + ret = parse_thread_cgroup_paths(&v1_path, &v2_path); + if (ret != 0) { + goto cleanup; + } + + if (v1_path) + traverse_cgroup_path(v1_path, CGROUP_TYPE_LEGACY, info, &found); + + if (!found && v2_path) + traverse_cgroup_path(v2_path, CGROUP_TYPE_UNIFIED, info, &found); + + if (!found) { + ret = ENOENT; + goto cleanup; + } + + /* Get all memory information - fail fast on any error */ + ret = memcg_get_memory_limit(info); + if (ret != 0) + goto cleanup; + + ret = memcg_get_memory_current(info); + if (ret != 0) + goto cleanup; + + ret = memcg_get_swap_limit(info); + if (ret != 0) + goto cleanup; + + ret = memcg_get_swap_current(info); + if (ret != 0) + goto cleanup; + + ret = memcg_get_memory_stat(info); + if (ret != 0) + goto cleanup; + + ret = 0; + +cleanup: + free(v1_path); + free(v2_path); + + return ret; +} + +static int memcg_meminfo_unref(struct memcg_meminfo **info) { + if (info == NULL || *info == NULL) + return EINVAL; + + (*info)->refcount--; + + if ((*info)->refcount < 1) { + cleanup_memcg_info(*info); + free(*info); + *info = NULL; + return 0; + } + + return (*info)->refcount; +} + +#define STRLITERALLEN(x) (sizeof(""x"") - 1) +static inline bool startswith(const char *line, const char *pref) +{ + return strncmp(line, pref, strlen(pref)) == 0; +} + +int cgroup_meminfo_read_buf(struct meminfo_info *info, char *buf, int len) { + struct memcg_meminfo *cginfo = NULL; + struct memcg_data data; + struct memory_stat mstat; + int total_len = 0; + unsigned long memlimit = 0, memusage = 0, swfree = 0, swusage = 0, swtotal = 0; + FILE *meminfo_file = NULL; + int ret; + char *line = NULL; + size_t line_len = 0; + ssize_t rlen; + + if (!buf || len <= 0) { + errno = EINVAL; + return EINVAL; + } + + cginfo = calloc(1, sizeof(struct memcg_meminfo)); + if (!cginfo) { + return ENOMEM; + } + + ret = memcg_get_memory_info(cginfo); + if (ret != 0) { + errno = ret; /* Set errno to the actual error code */ + goto cleanup; + } + + data = cginfo->cgmem_data; + mstat = data.memory_stat; + + meminfo_file = fopen("/proc/meminfo", "r"); + if (!meminfo_file) { + ret = errno; + goto cleanup; + } + + memusage = data.memory_current / BYTES_TO_KB; + memlimit = data.memory_limit / BYTES_TO_KB; + swtotal = data.swap_limit / BYTES_TO_KB; + swusage = data.swap_current / BYTES_TO_KB; + swfree = swtotal - swusage; + + while ((rlen = getline(&line, &line_len, meminfo_file)) != -1) { + ssize_t l; + char *printme, lbuf[100]; + memset(lbuf, 0, 100); + + if (startswith(line, "MemTotal:")) { + unsigned long hosttotal = 0; + sscanf(line+sizeof("MemTotal:")-1, "%" PRIu64, &hosttotal); + if (memlimit == 0) + memlimit = hosttotal; + + if (hosttotal < memlimit) + memlimit = hosttotal; + snprintf(lbuf, 100, "MemTotal: %8" PRIu64 " kB\n", memlimit); + printme = lbuf; + } else if (startswith(line, "MemFree:")) { + snprintf(lbuf, 100, "MemFree: %8" PRIu64 " kB\n", memlimit - memusage); + printme = lbuf; + } else if (startswith(line, "MemAvailable:")) { + snprintf(lbuf, 100, "MemAvailable: %8" PRIu64 " kB\n", memlimit - memusage + + (mstat.total_active_file + mstat.total_inactive_file + mstat.slab_reclaimable) / BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "SwapTotal:")) { + unsigned long hostswtotal = 0; + sscanf(line + STRLITERALLEN("SwapTotal:"), "%" PRIu64, &hostswtotal); + if (hostswtotal < swtotal) { + swtotal = hostswtotal; + } + + snprintf(lbuf, 100, "SwapTotal: %8" PRIu64 " kB\n", swtotal); + printme = lbuf; + } else if (startswith(line, "SwapFree:")) { + swfree = swtotal - swusage; + snprintf(lbuf, 100, "SwapFree: %8" PRIu64 " kB\n", swfree); + printme = lbuf; + } else if (startswith(line, "Slab:")) { + snprintf(lbuf, 100, "Slab: %8" PRIu64 " kB\n", mstat.slab / BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "Buffers:")) { + snprintf(lbuf, 100, "Buffers: %8" PRIu64 " kB\n", (uint64_t)0); + printme = lbuf; + } else if (startswith(line, "Cached:")) { + snprintf(lbuf, 100, "Cached: %8" PRIu64 " kB\n", + mstat.total_cache / BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "SwapCached:")) { + snprintf(lbuf, 100, "SwapCached: %8" PRIu64 " kB\n", (uint64_t)0); + printme = lbuf; + } else if (startswith(line, "Active:")) { + snprintf(lbuf, 100, "Active: %8" PRIu64 " kB\n", + (mstat.total_active_anon + + mstat.total_active_file) / + BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "Inactive:")) { + snprintf(lbuf, 100, "Inactive: %8" PRIu64 " kB\n", + (mstat.total_inactive_anon + + mstat.total_inactive_file) / + BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "Active(anon):")) { + snprintf(lbuf, 100, "Active(anon): %8" PRIu64 " kB\n", + mstat.total_active_anon / BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "Inactive(anon):")) { + snprintf(lbuf, 100, "Inactive(anon): %8" PRIu64 " kB\n", + mstat.total_inactive_anon / BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "Active(file):")) { + snprintf(lbuf, 100, "Active(file): %8" PRIu64 " kB\n", + mstat.total_active_file / BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "Inactive(file):")) { + snprintf(lbuf, 100, "Inactive(file): %8" PRIu64 " kB\n", + mstat.total_inactive_file / BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "Unevictable:")) { + snprintf(lbuf, 100, "Unevictable: %8" PRIu64 " kB\n", + mstat.total_unevictable / BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "Dirty:")) { + snprintf(lbuf, 100, "Dirty: %8" PRIu64 " kB\n", + mstat.total_dirty / BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "Writeback:")) { + snprintf(lbuf, 100, "Writeback: %8" PRIu64 " kB\n", + mstat.total_writeback / BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "AnonPages:")) { + snprintf(lbuf, 100, "AnonPages: %8" PRIu64 " kB\n", + (mstat.total_active_anon + + mstat.total_inactive_anon - mstat.total_shmem) / + BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "Mapped:")) { + snprintf(lbuf, 100, "Mapped: %8" PRIu64 " kB\n", + mstat.total_mapped_file / BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "SReclaimable:")) { + snprintf(lbuf, 100, "SReclaimable: %8" PRIu64 " kB\n", mstat.slab_reclaimable / BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "SUnreclaim:")) { + snprintf(lbuf, 100, "SUnreclaim: %8" PRIu64 " kB\n", mstat.slab_unreclaimable / BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "Shmem:")) { + snprintf(lbuf, 100, "Shmem: %8" PRIu64 " kB\n", + mstat.total_shmem / BYTES_TO_KB); + printme = lbuf; + } else if (startswith(line, "ShmemHugePages:")) { + snprintf(lbuf, 100, "ShmemHugePages: %8" PRIu64 " kB\n", (uint64_t)0); + printme = lbuf; + } else if (startswith(line, "ShmemPmdMapped:")) { + snprintf(lbuf, 100, "ShmemPmdMapped: %8" PRIu64 " kB\n", (uint64_t)0); + printme = lbuf; + } else if (startswith(line, "AnonHugePages:")) { + snprintf(lbuf, 100, "AnonHugePages: %8" PRIu64 " kB\n", + mstat.total_rss_huge / BYTES_TO_KB); + printme = lbuf; + } else { + printme = line; + } + + l = snprintf(buf + total_len, len - total_len, "%s", printme); + if (l < 0 || l >= (len - total_len)) { + ret = EOVERFLOW; + errno = ret; /* Set errno to the actual error code */ + goto cleanup; + } + total_len += l; + } + + buf[total_len] = '\0'; + ret = 0; + +cleanup: + free(line); + + if (meminfo_file) + fclose(meminfo_file); + + if (cginfo) + memcg_meminfo_unref(&cginfo); + + return ret; +} \ No newline at end of file From 5cb1ea083439aa7d40638d4a0a2c1f15e4feff0f Mon Sep 17 00:00:00 2001 From: Chunsheng Luo Date: Wed, 3 Sep 2025 17:32:29 +0800 Subject: [PATCH 3/7] library: Add cgroup memory info support to meminfo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces cgroup-aware memory information to the meminfo, enabling applications to read memory statistics from cgroup controllers instead of the traditional /proc/meminfo. This enhancement allows applications to access accurate memory limits and usage that reflect container constraints rather than host-wide system resources. The implementation adds a new CGROUP_MEMINFO_TYPE source type that leverages the existing cgmeminfo module to provide container-aware memory reporting. Changes: - adding the cgroup_meminfo_new() public API function Signed-off-by: Chunsheng Luo --- library/include/meminfo.h | 1 + library/libproc2.sym | 4 ++++ library/meminfo.c | 15 +++++++++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/library/include/meminfo.h b/library/include/meminfo.h index 8c115f5e..dbef6312 100644 --- a/library/include/meminfo.h +++ b/library/include/meminfo.h @@ -204,6 +204,7 @@ struct meminfo_info; int procps_meminfo_new (struct meminfo_info **info); int procps_meminfo_ref (struct meminfo_info *info); int procps_meminfo_unref (struct meminfo_info **info); +int cgroup_meminfo_new (struct meminfo_info **info); struct meminfo_result *procps_meminfo_get ( struct meminfo_info *info, diff --git a/library/libproc2.sym b/library/libproc2.sym index 1998b1ae..62473e1a 100644 --- a/library/libproc2.sym +++ b/library/libproc2.sym @@ -76,3 +76,7 @@ LIBPROC_2.2 { procps_sigmask_names; procps_capmask_names; } LIBPROC_2.1; + +LIBPROC_2.3 { + cgroup_meminfo_new; +} LIBPROC_2.2; diff --git a/library/meminfo.c b/library/meminfo.c index e215465d..6721974c 100644 --- a/library/meminfo.c +++ b/library/meminfo.c @@ -132,6 +132,7 @@ struct stacks_extent { enum meminfo_source_type { PROC_MEMINFO_TYPE = 0, + CGROUP_MEMINFO_TYPE = 1, }; struct meminfo_info { @@ -653,7 +654,9 @@ static int meminfo_make_hash_failed ( #undef htXTRA } // end: meminfo_make_hash_failed -static int procps_meminfo_read_buf(struct meminfo_info *info, char *buf, int len) { +extern int cgroup_meminfo_read_buf(struct meminfo_info *info, char *buf, int len); + +static int procps_meminfo_read_buf(struct meminfo_info *info,char *buf, int len) { int size; if (-1 == info->meminfo_fd @@ -706,7 +709,7 @@ static int meminfo_read_failed ( unsigned long *valptr; signed long mem_used; - if (stype != PROC_MEMINFO_TYPE) + if (stype != PROC_MEMINFO_TYPE && stype != CGROUP_MEMINFO_TYPE) return ret; // remember history from last time around @@ -716,6 +719,8 @@ static int meminfo_read_failed ( if (stype == PROC_MEMINFO_TYPE) { ret = procps_meminfo_read_buf(info, buf, sizeof(buf)); + } else if (stype == CGROUP_MEMINFO_TYPE) { + ret = cgroup_meminfo_read_buf(info, buf, sizeof(buf)); } if (ret != 0) @@ -878,6 +883,12 @@ static int meminfo_new_internal (struct meminfo_info **info, // ___ Public Functions ||||||||||||||||||||||||||||||||||||||||||||||||||||||| // --- standard required functions -------------------------------------------- + +PROCPS_EXPORT int cgroup_meminfo_new ( + struct meminfo_info **info){ + return meminfo_new_internal(info, CGROUP_MEMINFO_TYPE); +} + /* * procps_meminfo_new: * From badb7825af3f1f6f06c52ee26e6e8c0d81014f18 Mon Sep 17 00:00:00 2001 From: Chunsheng Luo Date: Wed, 3 Sep 2025 17:55:11 +0800 Subject: [PATCH 4/7] free: Add --container option for container Introduces container-aware memory reporting to the free command by adding a new --container option. When specified, the command uses cgroup_meminfo_new() instead of procps_meminfo_new() to retrieve memory information from cgroup controllers rather than /proc/meminfo. This enhancement allows users to view memory statistics that reflect container limits and usage in containerized environments, providing more accurate resource information for applications running within containers. Signed-off-by: Chunsheng Luo --- src/free.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/free.c b/src/free.c index d2b79de1..d334e43d 100644 --- a/src/free.c +++ b/src/free.c @@ -55,6 +55,7 @@ #define FREE_REPEATCOUNT (1 << 7) #define FREE_COMMITTED (1 << 8) #define FREE_LINE (1 << 9) +#define FREE_CONTAINER (1 << 10) struct commandline_arguments { int exponent; /* demanded in kilos, magas... */ @@ -92,6 +93,7 @@ static void __attribute__ ((__noreturn__)) fputs(_(" -s N, --seconds N repeat printing every N seconds\n"), out); fputs(_(" -c N, --count N repeat printing N times, then exit\n"), out); fputs(_(" -w, --wide wide output\n"), out); + fputs(_(" --container show container memory info\n"), out); fputs(USAGE_SEPARATOR, out); fputs(_(" --help display this help and exit\n"), out); fputs(USAGE_VERSION, out); @@ -156,6 +158,7 @@ int main(int argc, char **argv) PETA_OPTION, TEBI_OPTION, PEBI_OPTION, + CONTAINER_OPTION, HELP_OPTION }; @@ -180,6 +183,7 @@ int main(int argc, char **argv) { "seconds", required_argument, NULL, 's' }, { "count", required_argument, NULL, 'c' }, { "wide", no_argument, NULL, 'w' }, + { "container", no_argument, NULL, CONTAINER_OPTION }, { "help", no_argument, NULL, HELP_OPTION }, { "version", no_argument, NULL, 'V' }, { NULL, 0, NULL, 0 } @@ -287,6 +291,9 @@ int main(int argc, char **argv) case 'w': flags |= FREE_WIDE; break; + case CONTAINER_OPTION: + flags |= FREE_CONTAINER; + break; case HELP_OPTION: usage(stdout); case 'V': @@ -298,7 +305,13 @@ int main(int argc, char **argv) if (optind != argc) usage(stderr); - if ( (rc = procps_meminfo_new(&mem_info)) < 0) + if (flags & FREE_CONTAINER) { + rc = cgroup_meminfo_new(&mem_info); + } else { + rc = procps_meminfo_new(&mem_info); + } + + if (rc < 0) { if (rc == -ENOENT) errx(EXIT_FAILURE, From 43888c42db87d0c60f1c0c60caab24032b1a24a1 Mon Sep 17 00:00:00 2001 From: Chunsheng Luo Date: Wed, 3 Sep 2025 19:00:08 +0800 Subject: [PATCH 5/7] vmstat: Add --container option for container Introduces container-aware memory reporting to the vmstat command by adding a new --container (-C) option. When specified, the command uses cgroup_meminfo_new() instead of procps_meminfo_new() to retrieve memory information from cgroup controllers rather than /proc/meminfo. This enhancement enables vmstat to display memory statistics that reflect container limits and usage in containerized environments, providing more accurate resource information for vmstat memory output. Signed-off-by: Chunsheng Luo --- src/vmstat.c | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/vmstat.c b/src/vmstat.c index 65fa5a2f..4fce2fb7 100644 --- a/src/vmstat.c +++ b/src/vmstat.c @@ -82,6 +82,8 @@ static int y_option; /* "-t" means "show timestamp" */ static int t_option; +/* show memory info from cgroup inside container */ +static int container_mode = 0; static unsigned sleep_time = 1; static int infinite_updates = 0; static unsigned long num_updates =1; @@ -232,6 +234,7 @@ static void __attribute__ ((__noreturn__)) fputs(_(" -w, --wide wide output\n"), out); fputs(_(" -t, --timestamp show timestamp\n"), out); fputs(_(" -y, --no-first skips first line of output\n"), out); + fputs(_(" -C, --container show container memory info\n"), out); fputs(USAGE_SEPARATOR, out); fputs(USAGE_HELP, out); fputs(USAGE_VERSION, out); @@ -404,6 +407,7 @@ static void new_format(void) unsigned long pgpgin[2], pgpgout[2], pswpin[2] = {0,0}, pswpout[2]; unsigned int sleep_half; unsigned long kb_per_page = sysconf(_SC_PAGESIZE) / 1024ul; + int ret = 0; int debt = 0; /* handle idle ticks running backwards */ struct tm *tm_ptr; time_t the_time; @@ -422,7 +426,12 @@ static void new_format(void) errx(EXIT_FAILURE, _("Unable to create vmstat structure")); if (procps_stat_new(&stat_info) < 0) errx(EXIT_FAILURE, _("Unable to create system stat structure")); - if (procps_meminfo_new(&mem_info) < 0) + if (container_mode == 1) { + ret = cgroup_meminfo_new(&mem_info); + } else { + ret = procps_meminfo_new(&mem_info); + } + if (ret < 0) errx(EXIT_FAILURE, _("Unable to create meminfo structure")); if (procps_uptime(&uptime, NULL) < 0) err(EXIT_FAILURE, _("Unable to get uptime")); @@ -886,6 +895,7 @@ static void sum_format(void) #define TICv(E) STAT_VAL(E, ull_int, stat_stack) #define SYSv(E) STAT_VAL(E, ul_int, stat_stack) #define MEMv(E) unitConvert(MEMINFO_VAL(E, ul_int, mem_stack)) + int ret = 0; struct stat_info *stat_info = NULL; struct vmstat_info *vm_info = NULL; struct meminfo_info *mem_info = NULL; @@ -898,7 +908,12 @@ static void sum_format(void) errx(EXIT_FAILURE, _("Unable to select stat information")); if (procps_vmstat_new(&vm_info) < 0) errx(EXIT_FAILURE, _("Unable to create vmstat structure")); - if (procps_meminfo_new(&mem_info) < 0) + if (container_mode == 1) { + ret = cgroup_meminfo_new(&mem_info); + } else { + ret = procps_meminfo_new(&mem_info); + } + if (ret < 0) errx(EXIT_FAILURE, _("Unable to create meminfo structure")); if (!(mem_stack = procps_meminfo_select(mem_info, Sum_mem_items, 10))) errx(EXIT_FAILURE, _("Unable to select memory information")); @@ -991,6 +1006,7 @@ int main(int argc, char *argv[]) {"help", no_argument, NULL, 'h'}, {"version", no_argument, NULL, 'V'}, {"no-first", no_argument, NULL, 'y'}, + {"container", no_argument, NULL, 'C'}, {NULL, 0, NULL, 0} }; @@ -1003,7 +1019,7 @@ int main(int argc, char *argv[]) atexit(close_stdout); while ((c = - getopt_long(argc, argv, "afmnsdDp:S:wthVy", longopts, NULL)) != -1) + getopt_long(argc, argv, "afmnsdDp:S:wthVyC", longopts, NULL)) != -1) switch (c) { case 'V': printf(PROCPS_NG_VERSION); @@ -1037,6 +1053,9 @@ int main(int argc, char *argv[]) if (strncmp(partition, "/dev/", 5) == 0) partition += 5; break; + case 'C': + container_mode = 1; + break; case 'S': switch (optarg[0]) { case 'b': From 94be9d93462ffbc665d844461b7a48c95e12b532 Mon Sep 17 00:00:00 2001 From: Chunsheng Luo Date: Wed, 3 Sep 2025 19:11:00 +0800 Subject: [PATCH 6/7] doc: Add container memory support documentation Introduces detailed documentation for the new container memory support feature in procps project. The documentation covers the implementation of cgroup-aware memory reporting for free and vmstat commands, enabling accurate resource information display within containerized environments. Signed-off-by: Chunsheng Luo --- doc/Container-memory-Support.md | 267 ++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 doc/Container-memory-Support.md diff --git a/doc/Container-memory-Support.md b/doc/Container-memory-Support.md new file mode 100644 index 00000000..9f6404e4 --- /dev/null +++ b/doc/Container-memory-Support.md @@ -0,0 +1,267 @@ +# Container Memory Support +=========== + +## Overview + +Currently, containers do not have isolated resource views. As a result, commands +such as free, vmstat executed within a container reflect the host's global +memory state rather than the container's actual constrained resources. This +behavior is inherently misleading—especially when memory limits are imposed on +the container, since the output does not represent the resources truly +available to it. + +To resolve this discrepancy, we now extract memory usage data directly from the +container's corresponding cgroup. This enhancement enables free, vmstat to +report container-specific memory information. When run inside a container, these +commands can utilize dedicated parameters to display memory usage based on +cgroup limits, providing an accurate view of the container's resource context +instead of inheriting the host's memory statistics. + +## Features Added + +### 1. Cgroup Version Support +- Supports both cgroup v1 and cgroup v2 +- Automatically detects which version is in use + +### 2. Container Memory Information +- Reads comprehensive memory limits and usage from cgroup instead of + `/proc/meminfo` +- Provides accurate memory information within container limits +- Supports both memory and swap information when available + +### 3. New Command Line Options For `free`, `vmstat` +- `--container` option to container mode + +#### `--container` Option Design + +This implementation adds a `--container` option rather than automatically +switching display modes based on container detection. This design decision is +based on the following considerations: + +- **Resource Representation**: The limits set in cgroup represent restrictions + on container resources, not the actual physical resources available within + the container +- **User Control**: The `--container` option allows users to explicitly choose + which view they need: + - With the option: View container-specific resource limits and usage + - Without the option: Access the global memory state of the host system +- **Operational Flexibility**: This approach enables users to obtain resource + information from either perspective (container or host) as needed +- **Diagnostic Capabilities**: Maintaining access to host memory data is + crucial for comprehensive troubleshooting scenarios + +This flexible design enhances operational diagnostic capabilities compared to an +automatic detection approach that would force container-only views when in +container environments. + +## Files Added/Modified + +### New Files: +- `library/cgmeminfo.c` - Implementation of container memory detection and reading + +### Modified Files: +- `src/free.c` - Enhanced to support container memory display +- `src/vmstat.c` - Enhanced to use container memory information when available + +## Usage Examples + +### free Container Support +When executed within a container, `free --container` detects the container's +memory cgroup path and displays its memory usage and limits: + +```bash +## Start container with memory limits: 256M memory + 256M swap +$ docker run -m 256M --memory-swap 512M -it ubuntu + +## Display container memory information +$ free --container -h + total used free shared buff/cache available +Mem: 256Mi 3.0Mi 252Mi 0B 405Ki 252Mi +Swap: 256Mi 0B 256Mi + +## Display host memory information +$ free -h + total used free shared buff/cache available +Mem: 62Gi 16Gi 41Gi 1.3Gi 6.1Gi 45Gi +Swap: 15Gi 84Mi 15Gi +``` + + +### vmstat Container Support +The `vmstat` command automatically detects container environments and uses +cgroup memory information: + +```bash +## Start container with memory limits: 256M memory + 256M swap +$ docker run -m 256M --memory-swap 512M -it ubuntu + +## Display container memory information +$ vmstat -s M --container +procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu------- + r b swpd free buff cache si so bi bo in cs us sy id wa st gu + 2 0 0 257 0 5 0 0 376 330 12515 5 1 1 98 0 0 0 + +## Display host memory information +$ vmstat -s M +procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu------- + r b swpd free buff cache si so bi bo in cs us sy id wa st gu + 3 1 92 45621 28 2298 0 0 376 330 12515 5 1 1 98 0 0 0 +``` + +When running in a container, vmstat shows memory statistics based on container +limits rather than host system memory. The memory columns (free, buff, cache) +reflect the container's cgroup memory constraints. +`` + +## Technical Details + +### Container Cgroup Path Logic +The implementation uses a sophisticated multi-step approach to detect and +validate container environments: + +1. **Cgroup Path Analysis**: Examines `/proc/self/cgroup` to identify: + - cgroup v2 entries (format: `0::/path`) + - cgroup v1 memory controller entries (format: `x:memory:/path`) + +2. **Cgroup Hierarchy Traversal**: For each detected path: + - Attempts to find the current process in cgroup tasks/cgroup.procs files + - Traverses up the cgroup hierarchy if not found in the initial path + - Validates process membership in the cgroup + +3. **Mount Point Detection**: Automatically locates cgroup mount points: + - cgroup v2: Searches for `cgroup2` filesystem type + - cgroup v1: Searches for `cgroup` filesystem with `memory` option + + +### Cgroup Memory Information Retrieval + +The implementation provides comprehensive memory information by reading various +cgroup files: + +#### Memory Limits and Usage +- **Memory Limit**: Maximum memory that can be used by the container + - cgroup v1: `memory.limit_in_bytes` + - cgroup v2: `memory.max` (returns "max" for unlimited) +- **Memory Current**: Current memory usage + - cgroup v1: `memory.usage_in_bytes` + - cgroup v2: `memory.current` + +#### Swap Information +- **Swap Limit**: Maximum swap space available + - cgroup v1: `memory.memsw.limit_in_bytes` (includes memory + swap) + - cgroup v2: `memory.swap.max` +- **Swap Current**: Current swap usage + - cgroup v1: `memory.memsw.usage_in_bytes` (includes memory + swap) + - cgroup v2: `memory.swap.current` + +#### Detailed Memory Statistics (`memory.stat`) +The implementation parses comprehensive memory statistics including: + +**File and Cache Memory:** +- `total_cache` / `file`: File cache memory +- `total_mapped_file` / `file_mapped`: Memory-mapped files +- `total_dirty` / `file_dirty`: Dirty file pages +- `total_writeback` / `file_writeback`: Pages being written back + +**Anonymous Memory:** +- `total_active_anon` / `active_anon`: Active anonymous pages +- `total_inactive_anon` / `inactive_anon`: Inactive anonymous pages +- `total_rss_huge`: Huge page RSS (cgroup v1 only) + +**File Memory:** +- `total_active_file` / `active_file`: Active file pages +- `total_inactive_file` / `inactive_file`: Inactive file pages + +**Shared and Special Memory:** +- `total_shmem` / `shmem`: Shared memory +- `total_unevictable` / `unevictable`: Unevictable pages + +**Kernel Memory (cgroup v2 only):** +- `slab_reclaimable`: Reclaimable slab memory +- `slab_unreclaimable`: Unreclaimable slab memory +- `slab`: Total slab memory + +#### Memory Statistics Mapping +vmstat maps container memory information to its standard output format: + +- **swpd**: Swap used (from cgroup swap.current) +- **free**: Free memory (calculated from container memory limit - current usage) +- **buff**: Buffer memory (typically 0 in containers, mapped from cgroup cache statistics) +- **cache**: Cache memory (from cgroup file cache statistics) + +#### Data Conversion and Processing +- All memory values are converted from bytes to KB for consistency with + `/proc/meminfo` +- For cgroup v1, swap values are calculated by subtracting memory usage from + memsw values +- Memory statistics are mapped to standard `/proc/meminfo` format for + compatibility +- Host memory limits are used as fallback when container limits exceed host + capacity + +## Programming Examples + +### Basic Usage +```c +#include "meminfo.h" + +int main() { + struct meminfo_info *info = NULL; + struct meminfo_result *result; + + // Initialize container memory info + if (is_container != 0) { + rc = cgroup_meminfo_new(&mem_info); + } else { + rc = procps_meminfo_new(&mem_info); + } + + // Get memory total + result = procps_meminfo_get(info, MEMINFO_MEM_TOTAL); + if (result) { + printf("Memory Total: %lu KB\n", result->result.ul_int); + } + + // Get memory usage + result = procps_meminfo_get(info, MEMINFO_MEM_USED); + if (result) { + printf("Memory Used: %lu KB\n", result->result.ul_int); + } + + // Get swap information + result = procps_meminfo_get(info, MEMINFO_SWAP_TOTAL); + if (result) { + printf("Swap Total: %lu KB\n", result->result.ul_int); + } + + result = procps_meminfo_get(info, MEMINFO_SWAP_USED); + if (result) { + printf("Swap Used: %lu KB\n", result->result.ul_int); + } + + // Clean up + procps_meminfo_unref(&info); + return 0; +} +``` + +## Error Handling and Diagnostics + +### Common Error Scenarios +1. **Container Detection Failure**: When running outside a container or in + unsupported environments +2. **Permission Issues**: Insufficient permissions to read cgroup files +3. **Missing Cgroup Files**: Cgroup controllers not enabled or files not + available +4. **Invalid Cgroup Paths**: Malformed or inaccessible cgroup paths + + +## Benefits + +### For Both free and vmstat Commands + +1. **Accurate Container Memory Reporting**: Shows actual container limits + instead of host memory +2. **Both Cgroup Versions**: Supports both cgroup v1 and v2 +3. **Comprehensive Statistics**: Provides detailed memory breakdown including + cache, buffers, and swap From b9ea79fa42e9eb2ba9914386bf6b86795714d668 Mon Sep 17 00:00:00 2001 From: Chunsheng Luo Date: Wed, 3 Sep 2025 21:00:15 +0800 Subject: [PATCH 7/7] testsuite: Add testsuite for cgroup memory info Test suite for cgmeminfo module: - Basic tests: Cgroup memory info creation, cleanup - Parsing tests: cgroup v1/v2 memory statistics parsing Signed-off-by: Chunsheng Luo --- Makefile.am | 6 +- library/tests/test_cgmeminfo.c | 265 +++++++++++++++++++++++++++++++++ 2 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 library/tests/test_cgmeminfo.c diff --git a/Makefile.am b/Makefile.am index a573405e..28de075e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -366,7 +366,8 @@ check_PROGRAMS += \ library/tests/test_uptime \ library/tests/test_sysinfo \ library/tests/test_version \ - library/tests/test_namespace + library/tests/test_namespace \ + library/tests/test_cgmeminfo library_tests_test_Itemtables_SOURCES = library/tests/test_Itemtables.c library_tests_test_Itemtables_LDADD = library/libproc2.la @@ -380,6 +381,8 @@ library_tests_test_version_SOURCES = library/tests/test_version.c library_tests_test_version_LDADD = library/libproc2.la library_tests_test_namespace_SOURCES = library/tests/test_namespace.c library_tests_test_namespace_LDADD = library/libproc2.la +library_tests_test_cgmeminfo_SOURCES = library/tests/test_cgmeminfo.c +library_tests_test_cgmeminfo_LDADD = library/libproc2.la if CYGWIN src_skill_LDADD = $(CYGWINFLAGS) @@ -417,6 +420,7 @@ TESTS = \ library/tests/test_sysinfo \ library/tests/test_version \ library/tests/test_namespace \ + library/tests/test_cgmeminfo \ src/tests/test_fileutils \ src/tests/test_strtod_nol diff --git a/library/tests/test_cgmeminfo.c b/library/tests/test_cgmeminfo.c new file mode 100644 index 00000000..ce1a4ca3 --- /dev/null +++ b/library/tests/test_cgmeminfo.c @@ -0,0 +1,265 @@ +/* + * libproc2 - Library to read proc filesystem + * Tests for cgmeminfo library calls + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#include +#include +#include +#include +#include + +#include "tests.h" +#include "meminfo.h" + +/* Include the cgmeminfo.c source to test internal functions */ +#include "library/cgmeminfo.c" + +/* + * Test cgroup_meminfo_new() function + */ +int test_cgroup_meminfo_new(void *data) +{ + struct meminfo_info *info = NULL; + int rc; + + testname = "cgroup_meminfo_new: basic functionality"; + + rc = cgroup_meminfo_new(&info); + /* In non-container environment, this might fail, which is expected */ + if (rc < 0) { + /* Check if we're in a container environment */ + struct stat st; + if (stat("/proc/self/cgroup", &st) != 0) { + /* No cgroup file, skip test */ + return 1; + } + /* cgroup file exists but function failed - this could be normal + * if not in a container or cgroup memory controller not available */ + return 1; + } + + if (info == NULL) { + return 0; + } + + procps_meminfo_unref(&info); + return 1; +} + +/* + * Test cgroup_meminfo_new() with NULL parameter + */ +int test_cgroup_meminfo_new_null(void *data) +{ + int rc; + + testname = "cgroup_meminfo_new: NULL parameter handling"; + + rc = cgroup_meminfo_new(NULL); + /* Should return error for NULL parameter */ + if (rc >= 0) { + return 0; + } + + return 1; +} + +/* + * Test memory info retrieval in container environment + */ +int test_cgroup_meminfo_get(void *data) +{ + struct meminfo_info *info = NULL; + struct meminfo_result *result; + int rc; + + testname = "cgroup_meminfo: memory info retrieval"; + + rc = cgroup_meminfo_new(&info); + if (rc < 0 || info == NULL) { + /* Skip test if not in container environment */ + return 1; + } + + /* Try to get memory total */ + result = procps_meminfo_get(info, MEMINFO_MEM_TOTAL); + if (result == NULL) { + procps_meminfo_unref(&info); + return 0; + } + + /* Memory total should be positive */ + if (result->result.ul_int <= 0) { + procps_meminfo_unref(&info); + return 0; + } + + /* Try to get memory available */ + result = procps_meminfo_get(info, MEMINFO_MEM_AVAILABLE); + if (result == NULL) { + procps_meminfo_unref(&info); + return 0; + } + + procps_meminfo_unref(&info); + return 1; +} + +/* + * Test reference counting + */ +int test_cgroup_meminfo_ref(void *data) +{ + struct meminfo_info *info = NULL; + int rc, refcount; + + testname = "cgroup_meminfo: reference counting"; + + rc = cgroup_meminfo_new(&info); + if (rc < 0 || info == NULL) { + /* Skip test if not in container environment */ + return 1; + } + + /* Initial refcount should be 1 */ + refcount = procps_meminfo_ref(info); + if (refcount != 2) { /* Should be 2 after ref() call */ + procps_meminfo_unref(&info); + return 0; + } + + /* Decrease refcount */ + refcount = procps_meminfo_unref(&info); + if (refcount != 1) { + procps_meminfo_unref(&info); + return 0; + } + + /* Final cleanup */ + procps_meminfo_unref(&info); + return 1; +} + +/* + * Test memory statistics parsing with cgroup v1 format + */ +int test_memcg_parse_memory_stat_v1(void *data) +{ + char *test_stat_v1 = strdup( + "total_cache 1048576\n" + "total_rss 2097152\n" + "total_rss_huge 0\n" + "total_shmem 524288\n" + "total_mapped_file 262144\n" + "total_dirty 4096\n" + "total_writeback 0\n" + "total_inactive_anon 1048576\n" + "total_active_anon 1048576\n" + "total_inactive_file 524288\n" + "total_active_file 524288\n" + "total_unevictable 0\n"); + + struct memory_stat mstat; + int ret; + + testname = "memcg_parse_memory_stat: cgroup v1 format"; + + memset(&mstat, 0, sizeof(mstat)); + ret = memcg_parse_memory_stat(test_stat_v1, &mstat, CGROUP_TYPE_LEGACY); + if (ret != 0) { + free(test_stat_v1); + return 0; + } + + /* Verify parsed values */ + if (mstat.total_cache != 1048576 || + mstat.total_shmem != 524288 || + mstat.total_mapped_file != 262144 || + mstat.total_active_anon != 1048576 || + mstat.total_inactive_anon != 1048576) { + free(test_stat_v1); + return 0; + } + + free(test_stat_v1); + return 1; +} + +/* + * Test memory statistics parsing with cgroup v2 format + */ +int test_memcg_parse_memory_stat_v2(void *data) +{ + char *test_stat_v2 = strdup( + "file 1048576\n" + "anon 2097152\n" + "file_mapped 262144\n" + "file_dirty 4096\n" + "file_writeback 0\n" + "shmem 524288\n" + "inactive_anon 1048576\n" + "active_anon 1048576\n" + "inactive_file 524288\n" + "active_file 524288\n" + "unevictable 0\n" + "slab_reclaimable 131072\n" + "slab_unreclaimable 65536\n" + "slab 196608\n"); + + struct memory_stat mstat; + int ret; + + testname = "memcg_parse_memory_stat: cgroup v2 format"; + + memset(&mstat, 0, sizeof(mstat)); + ret = memcg_parse_memory_stat(test_stat_v2, &mstat, CGROUP_TYPE_UNIFIED); + if (ret != 0) { + free(test_stat_v2); + return 0; + } + + /* Verify parsed values for v2 format */ + if (mstat.total_cache != 1048576 || /* file in v2 */ + mstat.total_shmem != 524288 || + mstat.total_mapped_file != 262144 || + mstat.total_active_anon != 1048576 || + mstat.total_inactive_anon != 1048576 || + mstat.slab_reclaimable != 131072 || + mstat.slab_unreclaimable != 65536 || + mstat.slab != 196608) { + free(test_stat_v2); + return 0; + } + + free(test_stat_v2); + return 1; +} + +TestFunction test_funcs[] = { + test_cgroup_meminfo_new, + test_cgroup_meminfo_new_null, + test_cgroup_meminfo_get, + test_cgroup_meminfo_ref, + test_memcg_parse_memory_stat_v1, + test_memcg_parse_memory_stat_v2, + NULL +}; + +int main(int argc, char *argv[]) +{ + return run_tests(test_funcs, NULL); +} \ No newline at end of file