diff --git a/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs b/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs index 287fc21ebedf0b..abb7baa63a673d 100644 --- a/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs +++ b/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs @@ -16,17 +16,23 @@ internal static partial class cgroups { // For cgroup v1, see https://www.kernel.org/doc/Documentation/cgroup-v1/ // For cgroup v2, see https://www.kernel.org/doc/Documentation/cgroup-v2.txt + // For disambiguation, see https://systemd.io/CGROUP_DELEGATION/#three-different-tree-setups- - /// The version of cgroup that's being used + /// The supported versions of cgroup. internal enum CGroupVersion { None, CGroup1, CGroup2 }; + /// Path to cgroup filesystem that tells us which version of cgroup is in use. + private const string SysFsCgroupFileSystemPath = "/sys/fs/cgroup"; /// Path to mountinfo file in procfs for the current process. private const string ProcMountInfoFilePath = "/proc/self/mountinfo"; /// Path to cgroup directory in procfs for the current process. private const string ProcCGroupFilePath = "/proc/self/cgroup"; + /// The version of cgroup that's being used. Mutated by tests only. + internal static readonly CGroupVersion s_cgroupVersion = FindCGroupVersion(); + /// Path to the found cgroup memory limit path, or null if it couldn't be found. - internal static readonly string? s_cgroupMemoryLimitPath = FindCGroupMemoryLimitPath(); + internal static readonly string? s_cgroupMemoryLimitPath = FindCGroupMemoryLimitPath(s_cgroupVersion); /// Tries to read the memory limit from the cgroup memory location. /// The read limit, or 0 if it couldn't be read. @@ -102,19 +108,39 @@ internal static bool TryReadMemoryValueFromFile(string path, out ulong result) return false; } + /// Find the cgroup version in use on the system. + /// The cgroup version. + private static CGroupVersion FindCGroupVersion() + { + try + { + return new DriveInfo(SysFsCgroupFileSystemPath).DriveFormat switch + { + "cgroup2fs" => CGroupVersion.CGroup2, + "tmpfs" => CGroupVersion.CGroup1, + _ => CGroupVersion.None, + }; + } + catch (Exception ex) when (ex is DriveNotFoundException || ex is ArgumentException) + { + return CGroupVersion.None; + } + } + /// Find the cgroup memory limit path. + /// The cgroup version currently in use on the system. /// The limit path if found; otherwise, null. - private static string? FindCGroupMemoryLimitPath() + private static string? FindCGroupMemoryLimitPath(CGroupVersion cgroupVersion) { - string? cgroupMemoryPath = FindCGroupPath("memory", out CGroupVersion version); + string? cgroupMemoryPath = FindCGroupPath(cgroupVersion, "memory"); if (cgroupMemoryPath != null) { - if (version == CGroupVersion.CGroup1) + if (cgroupVersion == CGroupVersion.CGroup1) { return cgroupMemoryPath + "/memory.limit_in_bytes"; } - if (version == CGroupVersion.CGroup2) + if (cgroupVersion == CGroupVersion.CGroup2) { // 'memory.high' is a soft limit; the process may get throttled // 'memory.max' is where OOM killer kicks in @@ -126,34 +152,71 @@ internal static bool TryReadMemoryValueFromFile(string path, out ulong result) } /// Find the cgroup path for the specified subsystem. + /// The cgroup version currently in use on the system. /// The subsystem, e.g. "memory". /// The cgroup path if found; otherwise, null. - private static string? FindCGroupPath(string subsystem, out CGroupVersion version) + private static string? FindCGroupPath(CGroupVersion cgroupVersion, string subsystem) { - if (TryFindHierarchyMount(subsystem, out version, out string? hierarchyRoot, out string? hierarchyMount) && - TryFindCGroupPathForSubsystem(subsystem, out string? cgroupPathRelativeToMount)) + if (cgroupVersion == CGroupVersion.None) + { + return null; + } + + if (TryFindHierarchyMount(cgroupVersion, subsystem, out string? hierarchyRoot, out string? hierarchyMount) && + TryFindCGroupPathForSubsystem(cgroupVersion, subsystem, out string? cgroupPathRelativeToMount)) { - // For a host cgroup, we need to append the relative path. - // In a docker container, the root and relative path are the same and we don't need to append. - return (hierarchyRoot != cgroupPathRelativeToMount) ? - hierarchyMount + cgroupPathRelativeToMount : - hierarchyMount; + return FindCGroupPath(hierarchyRoot, hierarchyMount, cgroupPathRelativeToMount); } return null; } + internal static string FindCGroupPath(string hierarchyRoot, string hierarchyMount, string cgroupPathRelativeToMount) + { + // For a host cgroup, we need to append the relative path. + // The root and cgroup path can share a common prefix of the path that should not be appended. + // Example 1 (docker): + // hierarchyMount: /sys/fs/cgroup/cpu + // hierarchyRoot: /docker/87ee2de57e51bc75175a4d2e81b71d162811b179d549d6601ed70b58cad83578 + // cgroupPathRelativeToMount: /docker/87ee2de57e51bc75175a4d2e81b71d162811b179d549d6601ed70b58cad83578/my_named_cgroup + // append to the cgroupPath: /my_named_cgroup + // final cgroupPath: /sys/fs/cgroup/cpu/my_named_cgroup + // + // Example 2 (out of docker) + // hierarchyMount: /sys/fs/cgroup/cpu + // hierarchyRoot: / + // cgroupPathRelativeToMount: /my_named_cgroup + // append to the cgroupPath: /my_named_cgroup + // final cgroupPath: /sys/fs/cgroup/cpu/my_named_cgroup + + int commonPathPrefixLength = hierarchyRoot.Length; + if ((commonPathPrefixLength == 1) || !cgroupPathRelativeToMount.StartsWith(hierarchyRoot, StringComparison.Ordinal)) + { + commonPathPrefixLength = 0; + } + + return string.Concat(hierarchyMount, cgroupPathRelativeToMount.AsSpan(commonPathPrefixLength)); + } + /// Find the cgroup mount information for the specified subsystem. + /// The cgroup version currently in use on the system. /// The subsystem, e.g. "memory". /// The path of the directory in the filesystem which forms the root of this mount; null if not found. /// The path of the mount point relative to the process's root directory; null if not found. /// true if the mount was found; otherwise, null. - private static bool TryFindHierarchyMount(string subsystem, out CGroupVersion version, [NotNullWhen(true)] out string? root, [NotNullWhen(true)] out string? path) + private static bool TryFindHierarchyMount(CGroupVersion cgroupVersion, string subsystem, [NotNullWhen(true)] out string? root, [NotNullWhen(true)] out string? path) { - return TryFindHierarchyMount(ProcMountInfoFilePath, subsystem, out version, out root, out path); + return TryFindHierarchyMount(cgroupVersion, ProcMountInfoFilePath, subsystem, out root, out path); } - internal static bool TryFindHierarchyMount(string mountInfoFilePath, string subsystem, out CGroupVersion version, [NotNullWhen(true)] out string? root, [NotNullWhen(true)] out string? path) + /// Find the cgroup mount information for the specified subsystem. + /// The cgroup version currently in use on the system. + /// The path to the /mountinfo file. Useful for tests. + /// The subsystem, e.g. "memory". + /// The path of the directory in the filesystem which forms the root of this mount; null if not found. + /// The path of the mount point relative to the process's root directory; null if not found. + /// true if the mount was found; otherwise, null. + internal static bool TryFindHierarchyMount(CGroupVersion cgroupVersion, string mountInfoFilePath, string subsystem, [NotNullWhen(true)] out string? root, [NotNullWhen(true)] out string? path) { if (File.Exists(mountInfoFilePath)) { @@ -188,31 +251,30 @@ internal static bool TryFindHierarchyMount(string mountInfoFilePath, string subs continue; } - bool validCGroup1Entry = ((postSeparatorlineParts[0] == "cgroup") && - (Array.IndexOf(postSeparatorlineParts[2].Split(','), subsystem) >= 0)); - bool validCGroup2Entry = postSeparatorlineParts[0] == "cgroup2"; - - if (!validCGroup1Entry && !validCGroup2Entry) + if (cgroupVersion == CGroupVersion.CGroup1) { - // Not the relevant entry. - continue; + bool validCGroup1Entry = ((postSeparatorlineParts[0] == "cgroup") && + (Array.IndexOf(postSeparatorlineParts[2].Split(','), subsystem) >= 0)); + if (!validCGroup1Entry) + { + continue; + } } + else if (cgroupVersion == CGroupVersion.CGroup2) + { + bool validCGroup2Entry = postSeparatorlineParts[0] == "cgroup2"; + if (!validCGroup2Entry) + { + continue; + } - // Found the relevant entry. Extract the cgroup version, mount root and path. - switch (postSeparatorlineParts[0]) + } + else { - case "cgroup": - version = CGroupVersion.CGroup1; - break; - case "cgroup2": - version = CGroupVersion.CGroup2; - break; - default: - version = CGroupVersion.None; - Debug.Fail($"invalid value for CGroupVersion \"{postSeparatorlineParts[0]}\""); - break; + Debug.Fail($"Unexpected cgroup version \"{cgroupVersion}\""); } + string[] lineParts = line.Substring(0, endOfOptionalFields).Split(' '); root = lineParts[3]; path = lineParts[4]; @@ -227,22 +289,27 @@ internal static bool TryFindHierarchyMount(string mountInfoFilePath, string subs } } - version = CGroupVersion.None; root = null; path = null; return false; } /// Find the cgroup relative path for the specified subsystem. + /// The cgroup version currently in use on the system. /// The subsystem, e.g. "memory". /// The found path, or null if it couldn't be found. - /// - private static bool TryFindCGroupPathForSubsystem(string subsystem, [NotNullWhen(true)] out string? path) + /// true if a cgroup path for the subsystem is found. + private static bool TryFindCGroupPathForSubsystem(CGroupVersion cgroupVersion, string subsystem, [NotNullWhen(true)] out string? path) { - return TryFindCGroupPathForSubsystem(ProcCGroupFilePath, subsystem, out path); + return TryFindCGroupPathForSubsystem(cgroupVersion, ProcCGroupFilePath, subsystem, out path); } - internal static bool TryFindCGroupPathForSubsystem(string procCGroupFilePath, string subsystem, [NotNullWhen(true)] out string? path) + /// Find the cgroup relative path for the specified subsystem. + /// The cgroup version currently in use on the system. + /// The subsystem, e.g. "memory". + /// The found path, or null if it couldn't be found. + /// true if a cgroup path for the subsystem is found. + internal static bool TryFindCGroupPathForSubsystem(CGroupVersion cgroupVersion, string procCGroupFilePath, string subsystem, [NotNullWhen(true)] out string? path) { if (File.Exists(procCGroupFilePath)) { @@ -261,28 +328,36 @@ internal static bool TryFindCGroupPathForSubsystem(string procCGroupFilePath, st continue; } - // cgroup v2: Find the first entry that matches the cgroup v2 hierarchy: - // 0::$PATH - - if ((lineParts[0] == "0") && (string.Empty == lineParts[1])) + if (cgroupVersion == CGroupVersion.CGroup1) { + // cgroup v1: Find the first entry that has the subsystem listed in its controller + // list. See man page for cgroups for /proc/[pid]/cgroups format, e.g: + // hierarchy-ID:controller-list:cgroup-path + // 5:cpuacct,cpu,cpuset:/daemons + if (Array.IndexOf(lineParts[1].Split(','), subsystem) < 0) + { + // Not the relevant entry. + continue; + } + path = lineParts[2]; return true; } - - // cgroup v1: Find the first entry that has the subsystem listed in its controller - // list. See man page for cgroups for /proc/[pid]/cgroups format, e.g: - // hierarchy-ID:controller-list:cgroup-path - // 5:cpuacct,cpu,cpuset:/daemons - - if (Array.IndexOf(lineParts[1].Split(','), subsystem) < 0) + else if (cgroupVersion == CGroupVersion.CGroup2) { - // Not the relevant entry. - continue; + // cgroup v2: Find the first entry that matches the cgroup v2 hierarchy: + // 0::$PATH + + if ((lineParts[0] == "0") && (lineParts[1] == string.Empty)) + { + path = lineParts[2]; + return true; + } + } + else + { + Debug.Fail($"Unexpected cgroup version: \"{cgroupVersion}\""); } - - path = lineParts[2]; - return true; } } } diff --git a/src/libraries/Common/tests/Tests/Interop/cgroupsTests.cs b/src/libraries/Common/tests/Tests/Interop/cgroupsTests.cs index fc6ab5c9753ce4..a40713ed36899a 100644 --- a/src/libraries/Common/tests/Tests/Interop/cgroupsTests.cs +++ b/src/libraries/Common/tests/Tests/Interop/cgroupsTests.cs @@ -9,6 +9,12 @@ namespace Common.Tests { public class cgroupsTests : FileCleanupTestBase { + [Fact] + public void ValidateFindCGroupVersion() + { + Assert.InRange((int)Interop.cgroups.s_cgroupVersion, 0, 2); + } + [Theory] [InlineData(true, "0", 0)] [InlineData(false, "max", 0)] @@ -27,48 +33,57 @@ public void ValidateTryReadMemoryValue(bool expectedResult, string valueText, ul } [Theory] - [InlineData(false, "0 0 0:0 / /foo ignore ignore - overlay overlay ignore", "ignore", 0, "/", "/")] - [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup2 cgroup2 ignore", "ignore", 2, "/", "/foo")] - [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup2 cgroup2 ignore", "memory", 2, "/", "/foo")] - [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup2 cgroup2 ignore", "cpu", 2, "/", "/foo")] - [InlineData(true, "0 0 0:0 / /foo ignore - cgroup2 cgroup2 ignore", "cpu", 2, "/", "/foo")] - [InlineData(true, "0 0 0:0 / /foo ignore ignore ignore - cgroup2 cgroup2 ignore", "cpu", 2, "/", "/foo")] - [InlineData(true, "0 0 0:0 / /foo-with-dashes ignore ignore - cgroup2 cgroup2 ignore", "ignore", 2, "/", "/foo-with-dashes")] - [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup cgroup memory", "memory", 1, "/", "/foo")] - [InlineData(true, "0 0 0:0 / /foo-with-dashes ignore ignore - cgroup cgroup memory", "memory", 1, "/", "/foo-with-dashes")] - [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup cgroup cpu,memory", "memory", 1, "/", "/foo")] - [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup cgroup memory,cpu", "memory", 1, "/", "/foo")] - [InlineData(false, "0 0 0:0 / /foo ignore ignore - cgroup cgroup cpu", "memory", 0, "/", "/foo")] - public void ParseValidateMountInfo(bool expectedFound, string procSelfMountInfoText, string subsystem, int expectedVersion, string expectedRoot, string expectedMount) + [InlineData("/sys/fs/cgroup/cpu/my_cgroup", "/docker/1234", "/sys/fs/cgroup/cpu", "/docker/1234/my_cgroup")] + [InlineData("/sys/fs/cgroup/cpu/my_cgroup", "/", "/sys/fs/cgroup/cpu", "/my_cgroup")] + public void ValidateFindCGroupPath(string expectedResult, string hierarchyRoot, string hierarchyMount, string cgroupPathRelativeToMount) + { + Assert.Equal(expectedResult, Interop.cgroups.FindCGroupPath(hierarchyRoot, hierarchyMount, cgroupPathRelativeToMount)); + } + + [Theory] + [InlineData(true, 2, "0 0 0:0 / /foo ignore ignore - cgroup2 cgroup2 ignore", "ignore", "/", "/foo")] + [InlineData(true, 2, "0 0 0:0 / /foo ignore ignore - cgroup2 cgroup2 ignore", "memory", "/", "/foo")] + [InlineData(true, 2, "0 0 0:0 / /foo ignore ignore - cgroup2 cgroup2 ignore", "cpu", "/", "/foo")] + [InlineData(true, 2, "0 0 0:0 / /foo ignore - cgroup2 cgroup2 ignore", "cpu", "/", "/foo")] + [InlineData(true, 2, "0 0 0:0 / /foo ignore ignore ignore - cgroup2 cgroup2 ignore", "cpu", "/", "/foo")] + [InlineData(true, 2, "0 0 0:0 / /foo-with-dashes ignore ignore - cgroup2 cgroup2 ignore", "ignore", "/", "/foo-with-dashes")] + [InlineData(true, 1, "0 0 0:0 / /foo ignore ignore - cgroup cgroup memory", "memory", "/", "/foo")] + [InlineData(true, 1, "0 0 0:0 / /foo-with-dashes ignore ignore - cgroup cgroup memory", "memory", "/", "/foo-with-dashes")] + [InlineData(true, 1, "0 0 0:0 / /foo ignore ignore - cgroup cgroup cpu,memory", "memory", "/", "/foo")] + [InlineData(true, 1, "0 0 0:0 / /foo ignore ignore - cgroup cgroup memory,cpu", "memory", "/", "/foo")] + public void ParseValidateMountInfo(bool expectedFound, int cgroupVersion, string procSelfMountInfoText, string subsystem, string expectedRoot, string expectedMount) { string path = GetTestFilePath(); File.WriteAllText(path, procSelfMountInfoText); - Assert.Equal(expectedFound, Interop.cgroups.TryFindHierarchyMount(path, subsystem, out Interop.cgroups.CGroupVersion version, out string root, out string mount)); + Assert.Equal(expectedFound, Interop.cgroups.TryFindHierarchyMount((Interop.cgroups.CGroupVersion) cgroupVersion, + path, subsystem, out string root, out string mount)); if (expectedFound) { - Assert.Equal(expectedVersion, (int)version); Assert.Equal(expectedRoot, root); Assert.Equal(expectedMount, mount); } } [Theory] - [InlineData(true, "0::/foo", "ignore", "/foo")] - [InlineData(true, "0::/bar", "ignore", "/bar")] - [InlineData(true, "0::frob", "ignore", "frob")] - [InlineData(false, "1::frob", "ignore", "ignore")] - [InlineData(true, "1:foo:bar", "foo", "bar")] - [InlineData(true, "2:foo:bar", "foo", "bar")] - [InlineData(false, "2:foo:bar", "bar", "ignore")] - [InlineData(true, "1:foo:bar\n2:eggs:spam", "foo", "bar")] - [InlineData(true, "1:foo:bar\n2:eggs:spam", "eggs", "spam")] - public void ParseValidateProcCGroup(bool expectedFound, string procSelfCgroupText, string subsystem, string expectedMountPath) + [InlineData(true, 2, "0::/foo", "ignore", "/foo")] + [InlineData(true, 2, "0::/bar", "ignore", "/bar")] + [InlineData(true, 2, "0::frob", "ignore", "frob")] + [InlineData(false, 1, "1::frob", "ignore", "ignore")] + [InlineData(true, 1, "1:foo:bar", "foo", "bar")] + [InlineData(true, 1, "0::baz\n1:foo:bar", "foo", "bar")] + [InlineData(true, 1, "2:foo:bar", "foo", "bar")] + [InlineData(false, 1, "2:foo:bar", "bar", "ignore")] + [InlineData(true, 1, "1:foo:bar\n2:eggs:spam", "foo", "bar")] + [InlineData(true, 1, "1:foo:bar\n2:eggs:spam", "eggs", "spam")] + [InlineData(true, 1, "2:eggs:spam\n0:foo:bar", "eggs", "spam")] + public void ParseValidateProcCGroup(bool expectedFound, int cgroupVersion, string procSelfCgroupText, string subsystem, string expectedMountPath) { string path = GetTestFilePath(); File.WriteAllText(path, procSelfCgroupText); - Assert.Equal(expectedFound, Interop.cgroups.TryFindCGroupPathForSubsystem(path, subsystem, out string mountPath)); + Assert.Equal(expectedFound, Interop.cgroups.TryFindCGroupPathForSubsystem((Interop.cgroups.CGroupVersion) cgroupVersion, + path, subsystem, out string mountPath)); if (expectedFound) { Assert.Equal(expectedMountPath, mountPath); diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index f6e156df516380..99e96c4d872d68 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -458,6 +458,7 @@ + diff --git a/src/libraries/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs b/src/libraries/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs index d65f92ec770f91..7ac23e69e8f145 100644 --- a/src/libraries/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs +++ b/src/libraries/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs @@ -41,6 +41,7 @@ public void DumpRuntimeInformationToConsole() Console.WriteLine($"### CURRENT DIRECTORY: {Environment.CurrentDirectory}"); + Console.WriteLine($"### CGROUPS VERSION: {Interop.cgroups.s_cgroupVersion}"); string cgroupsLocation = Interop.cgroups.s_cgroupMemoryLimitPath; if (cgroupsLocation != null) {