From 6ce9ef3f05e9aeef93fa732287b9aad99371b9c5 Mon Sep 17 00:00:00 2001 From: Severin Gehwolf Date: Wed, 29 May 2024 08:46:27 +0000 Subject: [PATCH 1/4] 8302744: Refactor Hotspot container detection code Reviewed-by: jsjolen, stuefe --- .../os/linux/cgroupSubsystem_linux.cpp | 149 +++++- .../os/linux/cgroupSubsystem_linux.hpp | 216 +++----- .../os/linux/cgroupV1Subsystem_linux.cpp | 141 +++--- .../os/linux/cgroupV1Subsystem_linux.hpp | 2 - .../os/linux/cgroupV2Subsystem_linux.cpp | 128 ++--- .../os/linux/cgroupV2Subsystem_linux.hpp | 8 +- .../os/linux/test_cgroupSubsystem_linux.cpp | 204 -------- .../runtime/test_cgroupSubsystem_linux.cpp | 464 ++++++++++++++++++ .../gtest/runtime/test_os_linux_cgroups.cpp | 86 ---- .../docker/TestMemoryAwareness.java | 4 +- 10 files changed, 827 insertions(+), 575 deletions(-) delete mode 100644 test/hotspot/gtest/os/linux/test_cgroupSubsystem_linux.cpp create mode 100644 test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp delete mode 100644 test/hotspot/gtest/runtime/test_os_linux_cgroups.cpp diff --git a/src/hotspot/os/linux/cgroupSubsystem_linux.cpp b/src/hotspot/os/linux/cgroupSubsystem_linux.cpp index 2a2d83ed748..a348ea424fb 100644 --- a/src/hotspot/os/linux/cgroupSubsystem_linux.cpp +++ b/src/hotspot/os/linux/cgroupSubsystem_linux.cpp @@ -558,21 +558,162 @@ jlong CgroupSubsystem::memory_limit_in_bytes() { return mem_limit; } -jlong CgroupSubsystem::limit_from_str(char* limit_str) { +bool CgroupController::read_string(const char* filename, char* buf, size_t buf_size) { + assert(buf != nullptr, "buffer must not be null"); + assert(filename != nullptr, "filename must be given"); + char* s_path = subsystem_path(); + if (s_path == nullptr) { + log_debug(os, container)("read_string: subsystem path is null"); + return false; + } + + stringStream file_path; + file_path.print_raw(s_path); + file_path.print_raw(filename); + + if (file_path.size() > MAXPATHLEN) { + log_debug(os, container)("File path too long %s, %s", file_path.base(), filename); + return false; + } + const char* absolute_path = file_path.freeze(); + log_trace(os, container)("Path to %s is %s", filename, absolute_path); + + FILE* fp = os::fopen(absolute_path, "r"); + if (fp == nullptr) { + log_debug(os, container)("Open of file %s failed, %s", absolute_path, os::strerror(errno)); + return false; + } + + // Read a single line into the provided buffer. + // At most buf_size - 1 characters. + char* line = fgets(buf, buf_size, fp); + fclose(fp); + if (line == nullptr) { + log_debug(os, container)("Empty file %s", absolute_path); + return false; + } + size_t len = strlen(line); + assert(len <= buf_size - 1, "At most buf_size - 1 bytes can be read"); + if (line[len - 1] == '\n') { + line[len - 1] = '\0'; // trim trailing new line + } + return true; +} + +bool CgroupController::read_number(const char* filename, julong* result) { + char buf[1024]; + bool is_ok = read_string(filename, buf, 1024); + if (!is_ok) { + return false; + } + int matched = sscanf(buf, JULONG_FORMAT, result); + if (matched == 1) { + return true; + } + return false; +} + +bool CgroupController::read_number_handle_max(const char* filename, jlong* result) { + char buf[1024]; + bool is_ok = read_string(filename, buf, 1024); + if (!is_ok) { + return false; + } + jlong val = limit_from_str(buf); + if (val == OSCONTAINER_ERROR) { + return false; + } + *result = val; + return true; +} + +bool CgroupController::read_numerical_key_value(const char* filename, const char* key, julong* result) { + assert(key != nullptr, "key must be given"); + assert(result != nullptr, "result pointer must not be null"); + assert(filename != nullptr, "file to search in must be given"); + char* s_path = subsystem_path(); + if (s_path == nullptr) { + log_debug(os, container)("read_numerical_key_value: subsystem path is null"); + return false; + } + + stringStream file_path; + file_path.print_raw(s_path); + file_path.print_raw(filename); + + if (file_path.size() > MAXPATHLEN) { + log_debug(os, container)("File path too long %s, %s", file_path.base(), filename); + return false; + } + const char* absolute_path = file_path.freeze(); + log_trace(os, container)("Path to %s is %s", filename, absolute_path); + FILE* fp = os::fopen(absolute_path, "r"); + if (fp == nullptr) { + log_debug(os, container)("Open of file %s failed, %s", absolute_path, os::strerror(errno)); + return false; + } + + const int buf_len = MAXPATHLEN+1; + char buf[buf_len]; + char* line = fgets(buf, buf_len, fp); + bool found_match = false; + // File consists of multiple lines in a "key value" + // fashion, we have to find the key. + const size_t key_len = strlen(key); + for (; line != nullptr; line = fgets(buf, buf_len, fp)) { + char after_key = line[key_len]; + if (strncmp(line, key, key_len) == 0 + && isspace(after_key) != 0 + && after_key != '\n') { + // Skip key, skip space + const char* value_substr = line + key_len + 1; + int matched = sscanf(value_substr, JULONG_FORMAT, result); + found_match = matched == 1; + if (found_match) { + break; + } + } + } + fclose(fp); + if (found_match) { + return true; + } + log_debug(os, container)("Type %s (key == %s) not found in file %s", JULONG_FORMAT, + key, absolute_path); + return false; +} + +bool CgroupController::read_numerical_tuple_value(const char* filename, bool use_first, jlong* result) { + char buf[1024]; + bool is_ok = read_string(filename, buf, 1024); + if (!is_ok) { + return false; + } + char token[1024]; + const int matched = sscanf(buf, (use_first ? "%1023s %*s" : "%*s %1023s"), token); + if (matched != 1) { + return false; + } + jlong val = limit_from_str(token); + if (val == OSCONTAINER_ERROR) { + return false; + } + *result = val; + return true; +} + +jlong CgroupController::limit_from_str(char* limit_str) { if (limit_str == nullptr) { return OSCONTAINER_ERROR; } // Unlimited memory in cgroups is the literal string 'max' for // some controllers, for example the pids controller. if (strcmp("max", limit_str) == 0) { - os::free(limit_str); return (jlong)-1; } julong limit; if (sscanf(limit_str, JULONG_FORMAT, &limit) != 1) { - os::free(limit_str); return OSCONTAINER_ERROR; } - os::free(limit_str); return (jlong)limit; } diff --git a/src/hotspot/os/linux/cgroupSubsystem_linux.hpp b/src/hotspot/os/linux/cgroupSubsystem_linux.hpp index 6c5470445f1..5888072bdf3 100644 --- a/src/hotspot/os/linux/cgroupSubsystem_linux.hpp +++ b/src/hotspot/os/linux/cgroupSubsystem_linux.hpp @@ -69,148 +69,91 @@ #define MEMORY_IDX 3 #define PIDS_IDX 4 -typedef char * cptr; - -class CgroupController: public CHeapObj { - public: - virtual char *subsystem_path() = 0; -}; - -PRAGMA_DIAG_PUSH -PRAGMA_FORMAT_NONLITERAL_IGNORED -// Parses a subsystem's file, looking for a matching line. -// If key is null, then the first line will be matched with scan_fmt. -// If key isn't null, then each line will be matched, looking for something that matches "$key $scan_fmt". -// The matching value will be assigned to returnval. -// scan_fmt uses scanf() syntax. -// Return value: 0 on match, OSCONTAINER_ERROR on error. -template int subsystem_file_line_contents(CgroupController* c, - const char *filename, - const char *key, - const char *scan_fmt, - T returnval) { - if (c == nullptr) { - log_debug(os, container)("subsystem_file_line_contents: CgroupController* is null"); - return OSCONTAINER_ERROR; - } - if (c->subsystem_path() == nullptr) { - log_debug(os, container)("subsystem_file_line_contents: subsystem path is null"); - return OSCONTAINER_ERROR; - } - - stringStream file_path; - file_path.print_raw(c->subsystem_path()); - file_path.print_raw(filename); - - if (file_path.size() > (MAXPATHLEN-1)) { - log_debug(os, container)("File path too long %s, %s", file_path.base(), filename); - return OSCONTAINER_ERROR; - } - const char* absolute_path = file_path.freeze(); - log_trace(os, container)("Path to %s is %s", filename, absolute_path); - - FILE* fp = os::fopen(absolute_path, "r"); - if (fp == nullptr) { - log_debug(os, container)("Open of file %s failed, %s", absolute_path, os::strerror(errno)); - return OSCONTAINER_ERROR; - } - - const int buf_len = MAXPATHLEN+1; - char buf[buf_len]; - char* line = fgets(buf, buf_len, fp); - if (line == nullptr) { - log_debug(os, container)("Empty file %s", absolute_path); - fclose(fp); - return OSCONTAINER_ERROR; - } - - bool found_match = false; - if (key == nullptr) { - // File consists of a single line according to caller, with only a value - int matched = sscanf(line, scan_fmt, returnval); - found_match = matched == 1; - } else { - // File consists of multiple lines in a "key value" - // fashion, we have to find the key. - const int key_len = (int)strlen(key); - for (; line != nullptr; line = fgets(buf, buf_len, fp)) { - char* key_substr = strstr(line, key); - char after_key = line[key_len]; - if (key_substr == line - && isspace(after_key) != 0 - && after_key != '\n') { - // Skip key, skip space - const char* value_substr = line + key_len + 1; - int matched = sscanf(value_substr, scan_fmt, returnval); - found_match = matched == 1; - if (found_match) { - break; - } - } - } - } - fclose(fp); - if (found_match) { - return 0; - } - log_debug(os, container)("Type %s (key == %s) not found in file %s", scan_fmt, - (key == nullptr ? "null" : key), absolute_path); - return OSCONTAINER_ERROR; -} -PRAGMA_DIAG_POP - -// log_fmt can be different than scan_fmt. For example -// cpu_period() for cgv2 uses log_fmt='%d' and scan_fmt='%*s %d' -#define GET_CONTAINER_INFO(return_type, subsystem, filename, \ - logstring, log_fmt, scan_fmt, variable) \ - return_type variable; \ -{ \ - int err; \ - err = subsystem_file_line_contents(subsystem, \ - filename, \ - nullptr, \ - scan_fmt, \ - &variable); \ - if (err != 0) { \ - log_trace(os, container)(logstring "%d", OSCONTAINER_ERROR); \ - return (return_type) OSCONTAINER_ERROR; \ - } \ - \ - log_trace(os, container)(logstring log_fmt, variable); \ +#define CONTAINER_READ_NUMBER_CHECKED(controller, filename, log_string, retval) \ +{ \ + bool is_ok; \ + is_ok = controller->read_number(filename, &retval); \ + if (!is_ok) { \ + log_trace(os, container)(log_string " failed: %d", OSCONTAINER_ERROR); \ + return OSCONTAINER_ERROR; \ + } \ + log_trace(os, container)(log_string " is: " JULONG_FORMAT, retval); \ } -#define GET_CONTAINER_INFO_CPTR(return_type, subsystem, filename, \ - logstring, scan_fmt, variable, bufsize) \ - char variable[bufsize]; \ -{ \ - int err; \ - err = subsystem_file_line_contents(subsystem, \ - filename, \ - nullptr, \ - scan_fmt, \ - variable); \ - if (err != 0) \ - return (return_type) nullptr; \ - \ - log_trace(os, container)(logstring, variable); \ +#define CONTAINER_READ_NUMBER_CHECKED_MAX(controller, filename, log_string, retval) \ +{ \ + bool is_ok; \ + is_ok = controller->read_number_handle_max(filename, &retval); \ + if (!is_ok) { \ + log_trace(os, container)(log_string " failed: %d", OSCONTAINER_ERROR); \ + return OSCONTAINER_ERROR; \ + } \ + log_trace(os, container)(log_string " is: " JLONG_FORMAT, retval); \ } -#define GET_CONTAINER_INFO_LINE(return_type, controller, filename, \ - matchline, logstring, scan_fmt, variable) \ - return_type variable; \ -{ \ - int err; \ - err = subsystem_file_line_contents(controller, \ - filename, \ - matchline, \ - scan_fmt, \ - &variable); \ - if (err != 0) \ - return (return_type) OSCONTAINER_ERROR; \ - \ - log_trace(os, container)(logstring, variable); \ +#define CONTAINER_READ_STRING_CHECKED(controller, filename, log_string, retval, buf_size) \ +{ \ + bool is_ok; \ + is_ok = controller->read_string(filename, retval, buf_size); \ + if (!is_ok) { \ + log_trace(os, container)(log_string " failed: %d", OSCONTAINER_ERROR); \ + return nullptr; \ + } \ + log_trace(os, container)(log_string " is: %s", retval); \ } +class CgroupController: public CHeapObj { + public: + virtual char* subsystem_path() = 0; + + /* Read a numerical value as unsigned long + * + * returns: false if any error occurred. true otherwise and + * the parsed value is set in the provided julong pointer. + */ + bool read_number(const char* filename, julong* result); + + /* Convenience method to deal with numbers as well as the string 'max' + * in interface files. Otherwise same as read_number(). + * + * returns: false if any error occurred. true otherwise and + * the parsed value (which might be negative) is being set in + * the provided jlong pointer. + */ + bool read_number_handle_max(const char* filename, jlong* result); + + /* Read a string of at most buf_size - 1 characters from the interface file. + * The provided buffer must be at least buf_size in size so as to account + * for the null terminating character. Callers must ensure that the buffer + * is appropriately in-scope and of sufficient size. + * + * returns: false if any error occured. true otherwise and the passed + * in buffer will contain the first buf_size - 1 characters of the string + * or up to the first new line character ('\n') whichever comes first. + */ + bool read_string(const char* filename, char* buf, size_t buf_size); + + /* Read a tuple value as a number. Tuple is: ' '. + * Handles 'max' (for unlimited) for any tuple value. This is handy for + * parsing interface files like cpu.max which contain such tuples. + * + * returns: false if any error occurred. true otherwise and the parsed + * value of the appropriate tuple entry set in the provided jlong pointer. + */ + bool read_numerical_tuple_value(const char* filename, bool use_first, jlong* result); + + /* Read a numerical value from a multi-line interface file. The matched line is + * determined by the provided 'key'. The associated numerical value is being set + * via the passed in julong pointer. Example interface file 'memory.stat' + * + * returns: false if any error occurred. true otherwise and the parsed value is + * being set in the provided julong pointer. + */ + bool read_numerical_key_value(const char* filename, const char* key, julong* result); + + private: + static jlong limit_from_str(char* limit_str); +}; class CachedMetric : public CHeapObj{ private: @@ -255,7 +198,6 @@ class CgroupSubsystem: public CHeapObj { public: jlong memory_limit_in_bytes(); int active_processor_count(); - jlong limit_from_str(char* limit_str); virtual int cpu_quota() = 0; virtual int cpu_period() = 0; diff --git a/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp b/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp index 307c3c53a0e..229e2e4237b 100644 --- a/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp +++ b/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp @@ -75,9 +75,9 @@ void CgroupV1Controller::set_subsystem_path(char *cgroup_path) { * OSCONTAINER_ERROR for not supported */ jlong CgroupV1MemoryController::uses_mem_hierarchy() { - GET_CONTAINER_INFO(jlong, this, "/memory.use_hierarchy", - "Use Hierarchy is: ", JLONG_FORMAT, JLONG_FORMAT, use_hierarchy); - return use_hierarchy; + julong use_hierarchy; + CONTAINER_READ_NUMBER_CHECKED(this, "/memory.use_hierarchy", "Use Hierarchy", use_hierarchy); + return (jlong)use_hierarchy; } void CgroupV1MemoryController::set_subsystem_path(char *cgroup_path) { @@ -89,15 +89,20 @@ void CgroupV1MemoryController::set_subsystem_path(char *cgroup_path) { } jlong CgroupV1Subsystem::read_memory_limit_in_bytes() { - GET_CONTAINER_INFO(julong, _memory->controller(), "/memory.limit_in_bytes", - "Memory Limit is: ", JULONG_FORMAT, JULONG_FORMAT, memlimit); - + julong memlimit; + CONTAINER_READ_NUMBER_CHECKED(_memory->controller(), "/memory.limit_in_bytes", "Memory Limit", memlimit); if (memlimit >= os::Linux::physical_memory()) { log_trace(os, container)("Non-Hierarchical Memory Limit is: Unlimited"); CgroupV1MemoryController* mem_controller = reinterpret_cast(_memory->controller()); if (mem_controller->is_hierarchical()) { - GET_CONTAINER_INFO_LINE(julong, _memory->controller(), "/memory.stat", "hierarchical_memory_limit", - "Hierarchical Memory Limit is: " JULONG_FORMAT, JULONG_FORMAT, hier_memlimit) + julong hier_memlimit; + bool is_ok = _memory->controller()->read_numerical_key_value("/memory.stat", + "hierarchical_memory_limit", + &hier_memlimit); + if (!is_ok) { + return OSCONTAINER_ERROR; + } + log_trace(os, container)("Hierarchical Memory Limit is: " JULONG_FORMAT, hier_memlimit); if (hier_memlimit >= os::Linux::physical_memory()) { log_trace(os, container)("Hierarchical Memory Limit is: Unlimited"); } else { @@ -125,16 +130,22 @@ jlong CgroupV1Subsystem::read_memory_limit_in_bytes() { */ jlong CgroupV1Subsystem::read_mem_swap() { julong host_total_memsw; - GET_CONTAINER_INFO(julong, _memory->controller(), "/memory.memsw.limit_in_bytes", - "Memory and Swap Limit is: ", JULONG_FORMAT, JULONG_FORMAT, memswlimit); + julong hier_memswlimit; + julong memswlimit; + CONTAINER_READ_NUMBER_CHECKED(_memory->controller(), "/memory.memsw.limit_in_bytes", "Memory and Swap Limit", memswlimit); host_total_memsw = os::Linux::host_swap() + os::Linux::physical_memory(); if (memswlimit >= host_total_memsw) { log_trace(os, container)("Non-Hierarchical Memory and Swap Limit is: Unlimited"); CgroupV1MemoryController* mem_controller = reinterpret_cast(_memory->controller()); if (mem_controller->is_hierarchical()) { const char* matchline = "hierarchical_memsw_limit"; - GET_CONTAINER_INFO_LINE(julong, _memory->controller(), "/memory.stat", matchline, - "Hierarchical Memory and Swap Limit is : " JULONG_FORMAT, JULONG_FORMAT, hier_memswlimit) + bool is_ok = _memory->controller()->read_numerical_key_value("/memory.stat", + matchline, + &hier_memswlimit); + if (!is_ok) { + return OSCONTAINER_ERROR; + } + log_trace(os, container)("Hierarchical Memory and Swap Limit is: " JULONG_FORMAT, hier_memswlimit); if (hier_memswlimit >= host_total_memsw) { log_trace(os, container)("Hierarchical Memory and Swap Limit is: Unlimited"); } else { @@ -168,15 +179,22 @@ jlong CgroupV1Subsystem::memory_and_swap_limit_in_bytes() { return memory_swap; } +static inline +jlong memory_swap_usage_impl(CgroupController* ctrl) { + julong memory_swap_usage; + CONTAINER_READ_NUMBER_CHECKED(ctrl, "/memory.memsw.usage_in_bytes", "mem swap usage", memory_swap_usage); + return (jlong)memory_swap_usage; +} + jlong CgroupV1Subsystem::read_mem_swappiness() { - GET_CONTAINER_INFO(julong, _memory->controller(), "/memory.swappiness", - "Swappiness is: ", JULONG_FORMAT, JULONG_FORMAT, swappiness); - return swappiness; + julong swappiness; + CONTAINER_READ_NUMBER_CHECKED(_memory->controller(), "/memory.swappiness", "Swappiness", swappiness); + return (jlong)swappiness; } jlong CgroupV1Subsystem::memory_soft_limit_in_bytes() { - GET_CONTAINER_INFO(julong, _memory->controller(), "/memory.soft_limit_in_bytes", - "Memory Soft Limit is: ", JULONG_FORMAT, JULONG_FORMAT, memsoftlimit); + julong memsoftlimit; + CONTAINER_READ_NUMBER_CHECKED(_memory->controller(), "/memory.soft_limit_in_bytes", "Memory Soft Limit", memsoftlimit); if (memsoftlimit >= os::Linux::physical_memory()) { log_trace(os, container)("Memory Soft Limit is: Unlimited"); return (jlong)-1; @@ -195,9 +213,9 @@ jlong CgroupV1Subsystem::memory_soft_limit_in_bytes() { * OSCONTAINER_ERROR for not supported */ jlong CgroupV1Subsystem::memory_usage_in_bytes() { - GET_CONTAINER_INFO(jlong, _memory->controller(), "/memory.usage_in_bytes", - "Memory Usage is: ", JLONG_FORMAT, JLONG_FORMAT, memusage); - return memusage; + julong memusage; + CONTAINER_READ_NUMBER_CHECKED(_memory->controller(), "/memory.usage_in_bytes", "Memory Usage", memusage); + return (jlong)memusage; } /* memory_max_usage_in_bytes @@ -209,21 +227,20 @@ jlong CgroupV1Subsystem::memory_usage_in_bytes() { * OSCONTAINER_ERROR for not supported */ jlong CgroupV1Subsystem::memory_max_usage_in_bytes() { - GET_CONTAINER_INFO(jlong, _memory->controller(), "/memory.max_usage_in_bytes", - "Maximum Memory Usage is: ", JLONG_FORMAT, JLONG_FORMAT, memmaxusage); - return memmaxusage; + julong memmaxusage; + CONTAINER_READ_NUMBER_CHECKED(_memory->controller(), "/memory.max_usage_in_bytes", "Maximum Memory Usage", memmaxusage); + return (jlong)memmaxusage; } - jlong CgroupV1Subsystem::kernel_memory_usage_in_bytes() { - GET_CONTAINER_INFO(jlong, _memory->controller(), "/memory.kmem.usage_in_bytes", - "Kernel Memory Usage is: ", JLONG_FORMAT, JLONG_FORMAT, kmem_usage); - return kmem_usage; + julong kmem_usage; + CONTAINER_READ_NUMBER_CHECKED(_memory->controller(), "/memory.kmem.usage_in_bytes", "Kernel Memory Usage", kmem_usage); + return (jlong)kmem_usage; } jlong CgroupV1Subsystem::kernel_memory_limit_in_bytes() { - GET_CONTAINER_INFO(julong, _memory->controller(), "/memory.kmem.limit_in_bytes", - "Kernel Memory Limit is: ", JULONG_FORMAT, JULONG_FORMAT, kmem_limit); + julong kmem_limit; + CONTAINER_READ_NUMBER_CHECKED(_memory->controller(), "/memory.kmem.limit_in_bytes", "Kernel Memory Limit", kmem_limit); if (kmem_limit >= os::Linux::physical_memory()) { return (jlong)-1; } @@ -231,9 +248,9 @@ jlong CgroupV1Subsystem::kernel_memory_limit_in_bytes() { } jlong CgroupV1Subsystem::kernel_memory_max_usage_in_bytes() { - GET_CONTAINER_INFO(jlong, _memory->controller(), "/memory.kmem.max_usage_in_bytes", - "Maximum Kernel Memory Usage is: ", JLONG_FORMAT, JLONG_FORMAT, kmem_max_usage); - return kmem_max_usage; + julong kmem_max_usage; + CONTAINER_READ_NUMBER_CHECKED(_memory->controller(), "/memory.kmem.max_usage_in_bytes", "Maximum Kernel Memory Usage", kmem_max_usage); + return (jlong)kmem_max_usage; } void CgroupV1Subsystem::print_version_specific_info(outputStream* st) { @@ -246,15 +263,15 @@ void CgroupV1Subsystem::print_version_specific_info(outputStream* st) { OSContainer::print_container_helper(st, kmem_max_usage, "kernel_memory_max_usage_in_bytes"); } -char * CgroupV1Subsystem::cpu_cpuset_cpus() { - GET_CONTAINER_INFO_CPTR(cptr, _cpuset, "/cpuset.cpus", - "cpuset.cpus is: %s", "%1023s", cpus, 1024); +char* CgroupV1Subsystem::cpu_cpuset_cpus() { + char cpus[1024]; + CONTAINER_READ_STRING_CHECKED(_cpuset, "/cpuset.cpus", "cpuset.cpus", cpus, 1024); return os::strdup(cpus); } -char * CgroupV1Subsystem::cpu_cpuset_memory_nodes() { - GET_CONTAINER_INFO_CPTR(cptr, _cpuset, "/cpuset.mems", - "cpuset.mems is: %s", "%1023s", mems, 1024); +char* CgroupV1Subsystem::cpu_cpuset_memory_nodes() { + char mems[1024]; + CONTAINER_READ_STRING_CHECKED(_cpuset, "/cpuset.mems", "cpuset.mems", mems, 1024); return os::strdup(mems); } @@ -269,15 +286,24 @@ char * CgroupV1Subsystem::cpu_cpuset_memory_nodes() { * OSCONTAINER_ERROR for not supported */ int CgroupV1Subsystem::cpu_quota() { - GET_CONTAINER_INFO(int, _cpu->controller(), "/cpu.cfs_quota_us", - "CPU Quota is: ", "%d", "%d", quota); - return quota; + julong quota; + bool is_ok = _cpu->controller()-> + read_number("/cpu.cfs_quota_us", "a); + if (!is_ok) { + log_trace(os, container)("CPU Quota failed: %d", OSCONTAINER_ERROR); + return OSCONTAINER_ERROR; + } + // cast to int since the read value might be negative + // and we want to avoid logging -1 as a large unsigned value. + int quota_int = (int)quota; + log_trace(os, container)("CPU Quota is: %d", quota_int); + return quota_int; } int CgroupV1Subsystem::cpu_period() { - GET_CONTAINER_INFO(int, _cpu->controller(), "/cpu.cfs_period_us", - "CPU Period is: ", "%d", "%d", period); - return period; + julong period; + CONTAINER_READ_NUMBER_CHECKED(_cpu->controller(), "/cpu.cfs_period_us", "CPU Period", period); + return (int)period; } /* cpu_shares @@ -291,19 +317,13 @@ int CgroupV1Subsystem::cpu_period() { * OSCONTAINER_ERROR for not supported */ int CgroupV1Subsystem::cpu_shares() { - GET_CONTAINER_INFO(int, _cpu->controller(), "/cpu.shares", - "CPU Shares is: ", "%d", "%d", shares); + julong shares; + CONTAINER_READ_NUMBER_CHECKED(_cpu->controller(), "/cpu.shares", "CPU Shares", shares); + int shares_int = (int)shares; // Convert 1024 to no shares setup - if (shares == 1024) return -1; - - return shares; -} - + if (shares_int == 1024) return -1; -char* CgroupV1Subsystem::pids_max_val() { - GET_CONTAINER_INFO_CPTR(cptr, _pids, "/pids.max", - "Maximum number of tasks is: %s", "%1023s", pidsmax, 1024); - return os::strdup(pidsmax); + return shares_int; } /* pids_max @@ -317,8 +337,9 @@ char* CgroupV1Subsystem::pids_max_val() { */ jlong CgroupV1Subsystem::pids_max() { if (_pids == nullptr) return OSCONTAINER_ERROR; - char * pidsmax_str = pids_max_val(); - return limit_from_str(pidsmax_str); + jlong pids_max; + CONTAINER_READ_NUMBER_CHECKED_MAX(_pids, "/pids.max", "Maximum number of tasks", pids_max); + return pids_max; } /* pids_current @@ -331,7 +352,7 @@ jlong CgroupV1Subsystem::pids_max() { */ jlong CgroupV1Subsystem::pids_current() { if (_pids == nullptr) return OSCONTAINER_ERROR; - GET_CONTAINER_INFO(jlong, _pids, "/pids.current", - "Current number of tasks is: ", JLONG_FORMAT, JLONG_FORMAT, pids_current); - return pids_current; + julong pids_current; + CONTAINER_READ_NUMBER_CHECKED(_pids, "/pids.current", "Current number of tasks", pids_current); + return (jlong)pids_current; } diff --git a/src/hotspot/os/linux/cgroupV1Subsystem_linux.hpp b/src/hotspot/os/linux/cgroupV1Subsystem_linux.hpp index fae65da2a58..c7535844bf5 100644 --- a/src/hotspot/os/linux/cgroupV1Subsystem_linux.hpp +++ b/src/hotspot/os/linux/cgroupV1Subsystem_linux.hpp @@ -111,8 +111,6 @@ class CgroupV1Subsystem: public CgroupSubsystem { CgroupV1Controller* _cpuacct = nullptr; CgroupV1Controller* _pids = nullptr; - char * pids_max_val(); - jlong read_mem_swappiness(); jlong read_mem_swap(); diff --git a/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp b/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp index 1a02bbe95d2..d189dd6626e 100644 --- a/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp +++ b/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp @@ -35,10 +35,11 @@ * OSCONTAINER_ERROR for not supported */ int CgroupV2Subsystem::cpu_shares() { - GET_CONTAINER_INFO(int, _unified, "/cpu.weight", - "Raw value for CPU Shares is: ", "%d", "%d", shares); + julong shares; + CONTAINER_READ_NUMBER_CHECKED(_unified, "/cpu.weight", "Raw value for CPU Shares", shares); + int shares_int = (int)shares; // Convert default value of 100 to no shares setup - if (shares == 100) { + if (shares_int == 100) { log_debug(os, container)("CPU Shares is: %d", -1); return -1; } @@ -50,7 +51,7 @@ int CgroupV2Subsystem::cpu_shares() { // Use the inverse of (x == OCI value, y == cgroupsv2 value): // ((262142 * y - 1)/9999) + 2 = x // - int x = 262142 * shares - 1; + int x = 262142 * shares_int - 1; double frac = x/9999.0; x = ((int)frac) + 2; log_trace(os, container)("Scaled CPU shares value is: %d", x); @@ -83,33 +84,37 @@ int CgroupV2Subsystem::cpu_shares() { * OSCONTAINER_ERROR for not supported */ int CgroupV2Subsystem::cpu_quota() { - char * cpu_quota_str = cpu_quota_val(); - int limit = (int)limit_from_str(cpu_quota_str); + jlong quota_val; + bool is_ok = _unified->read_numerical_tuple_value("/cpu.max", true /* use_first */, "a_val); + if (!is_ok) { + return OSCONTAINER_ERROR; + } + int limit = (int)quota_val; log_trace(os, container)("CPU Quota is: %d", limit); return limit; } -char * CgroupV2Subsystem::cpu_cpuset_cpus() { - GET_CONTAINER_INFO_CPTR(cptr, _unified, "/cpuset.cpus", - "cpuset.cpus is: %s", "%1023s", cpus, 1024); +char* CgroupV2Subsystem::cpu_cpuset_cpus() { + char cpus[1024]; + CONTAINER_READ_STRING_CHECKED(_unified, "/cpuset.cpus", "cpuset.cpus", cpus, 1024); return os::strdup(cpus); } -char* CgroupV2Subsystem::cpu_quota_val() { - GET_CONTAINER_INFO_CPTR(cptr, _unified, "/cpu.max", - "Raw value for CPU quota is: %s", "%1023s %*d", quota, 1024); - return os::strdup(quota); -} - -char * CgroupV2Subsystem::cpu_cpuset_memory_nodes() { - GET_CONTAINER_INFO_CPTR(cptr, _unified, "/cpuset.mems", - "cpuset.mems is: %s", "%1023s", mems, 1024); +char* CgroupV2Subsystem::cpu_cpuset_memory_nodes() { + char mems[1024]; + CONTAINER_READ_STRING_CHECKED(_unified, "/cpuset.mems", "cpuset.mems", mems, 1024); return os::strdup(mems); } int CgroupV2Subsystem::cpu_period() { - GET_CONTAINER_INFO(int, _unified, "/cpu.max", - "CPU Period is: ", "%d", "%*s %d", period); + jlong period_val; + bool is_ok = _unified->read_numerical_tuple_value("/cpu.max", false /* use_first */, &period_val); + if (!is_ok) { + log_trace(os, container)("CPU Period failed: %d", OSCONTAINER_ERROR); + return OSCONTAINER_ERROR; + } + int period = (int)period_val; + log_trace(os, container)("CPU Period is: %d", period); return period; } @@ -123,14 +128,15 @@ int CgroupV2Subsystem::cpu_period() { * OSCONTAINER_ERROR for not supported */ jlong CgroupV2Subsystem::memory_usage_in_bytes() { - GET_CONTAINER_INFO(jlong, _unified, "/memory.current", - "Memory Usage is: ", JLONG_FORMAT, JLONG_FORMAT, memusage); - return memusage; + julong memusage; + CONTAINER_READ_NUMBER_CHECKED(_unified, "/memory.current", "Memory Usage", memusage); + return (jlong)memusage; } jlong CgroupV2Subsystem::memory_soft_limit_in_bytes() { - char* mem_soft_limit_str = mem_soft_limit_val(); - return limit_from_str(mem_soft_limit_str); + jlong mem_soft_limit; + CONTAINER_READ_NUMBER_CHECKED_MAX(_unified, "/memory.low", "Memory Soft Limit", mem_soft_limit); + return mem_soft_limit; } jlong CgroupV2Subsystem::memory_max_usage_in_bytes() { @@ -139,26 +145,21 @@ jlong CgroupV2Subsystem::memory_max_usage_in_bytes() { return OSCONTAINER_ERROR; // not supported } -char* CgroupV2Subsystem::mem_soft_limit_val() { - GET_CONTAINER_INFO_CPTR(cptr, _unified, "/memory.low", - "Memory Soft Limit is: %s", "%1023s", mem_soft_limit_str, 1024); - return os::strdup(mem_soft_limit_str); -} - // Note that for cgroups v2 the actual limits set for swap and // memory live in two different files, memory.swap.max and memory.max // respectively. In order to properly report a cgroup v1 like // compound value we need to sum the two values. Setting a swap limit // without also setting a memory limit is not allowed. jlong CgroupV2Subsystem::memory_and_swap_limit_in_bytes() { - char* mem_swp_limit_str = mem_swp_limit_val(); - if (mem_swp_limit_str == nullptr) { + jlong swap_limit; + bool is_ok = _memory->controller()->read_number_handle_max("/memory.swap.max", &swap_limit); + if (!is_ok) { // Some container tests rely on this trace logging to happen. - log_trace(os, container)("Memory and Swap Limit is: %d", OSCONTAINER_ERROR); + log_trace(os, container)("Swap Limit failed: %d", OSCONTAINER_ERROR); // swap disabled at kernel level, treat it as no swap return read_memory_limit_in_bytes(); } - jlong swap_limit = limit_from_str(mem_swp_limit_str); + log_trace(os, container)("Swap Limit is: " JLONG_FORMAT, swap_limit); if (swap_limit >= 0) { jlong memory_limit = read_memory_limit_in_bytes(); assert(memory_limit >= 0, "swap limit without memory limit?"); @@ -168,17 +169,17 @@ jlong CgroupV2Subsystem::memory_and_swap_limit_in_bytes() { return swap_limit; } -char* CgroupV2Subsystem::mem_swp_limit_val() { - GET_CONTAINER_INFO_CPTR(cptr, _unified, "/memory.swap.max", - "Memory and Swap Limit is: %s", "%1023s", mem_swp_limit_str, 1024); - return os::strdup(mem_swp_limit_str); +jlong CgroupV2Subsystem::mem_swp_limit_val() { + jlong swap_limit; + CONTAINER_READ_NUMBER_CHECKED_MAX(_unified, "/memory.swap.max", "Swap Limit", swap_limit); + return swap_limit; } // memory.swap.current : total amount of swap currently used by the cgroup and its descendants -char* CgroupV2Subsystem::mem_swp_current_val() { - GET_CONTAINER_INFO_CPTR(cptr, _unified, "/memory.swap.current", - "Swap currently used is: %s", "%1023s", mem_swp_current_str, 1024); - return os::strdup(mem_swp_current_str); +jlong CgroupV2Subsystem::mem_swp_current_val() { + julong swap_current; + CONTAINER_READ_NUMBER_CHECKED(_unified, "/memory.swap.current", "Swap currently used", swap_current); + return (jlong)swap_current; } /* memory_limit_in_bytes @@ -190,30 +191,14 @@ char* CgroupV2Subsystem::mem_swp_current_val() { * -1 for unlimited, OSCONTAINER_ERROR for an error */ jlong CgroupV2Subsystem::read_memory_limit_in_bytes() { - char * mem_limit_str = mem_limit_val(); - jlong limit = limit_from_str(mem_limit_str); - if (log_is_enabled(Trace, os, container)) { - if (limit == -1) { - log_trace(os, container)("Memory Limit is: Unlimited"); - } else { - log_trace(os, container)("Memory Limit is: " JLONG_FORMAT, limit); - } - } - return limit; -} - -char* CgroupV2Subsystem::mem_limit_val() { - GET_CONTAINER_INFO_CPTR(cptr, _unified, "/memory.max", - "Raw value for memory limit is: %s", "%1023s", mem_limit_str, 1024); - return os::strdup(mem_limit_str); + jlong memory_limit; + CONTAINER_READ_NUMBER_CHECKED_MAX(_unified, "/memory.max", "Memory Limit", memory_limit); + return memory_limit; } void CgroupV2Subsystem::print_version_specific_info(outputStream* st) { - char* mem_swp_current_str = mem_swp_current_val(); - jlong swap_current = limit_from_str(mem_swp_current_str); - - char* mem_swp_limit_str = mem_swp_limit_val(); - jlong swap_limit = limit_from_str(mem_swp_limit_str); + jlong swap_current = mem_swp_current_val(); + jlong swap_limit = mem_swp_limit_val(); OSContainer::print_container_helper(st, swap_current, "memory_swap_current_in_bytes"); OSContainer::print_container_helper(st, swap_limit, "memory_swap_max_limit_in_bytes"); @@ -228,12 +213,6 @@ char* CgroupV2Controller::construct_path(char* mount_path, char *cgroup_path) { return os::strdup(ss.base()); } -char* CgroupV2Subsystem::pids_max_val() { - GET_CONTAINER_INFO_CPTR(cptr, _unified, "/pids.max", - "Maximum number of tasks is: %s", "%1023s", pidsmax, 1024); - return os::strdup(pidsmax); -} - /* pids_max * * Return the maximum number of tasks available to the process @@ -244,8 +223,9 @@ char* CgroupV2Subsystem::pids_max_val() { * OSCONTAINER_ERROR for not supported */ jlong CgroupV2Subsystem::pids_max() { - char * pidsmax_str = pids_max_val(); - return limit_from_str(pidsmax_str); + jlong pids_max; + CONTAINER_READ_NUMBER_CHECKED_MAX(_unified, "/pids.max", "Maximum number of tasks", pids_max); + return pids_max; } /* pids_current @@ -257,7 +237,7 @@ jlong CgroupV2Subsystem::pids_max() { * OSCONTAINER_ERROR for not supported */ jlong CgroupV2Subsystem::pids_current() { - GET_CONTAINER_INFO(jlong, _unified, "/pids.current", - "Current number of tasks is: ", JLONG_FORMAT, JLONG_FORMAT, pids_current); + julong pids_current; + CONTAINER_READ_NUMBER_CHECKED(_unified, "/pids.current", "Current number of tasks", pids_current); return pids_current; } diff --git a/src/hotspot/os/linux/cgroupV2Subsystem_linux.hpp b/src/hotspot/os/linux/cgroupV2Subsystem_linux.hpp index bb6b538c216..18d4a50ff0b 100644 --- a/src/hotspot/os/linux/cgroupV2Subsystem_linux.hpp +++ b/src/hotspot/os/linux/cgroupV2Subsystem_linux.hpp @@ -56,12 +56,8 @@ class CgroupV2Subsystem: public CgroupSubsystem { CachingCgroupController* _memory = nullptr; CachingCgroupController* _cpu = nullptr; - char *mem_limit_val(); - char *mem_swp_limit_val(); - char *mem_swp_current_val(); - char *mem_soft_limit_val(); - char *cpu_quota_val(); - char *pids_max_val(); + jlong mem_swp_limit_val(); + jlong mem_swp_current_val(); public: CgroupV2Subsystem(CgroupController * unified) { diff --git a/test/hotspot/gtest/os/linux/test_cgroupSubsystem_linux.cpp b/test/hotspot/gtest/os/linux/test_cgroupSubsystem_linux.cpp deleted file mode 100644 index 475b6eadb74..00000000000 --- a/test/hotspot/gtest/os/linux/test_cgroupSubsystem_linux.cpp +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code 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 General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -#include "precompiled.hpp" - -#ifdef LINUX - -#include "runtime/os.hpp" -#include "cgroupSubsystem_linux.hpp" -#include "unittest.hpp" -#include "utilities/globalDefinitions.hpp" - -#include - - -// Utilities -bool file_exists(const char* filename) { - struct stat st; - return os::stat(filename, &st) == 0; -} - -char* temp_file(const char* prefix) { - const testing::TestInfo* test_info = ::testing::UnitTest::GetInstance()->current_test_info(); - stringStream path; - path.print_raw(os::get_temp_directory()); - path.print_raw(os::file_separator()); - path.print("%s-test-jdk.pid%d.%s.%s", prefix, os::current_process_id(), - test_info->test_case_name(), test_info->name()); - return path.as_string(true); -} - -void delete_file(const char* filename) { - if (!file_exists(filename)) { - return; - } - int ret = remove(filename); - EXPECT_TRUE(ret == 0 || errno == ENOENT) << "failed to remove file '" << filename << "': " - << os::strerror(errno) << " (" << errno << ")"; -} - -class TestController : public CgroupController { -public: - char* subsystem_path() override { - // The real subsystem is in /tmp/, generated by temp_file() - return (char*)"/"; - }; -}; - -void fill_file(const char* path, const char* content) { - delete_file(path); - FILE* fp = os::fopen(path, "w"); - if (fp == nullptr) { - return; - } - if (content != nullptr) { - fprintf(fp, "%s", content); - } - fclose(fp); -} - -TEST(cgroupTest, SubSystemFileLineContentsMultipleLinesErrorCases) { - TestController my_controller{}; - const char* test_file = temp_file("cgroups"); - int x = 0; - char s[1024]; - int err = 0; - - s[0] = '\0'; - fill_file(test_file, "foo "); - err = subsystem_file_line_contents(&my_controller, test_file, "foo", "%s", &s); - EXPECT_NE(err, 0) << "Value must not be missing in key/value case"; - - s[0] = '\0'; - fill_file(test_file, "faulty_start foo bar"); - err = subsystem_file_line_contents(&my_controller, test_file, "foo", "%s", &s); - EXPECT_NE(err, 0) << "Key must be at start"; - - s[0] = '\0'; - fill_file(test_file, "foof bar"); - err = subsystem_file_line_contents(&my_controller, test_file, "foo", "%s", &s); - EXPECT_NE(err, 0) << "Key must be exact match"; - - // Cleanup - delete_file(test_file); -} - -TEST(cgroupTest, SubSystemFileLineContentsMultipleLinesSuccessCases) { - TestController my_controller{}; - const char* test_file = temp_file("cgroups"); - int x = 0; - char s[1024]; - int err = 0; - - s[0] = '\0'; - fill_file(test_file, "foo bar"); - err = subsystem_file_line_contents(&my_controller, test_file, "foo", "%s", &s); - EXPECT_EQ(err, 0); - EXPECT_STREQ(s, "bar") << "Incorrect!"; - - s[0] = '\0'; - fill_file(test_file, "foo\tbar"); - err = subsystem_file_line_contents(&my_controller, test_file, "foo", "%s", &s); - EXPECT_EQ(err, 0); - EXPECT_STREQ(s, "bar") << "Incorrect!"; - - s[0] = '\0'; - fill_file(test_file, "foof bar\nfoo car"); - err = subsystem_file_line_contents(&my_controller, test_file, "foo", "%s", &s); - EXPECT_EQ(err, 0); - EXPECT_STREQ(s, "car"); - - s[0] = '\0'; - fill_file(test_file, "foo\ttest\nfoot car"); - err = subsystem_file_line_contents(&my_controller, test_file, "foo", "%s", &s); - EXPECT_EQ(err, 0); - EXPECT_STREQ(s, "test"); - - s[0] = '\0'; - fill_file(test_file, "foo 1\nfoo car"); - err = subsystem_file_line_contents(&my_controller, test_file, "foo", "%s", &s); - EXPECT_EQ(err, 0); - EXPECT_STREQ(s, "1"); - - s[0] = '\0'; - fill_file(test_file, "max 10000"); - err = subsystem_file_line_contents(&my_controller, test_file, nullptr, "%s %*d", &s); - EXPECT_EQ(err, 0); - EXPECT_STREQ(s, "max"); - - x = -3; - fill_file(test_file, "max 10001"); - err = subsystem_file_line_contents(&my_controller, test_file, nullptr, "%*s %d", &x); - EXPECT_EQ(err, 0); - EXPECT_EQ(x, 10001); - - // Cleanup - delete_file(test_file); -} - -TEST(cgroupTest, SubSystemFileLineContentsSingleLine) { - TestController my_controller{}; - const char* test_file = temp_file("cgroups"); - int x = 0; - char s[1024]; - int err = 0; - - fill_file(test_file, "foo"); - err = subsystem_file_line_contents(&my_controller, test_file, nullptr, "%s", &s); - EXPECT_EQ(err, 0); - EXPECT_STREQ(s, "foo"); - - fill_file(test_file, "1337"); - err = subsystem_file_line_contents(&my_controller, test_file, nullptr, "%d", &x); - EXPECT_EQ(err, 0); - EXPECT_EQ(x, 1337) << "Wrong value for x"; - - s[0] = '\0'; - fill_file(test_file, "1337"); - err = subsystem_file_line_contents(&my_controller, test_file, nullptr, "%s", &s); - EXPECT_EQ(err, 0); - EXPECT_STREQ(s, "1337"); - - x = -1; - fill_file(test_file, nullptr); - err = subsystem_file_line_contents(&my_controller, test_file, nullptr, "%d", &x); - EXPECT_NE(err, 0) << "Empty file should've failed"; - EXPECT_EQ(x, -1) << "x was altered"; - - jlong y; - fill_file(test_file, "1337"); - err = subsystem_file_line_contents(&my_controller, test_file, nullptr, JLONG_FORMAT, &y); - EXPECT_EQ(err, 0); - EXPECT_EQ(y, 1337) << "Wrong value for y"; - julong z; - fill_file(test_file, "1337"); - err = subsystem_file_line_contents(&my_controller, test_file, nullptr, JULONG_FORMAT, &z); - EXPECT_EQ(err, 0); - EXPECT_EQ(z, (julong)1337) << "Wrong value for z"; - - // Cleanup - delete_file(test_file); -} - -#endif // LINUX diff --git a/test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp b/test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp new file mode 100644 index 00000000000..cc326dbb502 --- /dev/null +++ b/test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp @@ -0,0 +1,464 @@ +/* + * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "precompiled.hpp" + +#ifdef LINUX + +#include "runtime/os.hpp" +#include "cgroupSubsystem_linux.hpp" +#include "cgroupV1Subsystem_linux.hpp" +#include "cgroupV2Subsystem_linux.hpp" +#include "unittest.hpp" +#include "utilities/globalDefinitions.hpp" + +#include + +typedef struct { + const char* mount_path; + const char* root_path; + const char* cgroup_path; + const char* expected_path; +} TestCase; + +// Utilities +static bool file_exists(const char* filename) { + struct stat st; + return os::stat(filename, &st) == 0; +} + +static char* temp_file(const char* prefix) { + const testing::TestInfo* test_info = ::testing::UnitTest::GetInstance()->current_test_info(); + stringStream path; + path.print_raw(os::get_temp_directory()); + path.print_raw(os::file_separator()); + path.print("%s-test-jdk.pid%d.%s.%s", prefix, os::current_process_id(), + test_info->test_case_name(), test_info->name()); + return path.as_string(true); +} + +static void delete_file(const char* filename) { + if (!file_exists(filename)) { + return; + } + int ret = remove(filename); + EXPECT_TRUE(ret == 0 || errno == ENOENT) << "failed to remove file '" << filename << "': " + << os::strerror(errno) << " (" << errno << ")"; +} + +class TestController : public CgroupController { +private: + char* _path; +public: + TestController(char* p): _path(p) {} + char* subsystem_path() override { + return _path; + }; +}; + +static void fill_file(const char* path, const char* content) { + delete_file(path); + FILE* fp = os::fopen(path, "w"); + if (fp == nullptr) { + return; + } + if (content != nullptr) { + fprintf(fp, "%s", content); + } + fclose(fp); +} + +TEST(cgroupTest, read_numerical_key_value_failure_cases) { + const char* test_file = temp_file("cgroups"); + const char* b = basename(test_file); + EXPECT_TRUE(b != nullptr) << "basename was null"; + stringStream path; + path.print_raw(os::file_separator()); + path.print_raw(b); + const char* base_with_slash = path.as_string(true); + + TestController* controller = new TestController((char*)os::get_temp_directory()); + constexpr julong bad = 0xBAD; + julong x = bad; + + fill_file(test_file, "foo "); + bool is_ok = controller->read_numerical_key_value(base_with_slash, "foo", &x); + EXPECT_FALSE(is_ok) << "Value is missing in key/value case, expecting false"; + EXPECT_EQ(bad, x) << "x must be unchanged"; + + x = bad; + fill_file(test_file, "faulty_start foo 101"); + is_ok = controller->read_numerical_key_value(base_with_slash, "foo", &x); + EXPECT_FALSE(is_ok) << "key must be at the start"; + EXPECT_EQ(bad, x) << "x must be unchanged"; + + x = bad; + fill_file(test_file, nullptr); + is_ok = controller->read_numerical_key_value(base_with_slash, "foo", &x); + EXPECT_FALSE(is_ok) << "key not in empty file"; + EXPECT_EQ(bad, x) << "x must be unchanged"; + + x = bad; + fill_file(test_file, "foo\n"); + is_ok = controller->read_numerical_key_value(base_with_slash, "foo", &x); + EXPECT_FALSE(is_ok) << "key must have a value"; + EXPECT_EQ(bad, x) << "x must be unchanged"; + + x = bad; + fill_file(test_file, "foof 1002"); + is_ok = controller->read_numerical_key_value(base_with_slash, "foo", &x); + EXPECT_FALSE(is_ok) << "key must be exact match"; + EXPECT_EQ(bad, x) << "x must be unchanged"; + + // Cleanup + delete_file(test_file); +} + +TEST(cgroupTest, read_numerical_key_value_success_cases) { + const char* test_file = temp_file("cgroups"); + const char* b = basename(test_file); + EXPECT_TRUE(b != nullptr) << "basename was null"; + stringStream path; + path.print_raw(os::file_separator()); + path.print_raw(b); + const char* base_with_slash = path.as_string(true); + + TestController* controller = new TestController((char*)os::get_temp_directory()); + constexpr julong bad = 0xBAD; + julong x = bad; + + fill_file(test_file, "foo 100"); + bool is_ok = controller->read_numerical_key_value(base_with_slash, "foo", &x); + EXPECT_TRUE(is_ok); + EXPECT_EQ((julong)100, x); + + x = bad; + fill_file(test_file, "foo\t111"); + is_ok = controller->read_numerical_key_value(base_with_slash, "foo", &x); + EXPECT_TRUE(is_ok); + EXPECT_EQ((julong)111, x); + + x = bad; + fill_file(test_file, "foo\nbar 333\nfoo\t111"); + is_ok = controller->read_numerical_key_value(base_with_slash, "foo", &x); + EXPECT_TRUE(is_ok); + EXPECT_EQ((julong)111, x); + + x = bad; + fill_file(test_file, "foof 100\nfoo 133"); + is_ok = controller->read_numerical_key_value(base_with_slash, "foo", &x); + EXPECT_TRUE(is_ok); + EXPECT_EQ((julong)133, x); + + x = bad; + fill_file(test_file, "foo\t333\nfoot 999"); + is_ok = controller->read_numerical_key_value(base_with_slash, "foo", &x); + EXPECT_TRUE(is_ok); + EXPECT_EQ((julong)333, x); + + x = bad; + fill_file(test_file, "foo 1\nfoo car"); + is_ok = controller->read_numerical_key_value(base_with_slash, "foo", &x); + EXPECT_TRUE(is_ok); + EXPECT_EQ((julong)1, x); + + // Cleanup + delete_file(test_file); +} + +TEST(cgroupTest, read_number_null) { + TestController* null_path_controller = new TestController((char*)nullptr); + const char* test_file_path = "/not-used"; + constexpr julong bad = 0xBAD; + julong a = bad; + // null subsystem_path() case + bool is_ok = null_path_controller->read_number(test_file_path, &a); + EXPECT_FALSE(is_ok) << "Null subsystem path should be an error"; + EXPECT_EQ(bad, a) << "Expected untouched scan value"; +} + +TEST(cgroupTest, read_string_beyond_max_path) { + char larger_than_max[MAXPATHLEN + 1]; + for (int i = 0; i < (MAXPATHLEN); i++) { + larger_than_max[i] = 'A' + (i % 26); + } + larger_than_max[MAXPATHLEN] = '\0'; + TestController* too_large_path_controller = new TestController(larger_than_max); + const char* test_file_path = "/file-not-found"; + char foo[1024]; + foo[0] = '\0'; + bool is_ok = too_large_path_controller->read_string(test_file_path, foo, 1024); + EXPECT_FALSE(is_ok) << "Too long path should be an error"; + EXPECT_STREQ("", foo) << "Expected untouched scan value"; +} + +TEST(cgroupTest, read_number_file_not_exist) { + TestController* unknown_path_ctrl = new TestController((char*)"/do/not/exist"); + const char* test_file_path = "/file-not-found"; + constexpr julong bad = 0xBAD; + julong result = bad; + bool is_ok = unknown_path_ctrl->read_number(test_file_path, &result); + EXPECT_FALSE(is_ok) << "File not found should be an error"; + EXPECT_EQ(bad, result) << "Expected untouched scan value"; +} + +TEST(cgroupTest, read_numerical_key_value_null) { + TestController* null_path_controller = new TestController((char*)nullptr); + const char* test_file_path = "/not-used"; + const char* key = "something"; + constexpr julong bad = 0xBAD; + julong a = bad; + // null subsystem_path() case + bool is_ok = null_path_controller->read_numerical_key_value(test_file_path, key, &a); + EXPECT_FALSE(is_ok) << "Null subsystem path should be an error"; + EXPECT_EQ(bad, a) << "Expected untouched scan value"; +} + +TEST(cgroupTest, read_number_tests) { + const char* test_file = temp_file("cgroups"); + const char* b = basename(test_file); + constexpr julong bad = 0xBAD; + EXPECT_TRUE(b != nullptr) << "basename was null"; + stringStream path; + path.print_raw(os::file_separator()); + path.print_raw(b); + const char* base_with_slash = path.as_string(true); + fill_file(test_file, "8888"); + + TestController* controller = new TestController((char*)os::get_temp_directory()); + julong foo = bad; + bool ok = controller->read_number(base_with_slash, &foo); + EXPECT_TRUE(ok) << "Number parsing should have been successful"; + EXPECT_EQ((julong)8888, foo) << "Wrong value for 'foo' (NOTE: 0xBAD == " << 0xBAD << ")"; + + // Some interface files might have negative values, ensure we can read + // them and manually cast them as needed. + fill_file(test_file, "-1"); + foo = bad; + ok = controller->read_number(base_with_slash, &foo); + EXPECT_TRUE(ok) << "Number parsing should have been successful"; + EXPECT_EQ((jlong)-1, (jlong)foo) << "Wrong value for 'foo' (NOTE: 0xBAD == " << 0xBAD << ")"; + + foo = bad; + fill_file(test_file, nullptr); + ok = controller->read_number(base_with_slash, &foo); + EXPECT_FALSE(ok) << "Empty file should have failed"; + EXPECT_EQ(bad, foo) << "foo was altered"; + + // Some interface files have numbers as well as the string + // 'max', which means unlimited. + jlong result = -10; + fill_file(test_file, "max\n"); + ok = controller->read_number_handle_max(base_with_slash, &result); + EXPECT_TRUE(ok) << "Number parsing for 'max' string should have been successful"; + EXPECT_EQ((jlong)-1, result) << "'max' means unlimited (-1)"; + + result = -10; + fill_file(test_file, "11114\n"); + ok = controller->read_number_handle_max(base_with_slash, &result); + EXPECT_TRUE(ok) << "Number parsing for should have been successful"; + EXPECT_EQ((jlong)11114, result) << "Incorrect result"; + + result = -10; + fill_file(test_file, "-51114\n"); + ok = controller->read_number_handle_max(base_with_slash, &result); + EXPECT_TRUE(ok) << "Number parsing for should have been successful"; + EXPECT_EQ((jlong)-51114, result) << "Incorrect result"; + + delete_file(test_file); +} + +TEST(cgroupTest, read_string_tests) { + const char* test_file = temp_file("cgroups"); + const char* b = basename(test_file); + EXPECT_TRUE(b != nullptr) << "basename was null"; + stringStream path; + path.print_raw(os::file_separator()); + path.print_raw(b); + const char* base_with_slash = path.as_string(true); + fill_file(test_file, "foo-bar"); + + TestController* controller = new TestController((char*)os::get_temp_directory()); + char result[1024]; + bool ok = controller->read_string(base_with_slash, result, 1024); + EXPECT_TRUE(ok) << "String parsing should have been successful"; + EXPECT_STREQ("foo-bar", result); + + result[0] = '\0'; + fill_file(test_file, "1234"); + ok = controller->read_string(base_with_slash, result, 1024); + EXPECT_TRUE(ok) << "String parsing should have been successful"; + EXPECT_STREQ("1234", result); + + // values with a space + result[0] = '\0'; + fill_file(test_file, "abc def"); + ok = controller->read_string(base_with_slash, result, 1024); + EXPECT_TRUE(ok) << "String parsing should have been successful"; + EXPECT_STREQ("abc def", result); + + result[0] = '\0'; + fill_file(test_file, " \na"); + ok = controller->read_string(base_with_slash, result, 1024); + EXPECT_TRUE(ok) << "String parsing should have been successful"; + EXPECT_STREQ(" ", result); + + // only the first line are being returned + result[0] = '\0'; + fill_file(test_file, "test\nabc"); + ok = controller->read_string(base_with_slash, result, 1024); + EXPECT_TRUE(ok) << "String parsing should have been successful"; + EXPECT_STREQ("test", result); + + result[0] = '\0'; + fill_file(test_file, nullptr); + ok = controller->read_string(base_with_slash, result, 1024); + EXPECT_FALSE(ok) << "Empty file should have failed"; + EXPECT_STREQ("", result) << "Expected untouched result"; + delete_file(test_file); + + // File contents larger than 1K + // We only read in the first 1K - 1 bytes + const size_t large_len = 2 * 1024; + char too_large[large_len]; + for (size_t i = 0; i < large_len; i++) { + too_large[i] = 'A' + (i % 26); + } + too_large[large_len - 1] = '\0'; + result[0] = '\0'; + fill_file(test_file, too_large); + ok = controller->read_string(base_with_slash, result, 1024); + EXPECT_TRUE(ok) << "String parsing should have been successful"; + EXPECT_TRUE(1023 == strlen(result)) << "Expected only the first 1023 chars to be read in"; + EXPECT_EQ(0, strncmp(too_large, result, 1023)); + EXPECT_EQ(result[1023], '\0') << "The last character must be the null character"; +} + +TEST(cgroupTest, read_number_tuple_test) { + const char* test_file = temp_file("cgroups"); + const char* b = basename(test_file); + EXPECT_TRUE(b != nullptr) << "basename was null"; + stringStream path; + path.print_raw(os::file_separator()); + path.print_raw(b); + const char* base_with_slash = path.as_string(true); + fill_file(test_file, "max 10000"); + + TestController* controller = new TestController((char*)os::get_temp_directory()); + jlong result = -10; + bool ok = controller->read_numerical_tuple_value(base_with_slash, true /* use_first */, &result); + EXPECT_TRUE(ok) << "Should be OK to read value"; + EXPECT_EQ((jlong)-1, result) << "max should be unlimited (-1)"; + + result = -10; + ok = controller->read_numerical_tuple_value(base_with_slash, false /* use_first */, &result); + EXPECT_TRUE(ok) << "Should be OK to read the value"; + EXPECT_EQ((jlong)10000, result); + + // non-max strings + fill_file(test_file, "abc 10000"); + result = -10; + ok = controller->read_numerical_tuple_value(base_with_slash, true /* use_first */, &result); + EXPECT_FALSE(ok) << "abc should not be parsable"; + EXPECT_EQ((jlong)-10, result) << "result value should be unchanged"; + + fill_file(test_file, nullptr); + result = -10; + ok = controller->read_numerical_tuple_value(base_with_slash, true /* use_first */, &result); + EXPECT_FALSE(ok) << "Empty file should be an error"; + EXPECT_EQ((jlong)-10, result) << "result value should be unchanged"; +} + +TEST(cgroupTest, read_numerical_key_beyond_max_path) { + char larger_than_max[MAXPATHLEN + 1]; + for (int i = 0; i < (MAXPATHLEN); i++) { + larger_than_max[i] = 'A' + (i % 26); + } + larger_than_max[MAXPATHLEN] = '\0'; + TestController* too_large_path_controller = new TestController(larger_than_max); + const char* test_file_path = "/file-not-found"; + const char* key = "something"; + julong a = 0xBAD; + bool is_ok = too_large_path_controller->read_numerical_key_value(test_file_path, key, &a); + EXPECT_FALSE(is_ok) << "Too long path should be an error"; + EXPECT_EQ((julong)0xBAD, a) << "Expected untouched scan value"; +} + +TEST(cgroupTest, read_numerical_key_file_not_exist) { + TestController* unknown_path_ctrl = new TestController((char*)"/do/not/exist"); + const char* test_file_path = "/file-not-found"; + const char* key = "something"; + julong a = 0xBAD; + bool is_ok = unknown_path_ctrl->read_numerical_key_value(test_file_path, key, &a); + EXPECT_FALSE(is_ok) << "File not found should be an error"; + EXPECT_EQ((julong)0xBAD, a) << "Expected untouched scan value"; +} + +TEST(cgroupTest, set_cgroupv1_subsystem_path) { + TestCase host = { + "/sys/fs/cgroup/memory", // mount_path + "/", // root_path + "/user.slice/user-1000.slice/user@1000.service", // cgroup_path + "/sys/fs/cgroup/memory/user.slice/user-1000.slice/user@1000.service" // expected_path + }; + TestCase container_engine = { + "/sys/fs/cgroup/mem", // mount_path + "/user.slice/user-1000.slice/user@1000.service", // root_path + "/user.slice/user-1000.slice/user@1000.service", // cgroup_path + "/sys/fs/cgroup/mem" // expected_path + }; + int length = 2; + TestCase* testCases[] = { &host, + &container_engine }; + for (int i = 0; i < length; i++) { + CgroupV1Controller* ctrl = new CgroupV1Controller( (char*)testCases[i]->root_path, + (char*)testCases[i]->mount_path); + ctrl->set_subsystem_path((char*)testCases[i]->cgroup_path); + ASSERT_STREQ(testCases[i]->expected_path, ctrl->subsystem_path()); + } +} + +TEST(cgroupTest, set_cgroupv2_subsystem_path) { + TestCase at_mount_root = { + "/sys/fs/cgroup", // mount_path + nullptr, // root_path, ignored + "/", // cgroup_path + "/sys/fs/cgroup" // expected_path + }; + TestCase sub_path = { + "/sys/fs/cgroup", // mount_path + nullptr, // root_path, ignored + "/foobar", // cgroup_path + "/sys/fs/cgroup/foobar" // expected_path + }; + int length = 2; + TestCase* testCases[] = { &at_mount_root, + &sub_path }; + for (int i = 0; i < length; i++) { + CgroupV2Controller* ctrl = new CgroupV2Controller( (char*)testCases[i]->mount_path, + (char*)testCases[i]->cgroup_path); + ASSERT_STREQ(testCases[i]->expected_path, ctrl->subsystem_path()); + } +} + +#endif // LINUX diff --git a/test/hotspot/gtest/runtime/test_os_linux_cgroups.cpp b/test/hotspot/gtest/runtime/test_os_linux_cgroups.cpp deleted file mode 100644 index 21e0152a43d..00000000000 --- a/test/hotspot/gtest/runtime/test_os_linux_cgroups.cpp +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2022, Red Hat, Inc. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code 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 General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -#include "precompiled.hpp" - -#ifdef LINUX - -#include "cgroupV1Subsystem_linux.hpp" -#include "cgroupV2Subsystem_linux.hpp" -#include "unittest.hpp" - -typedef struct { - const char* mount_path; - const char* root_path; - const char* cgroup_path; - const char* expected_path; -} TestCase; - -TEST(cgroupTest, set_cgroupv1_subsystem_path) { - TestCase host = { - "/sys/fs/cgroup/memory", // mount_path - "/", // root_path - "/user.slice/user-1000.slice/user@1000.service", // cgroup_path - "/sys/fs/cgroup/memory/user.slice/user-1000.slice/user@1000.service" // expected_path - }; - TestCase container_engine = { - "/sys/fs/cgroup/mem", // mount_path - "/user.slice/user-1000.slice/user@1000.service", // root_path - "/user.slice/user-1000.slice/user@1000.service", // cgroup_path - "/sys/fs/cgroup/mem" // expected_path - }; - int length = 2; - TestCase* testCases[] = { &host, - &container_engine }; - for (int i = 0; i < length; i++) { - CgroupV1Controller* ctrl = new CgroupV1Controller( (char*)testCases[i]->root_path, - (char*)testCases[i]->mount_path); - ctrl->set_subsystem_path((char*)testCases[i]->cgroup_path); - ASSERT_STREQ(testCases[i]->expected_path, ctrl->subsystem_path()); - } -} - -TEST(cgroupTest, set_cgroupv2_subsystem_path) { - TestCase at_mount_root = { - "/sys/fs/cgroup", // mount_path - nullptr, // root_path, ignored - "/", // cgroup_path - "/sys/fs/cgroup" // expected_path - }; - TestCase sub_path = { - "/sys/fs/cgroup", // mount_path - nullptr, // root_path, ignored - "/foobar", // cgroup_path - "/sys/fs/cgroup/foobar" // expected_path - }; - int length = 2; - TestCase* testCases[] = { &at_mount_root, - &sub_path }; - for (int i = 0; i < length; i++) { - CgroupV2Controller* ctrl = new CgroupV2Controller( (char*)testCases[i]->mount_path, - (char*)testCases[i]->cgroup_path); - ASSERT_STREQ(testCases[i]->expected_path, ctrl->subsystem_path()); - } -} - -#endif diff --git a/test/hotspot/jtreg/containers/docker/TestMemoryAwareness.java b/test/hotspot/jtreg/containers/docker/TestMemoryAwareness.java index 553ba692ee7..20354cf934d 100644 --- a/test/hotspot/jtreg/containers/docker/TestMemoryAwareness.java +++ b/test/hotspot/jtreg/containers/docker/TestMemoryAwareness.java @@ -200,8 +200,8 @@ private static void testMemorySwapLimitSanity() throws Exception { .shouldMatch("Memory Limit is:.*" + expectedTraceValue) // Either for cgroup v1: a_1) same as memory limit, or b_1) -2 on systems with swapaccount=0 // Either for cgroup v2: a_2) 0, or b_2) -2 on systems with swapaccount=0 - .shouldMatch("Memory and Swap Limit is:.*(" + expectedTraceValue + "|-2|0)") - .shouldNotMatch("Memory and Swap Limit is:.*" + neg2InUnsignedLong); + .shouldMatch("(Memory and )?Swap Limit is:.*(" + expectedTraceValue + "|-2|0)") + .shouldNotMatch("(Memory and )?Swap Limit is:.*" + neg2InUnsignedLong); } From 007b97efd79152d368fef24a9cae8683196e64a7 Mon Sep 17 00:00:00 2001 From: Matthias Baesken Date: Wed, 5 Jun 2024 07:11:27 +0000 Subject: [PATCH 2/4] 8333326: Linux Alpine build fails after 8302744 Reviewed-by: sgehwolf, clanger, stuefe --- .../gtest/runtime/test_cgroupSubsystem_linux.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp b/test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp index cc326dbb502..aa1d2a19b28 100644 --- a/test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp +++ b/test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp @@ -34,6 +34,9 @@ #include +// for basename +#include + typedef struct { const char* mount_path; const char* root_path; @@ -47,6 +50,7 @@ static bool file_exists(const char* filename) { return os::stat(filename, &st) == 0; } +// we rely on temp_file returning modifiable memory in resource area. static char* temp_file(const char* prefix) { const testing::TestInfo* test_info = ::testing::UnitTest::GetInstance()->current_test_info(); stringStream path; @@ -89,7 +93,7 @@ static void fill_file(const char* path, const char* content) { } TEST(cgroupTest, read_numerical_key_value_failure_cases) { - const char* test_file = temp_file("cgroups"); + char* test_file = temp_file("cgroups"); const char* b = basename(test_file); EXPECT_TRUE(b != nullptr) << "basename was null"; stringStream path; @@ -135,7 +139,7 @@ TEST(cgroupTest, read_numerical_key_value_failure_cases) { } TEST(cgroupTest, read_numerical_key_value_success_cases) { - const char* test_file = temp_file("cgroups"); + char* test_file = temp_file("cgroups"); const char* b = basename(test_file); EXPECT_TRUE(b != nullptr) << "basename was null"; stringStream path; @@ -235,7 +239,7 @@ TEST(cgroupTest, read_numerical_key_value_null) { } TEST(cgroupTest, read_number_tests) { - const char* test_file = temp_file("cgroups"); + char* test_file = temp_file("cgroups"); const char* b = basename(test_file); constexpr julong bad = 0xBAD; EXPECT_TRUE(b != nullptr) << "basename was null"; @@ -289,7 +293,7 @@ TEST(cgroupTest, read_number_tests) { } TEST(cgroupTest, read_string_tests) { - const char* test_file = temp_file("cgroups"); + char* test_file = temp_file("cgroups"); const char* b = basename(test_file); EXPECT_TRUE(b != nullptr) << "basename was null"; stringStream path; @@ -355,7 +359,7 @@ TEST(cgroupTest, read_string_tests) { } TEST(cgroupTest, read_number_tuple_test) { - const char* test_file = temp_file("cgroups"); + char* test_file = temp_file("cgroups"); const char* b = basename(test_file); EXPECT_TRUE(b != nullptr) << "basename was null"; stringStream path; From 1864bf5e2144c9eee55834a9d0601dd53fe7ef42 Mon Sep 17 00:00:00 2001 From: Jan Kratochvil Date: Wed, 6 Nov 2024 22:55:43 +0800 Subject: [PATCH 3/4] src/java.base/linux/native/libjava/CgroupMetrics.c:42:(.text+0x11): undefined reference to `JVM_IsContainerized' - a patch by Yuri Nesterenko --- make/data/hotspot-symbols/symbols-linux | 1 + 1 file changed, 1 insertion(+) diff --git a/make/data/hotspot-symbols/symbols-linux b/make/data/hotspot-symbols/symbols-linux index d1f258297d8..82fdc5cc4db 100644 --- a/make/data/hotspot-symbols/symbols-linux +++ b/make/data/hotspot-symbols/symbols-linux @@ -22,6 +22,7 @@ # JVM_handle_linux_signal +JVM_IsContainerized JVM_IsUseContainerSupport numa_error numa_warn From 0aaf6294cf5f8a13ad2e0891cea5626ddbfbedc4 Mon Sep 17 00:00:00 2001 From: Severin Gehwolf Date: Mon, 1 Jul 2024 08:47:29 +0000 Subject: [PATCH 4/4] 8261242: [Linux] OSContainer::is_containerized() returns true when run outside a container Reviewed-by: stuefe, iklam --- .../os/linux/cgroupSubsystem_linux.cpp | 119 +++++++++++++++--- .../os/linux/cgroupSubsystem_linux.hpp | 7 +- .../os/linux/cgroupV1Subsystem_linux.cpp | 11 ++ .../os/linux/cgroupV1Subsystem_linux.hpp | 8 +- .../os/linux/cgroupV2Subsystem_linux.cpp | 4 + .../os/linux/cgroupV2Subsystem_linux.hpp | 6 +- src/hotspot/os/linux/osContainer_linux.cpp | 39 +++++- src/hotspot/share/include/jvm.h | 3 + src/hotspot/share/prims/jvm.cpp | 12 ++ .../jdk/internal/platform/CgroupMetrics.java | 6 + .../internal/platform/CgroupSubsystem.java | 5 + .../linux/native/libjava/CgroupMetrics.c | 6 + .../jdk/internal/platform/Metrics.java | 17 +++ .../classes/sun/launcher/LauncherHelper.java | 4 + .../runtime/test_cgroupSubsystem_linux.cpp | 9 +- ...{PlainRead.java => TestContainerized.java} | 42 +------ .../platform/cgroup/TestSystemSettings.java | 45 +++++++ .../lib/containers/cgroup/MetricsTester.java | 10 ++ 18 files changed, 290 insertions(+), 63 deletions(-) rename test/hotspot/jtreg/containers/cgroup/{PlainRead.java => TestContainerized.java} (54%) create mode 100644 test/jdk/jdk/internal/platform/cgroup/TestSystemSettings.java diff --git a/src/hotspot/os/linux/cgroupSubsystem_linux.cpp b/src/hotspot/os/linux/cgroupSubsystem_linux.cpp index a348ea424fb..d97fc345600 100644 --- a/src/hotspot/os/linux/cgroupSubsystem_linux.cpp +++ b/src/hotspot/os/linux/cgroupSubsystem_linux.cpp @@ -63,7 +63,9 @@ CgroupSubsystem* CgroupSubsystemFactory::create() { // Construct the subsystem, free resources and return // Note: any index in cg_infos will do as the path is the same for // all controllers. - CgroupController* unified = new CgroupV2Controller(cg_infos[MEMORY_IDX]._mount_path, cg_infos[MEMORY_IDX]._cgroup_path); + CgroupController* unified = new CgroupV2Controller(cg_infos[MEMORY_IDX]._mount_path, + cg_infos[MEMORY_IDX]._cgroup_path, + cg_infos[MEMORY_IDX]._read_only); log_debug(os, container)("Detected cgroups v2 unified hierarchy"); cleanup(cg_infos); return new CgroupV2Subsystem(unified); @@ -100,19 +102,19 @@ CgroupSubsystem* CgroupSubsystemFactory::create() { CgroupInfo info = cg_infos[i]; if (info._data_complete) { // pids controller might have incomplete data if (strcmp(info._name, "memory") == 0) { - memory = new CgroupV1MemoryController(info._root_mount_path, info._mount_path); + memory = new CgroupV1MemoryController(info._root_mount_path, info._mount_path, info._read_only); memory->set_subsystem_path(info._cgroup_path); } else if (strcmp(info._name, "cpuset") == 0) { - cpuset = new CgroupV1Controller(info._root_mount_path, info._mount_path); + cpuset = new CgroupV1Controller(info._root_mount_path, info._mount_path, info._read_only); cpuset->set_subsystem_path(info._cgroup_path); } else if (strcmp(info._name, "cpu") == 0) { - cpu = new CgroupV1Controller(info._root_mount_path, info._mount_path); + cpu = new CgroupV1Controller(info._root_mount_path, info._mount_path, info._read_only); cpu->set_subsystem_path(info._cgroup_path); } else if (strcmp(info._name, "cpuacct") == 0) { - cpuacct = new CgroupV1Controller(info._root_mount_path, info._mount_path); + cpuacct = new CgroupV1Controller(info._root_mount_path, info._mount_path, info._read_only); cpuacct->set_subsystem_path(info._cgroup_path); } else if (strcmp(info._name, "pids") == 0) { - pids = new CgroupV1Controller(info._root_mount_path, info._mount_path); + pids = new CgroupV1Controller(info._root_mount_path, info._mount_path, info._read_only); pids->set_subsystem_path(info._cgroup_path); } } else { @@ -127,7 +129,8 @@ void CgroupSubsystemFactory::set_controller_paths(CgroupInfo* cg_infos, int controller, const char* name, char* mount_path, - char* root_path) { + char* root_path, + bool read_only) { if (cg_infos[controller]._mount_path != nullptr) { // On some systems duplicate controllers get mounted in addition to // the main cgroup controllers most likely under /sys/fs/cgroup. In that @@ -139,6 +142,7 @@ void CgroupSubsystemFactory::set_controller_paths(CgroupInfo* cg_infos, os::free(cg_infos[controller]._root_mount_path); cg_infos[controller]._mount_path = os::strdup(mount_path); cg_infos[controller]._root_mount_path = os::strdup(root_path); + cg_infos[controller]._read_only = read_only; } else { log_debug(os, container)("Duplicate %s controllers detected. Picking %s, skipping %s.", name, cg_infos[controller]._mount_path, mount_path); @@ -146,9 +150,66 @@ void CgroupSubsystemFactory::set_controller_paths(CgroupInfo* cg_infos, } else { cg_infos[controller]._mount_path = os::strdup(mount_path); cg_infos[controller]._root_mount_path = os::strdup(root_path); + cg_infos[controller]._read_only = read_only; } } +/* + * Determine whether or not the mount options, which are comma separated, + * contain the 'ro' string. + */ +static bool find_ro_opt(char* mount_opts) { + char* token; + char* mo_ptr = mount_opts; + // mount options are comma-separated (man proc). + while ((token = strsep(&mo_ptr, ",")) != NULL) { + if (strcmp(token, "ro") == 0) { + return true; + } + } + return false; +} + +/* + * Read values of a /proc/self/mountinfo line into variables. For cgroups v1 + * super options are needed. On cgroups v2 super options are not used. + * + * The scanning of a single mountinfo line entry is as follows: + * + * 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue + * (1) (2) (3):(4) (5) (6) (7) (8) (9) (10) (11) (12) + * + * The numbers in parentheses are labels for the descriptions below: + * + * (1) mount ID: matched with '%*d' and discarded + * (2) parent ID: matched with '%*d' and discarded + * (3) major: ---,---> major, minor separated by ':'. matched with '%*d:%*d' and discarded + * (4) minor: ---' + * (5) root: matched with '%s' and captured in 'tmproot'. Must be non-empty. + * (6) mount point: matched with '%s' and captured in 'tmpmount'. Must be non-empty. + * (7) mount options: matched with '%s' and captured in 'mount_opts'. Must be non-empty. + * (8) optional fields: ---,---> matched with '%*[^-]-'. Anything not a hyphen, followed by a hyphen + * (9) separator: ---' and discarded. Note: The discarded match is space characters if there + * are no optionals. Otherwise it includes the optional fields as well. + * (10) filesystem type: matched with '%s' and captured in 'tmp_fs_type' + * (11) mount source: matched with '%*s' and discarded + * (12) super options: matched with '%s' and captured in 'tmpcgroups' + */ +static inline bool match_mount_info_line(char* line, + char* tmproot, + char* tmpmount, + char* mount_opts, + char* tmp_fs_type, + char* tmpcgroups) { + return sscanf(line, + "%*d %*d %*d:%*d %s %s %s%*[^-]- %s %*s %s", + tmproot, + tmpmount, + mount_opts, + tmp_fs_type, + tmpcgroups) == 5; +} + bool CgroupSubsystemFactory::determine_type(CgroupInfo* cg_infos, const char* proc_cgroups, const char* proc_self_cgroup, @@ -320,26 +381,40 @@ bool CgroupSubsystemFactory::determine_type(CgroupInfo* cg_infos, char tmproot[MAXPATHLEN+1]; char tmpmount[MAXPATHLEN+1]; char tmpcgroups[MAXPATHLEN+1]; + char mount_opts[MAXPATHLEN+1]; char *cptr = tmpcgroups; char *token; - // Cgroup v2 relevant info. We only look for the _mount_path iff is_cgroupsV2 so - // as to avoid memory stomping of the _mount_path pointer later on in the cgroup v1 - // block in the hybrid case. - if (is_cgroupsV2 && sscanf(p, "%*d %*d %*d:%*d %s %s %*[^-]- %s %*s %*s", tmproot, tmpmount, tmp_fs_type) == 3) { + /* Cgroup v2 relevant info. We only look for the _mount_path iff is_cgroupsV2 so + * as to avoid memory stomping of the _mount_path pointer later on in the cgroup v1 + * block in the hybrid case. + * + * We collect the read only mount option in the cgroup infos so as to have that + * info ready when determining is_containerized(). + */ + if (is_cgroupsV2 && match_mount_info_line(p, + tmproot, + tmpmount, + mount_opts, + tmp_fs_type, + tmpcgroups /* unused */)) { // we likely have an early match return (e.g. cgroup fs match), be sure we have cgroup2 as fstype if (strcmp("cgroup2", tmp_fs_type) == 0) { cgroupv2_mount_point_found = true; any_cgroup_mounts_found = true; + // For unified we only have a single line with cgroup2 fs type. + // Therefore use that option for all CG info structs. + bool ro_option = find_ro_opt(mount_opts); for (int i = 0; i < CG_INFO_LENGTH; i++) { - set_controller_paths(cg_infos, i, "(cg2, unified)", tmpmount, tmproot); + set_controller_paths(cg_infos, i, "(cg2, unified)", tmpmount, tmproot, ro_option); } } } /* Cgroup v1 relevant info * - * Find the cgroup mount point for memory, cpuset, cpu, cpuacct, pids + * Find the cgroup mount point for memory, cpuset, cpu, cpuacct, pids. For each controller + * determine whether or not they show up as mounted read only or not. * * Example for docker: * 219 214 0:29 /docker/7208cebd00fa5f2e342b1094f7bed87fa25661471a4637118e65f1c995be8a34 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory @@ -348,8 +423,9 @@ bool CgroupSubsystemFactory::determine_type(CgroupInfo* cg_infos, * 34 28 0:29 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,memory * * 44 31 0:39 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:23 - cgroup cgroup rw,pids + * */ - if (sscanf(p, "%*d %*d %*d:%*d %s %s %*[^-]- %s %*s %s", tmproot, tmpmount, tmp_fs_type, tmpcgroups) == 4) { + if (match_mount_info_line(p, tmproot, tmpmount, mount_opts, tmp_fs_type, tmpcgroups)) { if (strcmp("cgroup", tmp_fs_type) != 0) { // Skip cgroup2 fs lines on hybrid or unified hierarchy. continue; @@ -357,23 +433,28 @@ bool CgroupSubsystemFactory::determine_type(CgroupInfo* cg_infos, while ((token = strsep(&cptr, ",")) != nullptr) { if (strcmp(token, "memory") == 0) { any_cgroup_mounts_found = true; - set_controller_paths(cg_infos, MEMORY_IDX, token, tmpmount, tmproot); + bool ro_option = find_ro_opt(mount_opts); + set_controller_paths(cg_infos, MEMORY_IDX, token, tmpmount, tmproot, ro_option); cg_infos[MEMORY_IDX]._data_complete = true; } else if (strcmp(token, "cpuset") == 0) { any_cgroup_mounts_found = true; - set_controller_paths(cg_infos, CPUSET_IDX, token, tmpmount, tmproot); + bool ro_option = find_ro_opt(mount_opts); + set_controller_paths(cg_infos, CPUSET_IDX, token, tmpmount, tmproot, ro_option); cg_infos[CPUSET_IDX]._data_complete = true; } else if (strcmp(token, "cpu") == 0) { any_cgroup_mounts_found = true; - set_controller_paths(cg_infos, CPU_IDX, token, tmpmount, tmproot); + bool ro_option = find_ro_opt(mount_opts); + set_controller_paths(cg_infos, CPU_IDX, token, tmpmount, tmproot, ro_option); cg_infos[CPU_IDX]._data_complete = true; } else if (strcmp(token, "cpuacct") == 0) { any_cgroup_mounts_found = true; - set_controller_paths(cg_infos, CPUACCT_IDX, token, tmpmount, tmproot); + bool ro_option = find_ro_opt(mount_opts); + set_controller_paths(cg_infos, CPUACCT_IDX, token, tmpmount, tmproot, ro_option); cg_infos[CPUACCT_IDX]._data_complete = true; } else if (strcmp(token, "pids") == 0) { any_cgroup_mounts_found = true; - set_controller_paths(cg_infos, PIDS_IDX, token, tmpmount, tmproot); + bool ro_option = find_ro_opt(mount_opts); + set_controller_paths(cg_infos, PIDS_IDX, token, tmpmount, tmproot, ro_option); cg_infos[PIDS_IDX]._data_complete = true; } } diff --git a/src/hotspot/os/linux/cgroupSubsystem_linux.hpp b/src/hotspot/os/linux/cgroupSubsystem_linux.hpp index 5888072bdf3..64458223e84 100644 --- a/src/hotspot/os/linux/cgroupSubsystem_linux.hpp +++ b/src/hotspot/os/linux/cgroupSubsystem_linux.hpp @@ -105,6 +105,7 @@ class CgroupController: public CHeapObj { public: virtual char* subsystem_path() = 0; + virtual bool is_read_only() = 0; /* Read a numerical value as unsigned long * @@ -208,6 +209,7 @@ class CgroupSubsystem: public CHeapObj { virtual jlong memory_and_swap_limit_in_bytes() = 0; virtual jlong memory_soft_limit_in_bytes() = 0; virtual jlong memory_max_usage_in_bytes() = 0; + virtual bool is_containerized() = 0; virtual char * cpu_cpuset_cpus() = 0; virtual char * cpu_cpuset_memory_nodes() = 0; @@ -230,6 +232,7 @@ class CgroupInfo : public StackObj { char* _name; int _hierarchy_id; bool _enabled; + bool _read_only; // whether or not the mount path is mounted read-only bool _data_complete; // indicating cgroup v1 data is complete for this controller char* _cgroup_path; // cgroup controller path from /proc/self/cgroup char* _root_mount_path; // root mount path from /proc/self/mountinfo. Unused for cgroup v2 @@ -240,6 +243,7 @@ class CgroupInfo : public StackObj { _name = nullptr; _hierarchy_id = -1; _enabled = false; + _read_only = false; _data_complete = false; _cgroup_path = nullptr; _root_mount_path = nullptr; @@ -271,7 +275,8 @@ class CgroupSubsystemFactory: AllStatic { int controller, const char* name, char* mount_path, - char* root_path); + char* root_path, + bool read_only); // Determine the cgroup type (version 1 or version 2), given // relevant paths to files. Sets 'flags' accordingly. static bool determine_type(CgroupInfo* cg_infos, diff --git a/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp b/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp index 229e2e4237b..bacd66609a2 100644 --- a/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp +++ b/src/hotspot/os/linux/cgroupV1Subsystem_linux.cpp @@ -203,6 +203,17 @@ jlong CgroupV1Subsystem::memory_soft_limit_in_bytes() { } } +bool CgroupV1Subsystem::is_containerized() { + // containerized iff all required controllers are mounted + // read-only. See OSContainer::is_containerized() for + // the full logic. + // + return _memory->controller()->is_read_only() && + _cpu->controller()->is_read_only() && + _cpuacct->is_read_only() && + _cpuset->is_read_only(); +} + /* memory_usage_in_bytes * * Return the amount of used memory for this process. diff --git a/src/hotspot/os/linux/cgroupV1Subsystem_linux.hpp b/src/hotspot/os/linux/cgroupV1Subsystem_linux.hpp index c7535844bf5..9a612a57606 100644 --- a/src/hotspot/os/linux/cgroupV1Subsystem_linux.hpp +++ b/src/hotspot/os/linux/cgroupV1Subsystem_linux.hpp @@ -36,19 +36,22 @@ class CgroupV1Controller: public CgroupController { /* mountinfo contents */ char *_root; char *_mount_point; + bool _read_only; /* Constructed subsystem directory */ char *_path; public: - CgroupV1Controller(char *root, char *mountpoint) { + CgroupV1Controller(char *root, char *mountpoint, bool ro) { _root = os::strdup(root); _mount_point = os::strdup(mountpoint); _path = nullptr; + _read_only = ro; } virtual void set_subsystem_path(char *cgroup_path); char *subsystem_path() { return _path; } + bool is_read_only() { return _read_only; } }; class CgroupV1MemoryController: public CgroupV1Controller { @@ -65,7 +68,7 @@ class CgroupV1MemoryController: public CgroupV1Controller { void set_hierarchical(bool value) { _uses_mem_hierarchy = value; } public: - CgroupV1MemoryController(char *root, char *mountpoint) : CgroupV1Controller(root, mountpoint) { + CgroupV1MemoryController(char *root, char *mountpoint, bool ro) : CgroupV1Controller(root, mountpoint, ro) { _uses_mem_hierarchy = false; } @@ -94,6 +97,7 @@ class CgroupV1Subsystem: public CgroupSubsystem { jlong pids_max(); jlong pids_current(); + bool is_containerized(); void print_version_specific_info(outputStream* st); diff --git a/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp b/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp index d189dd6626e..ad185f572cf 100644 --- a/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp +++ b/src/hotspot/os/linux/cgroupV2Subsystem_linux.cpp @@ -94,6 +94,10 @@ int CgroupV2Subsystem::cpu_quota() { return limit; } +bool CgroupV2Subsystem::is_containerized() { + return _unified->is_read_only(); +} + char* CgroupV2Subsystem::cpu_cpuset_cpus() { char cpus[1024]; CONTAINER_READ_STRING_CHECKED(_unified, "/cpuset.cpus", "cpuset.cpus", cpus, 1024); diff --git a/src/hotspot/os/linux/cgroupV2Subsystem_linux.hpp b/src/hotspot/os/linux/cgroupV2Subsystem_linux.hpp index 18d4a50ff0b..30527f16c8c 100644 --- a/src/hotspot/os/linux/cgroupV2Subsystem_linux.hpp +++ b/src/hotspot/os/linux/cgroupV2Subsystem_linux.hpp @@ -33,19 +33,22 @@ class CgroupV2Controller: public CgroupController { char *_mount_path; /* The cgroup path for the controller */ char *_cgroup_path; + bool _read_only; /* Constructed full path to the subsystem directory */ char *_path; static char* construct_path(char* mount_path, char *cgroup_path); public: - CgroupV2Controller(char * mount_path, char *cgroup_path) { + CgroupV2Controller(char * mount_path, char *cgroup_path, bool ro) { _mount_path = mount_path; _cgroup_path = os::strdup(cgroup_path); _path = construct_path(mount_path, cgroup_path); + _read_only = ro; } char *subsystem_path() { return _path; } + bool is_read_only() { return _read_only; } }; class CgroupV2Subsystem: public CgroupSubsystem { @@ -80,6 +83,7 @@ class CgroupV2Subsystem: public CgroupSubsystem { jlong pids_max(); jlong pids_current(); + bool is_containerized(); void print_version_specific_info(outputStream* st); const char * container_type() { diff --git a/src/hotspot/os/linux/osContainer_linux.cpp b/src/hotspot/os/linux/osContainer_linux.cpp index 3f8c352dee3..87bc484f36d 100644 --- a/src/hotspot/os/linux/osContainer_linux.cpp +++ b/src/hotspot/os/linux/osContainer_linux.cpp @@ -58,8 +58,43 @@ void OSContainer::init() { if (cgroup_subsystem == nullptr) { return; // Required subsystem files not found or other error } - - _is_containerized = true; + /* + * In order to avoid a false positive on is_containerized() on + * Linux systems outside a container *and* to ensure compatibility + * with in-container usage, we detemine is_containerized() by two + * steps: + * 1.) Determine if all the cgroup controllers are mounted read only. + * If yes, is_containerized() == true. Otherwise, do the fallback + * in 2.) + * 2.) Query for memory and cpu limits. If any limit is set, we set + * is_containerized() == true. + * + * Step 1.) covers the basic in container use-cases. Step 2.) ensures + * that limits enforced by other means (e.g. systemd slice) are properly + * detected. + */ + const char *reason; + bool any_mem_cpu_limit_present = false; + bool controllers_read_only = cgroup_subsystem->is_containerized(); + if (controllers_read_only) { + // in-container case + reason = " because all controllers are mounted read-only (container case)"; + } else { + // We can be in one of two cases: + // 1.) On a physical Linux system without any limit + // 2.) On a physical Linux system with a limit enforced by other means (like systemd slice) + any_mem_cpu_limit_present = cgroup_subsystem->memory_limit_in_bytes() > 0 || + os::Linux::active_processor_count() != cgroup_subsystem->active_processor_count(); + if (any_mem_cpu_limit_present) { + reason = " because either a cpu or a memory limit is present"; + } else { + reason = " because no cpu or memory limit is present"; + } + } + _is_containerized = controllers_read_only || any_mem_cpu_limit_present; + log_debug(os, container)("OSContainer::init: is_containerized() = %s%s", + _is_containerized ? "true" : "false", + reason); } const char * OSContainer::container_type() { diff --git a/src/hotspot/share/include/jvm.h b/src/hotspot/share/include/jvm.h index f1e2c35891a..73e29bcf200 100644 --- a/src/hotspot/share/include/jvm.h +++ b/src/hotspot/share/include/jvm.h @@ -150,6 +150,9 @@ JVM_ActiveProcessorCount(void); JNIEXPORT jboolean JNICALL JVM_IsUseContainerSupport(void); +JNIEXPORT jboolean JNICALL +JVM_IsContainerized(void); + JNIEXPORT void * JNICALL JVM_LoadZipLibrary(); diff --git a/src/hotspot/share/prims/jvm.cpp b/src/hotspot/share/prims/jvm.cpp index 7852f37d4be..64bed35d08f 100644 --- a/src/hotspot/share/prims/jvm.cpp +++ b/src/hotspot/share/prims/jvm.cpp @@ -110,6 +110,9 @@ #if INCLUDE_MANAGEMENT #include "services/finalizerService.hpp" #endif +#ifdef LINUX +#include "osContainer_linux.hpp" +#endif #include @@ -493,6 +496,15 @@ JVM_LEAF(jboolean, JVM_IsUseContainerSupport(void)) return JNI_FALSE; JVM_END +JVM_LEAF(jboolean, JVM_IsContainerized(void)) +#ifdef LINUX + if (OSContainer::is_containerized()) { + return JNI_TRUE; + } +#endif + return JNI_FALSE; +JVM_END + // java.lang.Throwable ////////////////////////////////////////////////////// JVM_ENTRY(void, JVM_FillInStackTrace(JNIEnv *env, jobject receiver)) diff --git a/src/java.base/linux/classes/jdk/internal/platform/CgroupMetrics.java b/src/java.base/linux/classes/jdk/internal/platform/CgroupMetrics.java index 8797711bf4b..af551a07b1e 100644 --- a/src/java.base/linux/classes/jdk/internal/platform/CgroupMetrics.java +++ b/src/java.base/linux/classes/jdk/internal/platform/CgroupMetrics.java @@ -35,6 +35,11 @@ public class CgroupMetrics implements Metrics { this.subsystem = Objects.requireNonNull(subsystem); } + @Override + public boolean isContainerized() { + return isContainerized0(); + } + @Override public String getProvider() { return subsystem.getProvider(); @@ -194,6 +199,7 @@ public static Metrics getInstance() { } private static native boolean isUseContainerSupport(); + private static native boolean isContainerized0(); private static native long getTotalMemorySize0(); private static native long getTotalSwapSize0(); diff --git a/src/java.base/linux/classes/jdk/internal/platform/CgroupSubsystem.java b/src/java.base/linux/classes/jdk/internal/platform/CgroupSubsystem.java index 952de13e9f2..7df86d03ff4 100644 --- a/src/java.base/linux/classes/jdk/internal/platform/CgroupSubsystem.java +++ b/src/java.base/linux/classes/jdk/internal/platform/CgroupSubsystem.java @@ -31,6 +31,11 @@ */ public interface CgroupSubsystem extends Metrics { + + default boolean isContainerized() { + return false; // This default impl is never used + } + /** * Returned for metrics of type long if the underlying implementation * has determined that no limit is being imposed. diff --git a/src/java.base/linux/native/libjava/CgroupMetrics.c b/src/java.base/linux/native/libjava/CgroupMetrics.c index a5e41167bc3..e2f98633459 100644 --- a/src/java.base/linux/native/libjava/CgroupMetrics.c +++ b/src/java.base/linux/native/libjava/CgroupMetrics.c @@ -36,6 +36,12 @@ Java_jdk_internal_platform_CgroupMetrics_isUseContainerSupport(JNIEnv *env, jcla return JVM_IsUseContainerSupport(); } +JNIEXPORT jboolean JNICALL +Java_jdk_internal_platform_CgroupMetrics_isContainerized0(JNIEnv *env, jclass ignored) +{ + return JVM_IsContainerized(); +} + JNIEXPORT jlong JNICALL Java_jdk_internal_platform_CgroupMetrics_getTotalMemorySize0 (JNIEnv *env, jclass ignored) diff --git a/src/java.base/share/classes/jdk/internal/platform/Metrics.java b/src/java.base/share/classes/jdk/internal/platform/Metrics.java index c45e3e52257..b14dbd7738e 100644 --- a/src/java.base/share/classes/jdk/internal/platform/Metrics.java +++ b/src/java.base/share/classes/jdk/internal/platform/Metrics.java @@ -71,6 +71,23 @@ public static Metrics systemMetrics() { */ public String getProvider(); + /** + * Determines whether or not the underlying system + * has useful metrics (a.k.a. is containerized). + * + * @implNote + * Note that Metrics on some systems aren't useful. + * For example on a regular Linux system with cgroups + * present, with no limits enforced and not running in + * a container, Metric aren't useful. This can be used + * in order to determine if the system is containerized. + * + * @return true when the JVM runs in containerized mode. + * false otherwise. + * + */ + public boolean isContainerized(); + /***************************************************************** * CPU Accounting Subsystem diff --git a/src/java.base/share/classes/sun/launcher/LauncherHelper.java b/src/java.base/share/classes/sun/launcher/LauncherHelper.java index 58a6be5fa1e..ff81acbbf7f 100644 --- a/src/java.base/share/classes/sun/launcher/LauncherHelper.java +++ b/src/java.base/share/classes/sun/launcher/LauncherHelper.java @@ -347,6 +347,10 @@ private static void printSystemMetrics() { final long longRetvalNotSupported = -2; ostream.println(INDENT + "Provider: " + c.getProvider()); + if (!c.isContainerized()) { + ostream.println(INDENT + "System not containerized."); + return; + } ostream.println(INDENT + "Effective CPU Count: " + c.getEffectiveCpuCount()); ostream.println(formatCpuVal(c.getCpuPeriod(), INDENT + "CPU Period: ", longRetvalNotSupported)); ostream.println(formatCpuVal(c.getCpuQuota(), INDENT + "CPU Quota: ", longRetvalNotSupported)); diff --git a/test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp b/test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp index aa1d2a19b28..0f054a3bd72 100644 --- a/test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp +++ b/test/hotspot/gtest/runtime/test_cgroupSubsystem_linux.cpp @@ -78,6 +78,9 @@ class TestController : public CgroupController { char* subsystem_path() override { return _path; }; + bool is_read_only() override { + return true; // doesn't matter + } }; static void fill_file(const char* path, const char* content) { @@ -436,7 +439,8 @@ TEST(cgroupTest, set_cgroupv1_subsystem_path) { &container_engine }; for (int i = 0; i < length; i++) { CgroupV1Controller* ctrl = new CgroupV1Controller( (char*)testCases[i]->root_path, - (char*)testCases[i]->mount_path); + (char*)testCases[i]->mount_path, + true /* read-only mount */); ctrl->set_subsystem_path((char*)testCases[i]->cgroup_path); ASSERT_STREQ(testCases[i]->expected_path, ctrl->subsystem_path()); } @@ -460,7 +464,8 @@ TEST(cgroupTest, set_cgroupv2_subsystem_path) { &sub_path }; for (int i = 0; i < length; i++) { CgroupV2Controller* ctrl = new CgroupV2Controller( (char*)testCases[i]->mount_path, - (char*)testCases[i]->cgroup_path); + (char*)testCases[i]->cgroup_path, + true /* read-only mount */); ASSERT_STREQ(testCases[i]->expected_path, ctrl->subsystem_path()); } } diff --git a/test/hotspot/jtreg/containers/cgroup/PlainRead.java b/test/hotspot/jtreg/containers/cgroup/TestContainerized.java similarity index 54% rename from test/hotspot/jtreg/containers/cgroup/PlainRead.java rename to test/hotspot/jtreg/containers/cgroup/TestContainerized.java index 21eccd79835..52cf5451a8d 100644 --- a/test/hotspot/jtreg/containers/cgroup/PlainRead.java +++ b/test/hotspot/jtreg/containers/cgroup/TestContainerized.java @@ -22,57 +22,27 @@ */ /* - * @test PlainRead + * @test + * @bug 8261242 * @key cgroups * @requires os.family == "linux" * @requires vm.flagless * @library /testlibrary /test/lib * @build jdk.test.whitebox.WhiteBox * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox - * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI PlainRead + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI TestContainerized */ import jdk.test.lib.process.ProcessTools; -import jdk.test.lib.process.OutputAnalyzer; -import jdk.test.lib.Platform; import jdk.test.whitebox.WhiteBox; -public class PlainRead { - - static public void match(OutputAnalyzer oa, String what, String value) { - oa.shouldMatch("^.*" + what + " *" + value + ".*$"); - } - - static public void noMatch(OutputAnalyzer oa, String what, String value) { - oa.shouldNotMatch("^.*" + what + " *" + value + ".*$"); - } - - static final String good_value = "(\\d+|-1|-2|Unlimited)"; - static final String bad_value = "(failed)"; - - static final String[] variables = {"Memory Limit is:", "CPU Quota is:", "CPU Period is:", "active_processor_count:"}; - - static public void isContainer(OutputAnalyzer oa) { - for (String v: variables) { - match(oa, v, good_value); - } - for (String v: variables) { - noMatch(oa, v, bad_value); - } - } - - static public void isNotContainer(OutputAnalyzer oa) { - oa.shouldMatch("^.*Can't open /proc/self/mountinfo.*$"); - } +public class TestContainerized { public static void main(String[] args) throws Exception { WhiteBox wb = WhiteBox.getWhiteBox(); - ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder("-Xlog:os+container=trace", "-version"); - OutputAnalyzer output = new OutputAnalyzer(pb.start()); - if (wb.isContainerized()) { - System.out.println("Inside a cgroup, testing..."); - isContainer(output); + throw new RuntimeException("Test failed! Expected not containerized on plain Linux."); } + System.out.println("Plain linux, no limits. Passed!"); } } diff --git a/test/jdk/jdk/internal/platform/cgroup/TestSystemSettings.java b/test/jdk/jdk/internal/platform/cgroup/TestSystemSettings.java new file mode 100644 index 00000000000..8d9279e1603 --- /dev/null +++ b/test/jdk/jdk/internal/platform/cgroup/TestSystemSettings.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @key cgroups + * @requires os.family == "linux" + * @requires vm.flagless + * @library /test/lib + * @build TestSystemSettings + * @run main/othervm TestSystemSettings + */ + +import jdk.test.lib.process.ProcessTools; +import jdk.test.lib.process.OutputAnalyzer; + +public class TestSystemSettings { + + public static void main(String[] args) throws Exception { + ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder("-XshowSettings:system", "-version"); + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + + output.shouldContain("System not containerized."); + } +} diff --git a/test/lib/jdk/test/lib/containers/cgroup/MetricsTester.java b/test/lib/jdk/test/lib/containers/cgroup/MetricsTester.java index a6eff3d237a..0b29d288cd9 100644 --- a/test/lib/jdk/test/lib/containers/cgroup/MetricsTester.java +++ b/test/lib/jdk/test/lib/containers/cgroup/MetricsTester.java @@ -69,6 +69,16 @@ private void testAll(Metrics m, boolean inContainer) throws Exception { tester.testMemoryUsage(); } tester.testMisc(); + testContainerized(m, inContainer); + } + + private void testContainerized(Metrics m, boolean inContainer) { + if (m.isContainerized() != inContainer) { + throw new RuntimeException("containerized test failed. " + + "Expected isContainerized()==" + inContainer + + " but got '" + m.isContainerized() + "'"); + } + System.out.println("testContainerized() PASSED!"); } public static void main(String[] args) throws Exception {