Blame SOURCES/corefx-39686-cgroupv2-01.patch

debe55
From 2b2273ea4ea1c28472fa0d6ad2ffeb6374500550 Mon Sep 17 00:00:00 2001
debe55
From: Omair Majid <omajid@redhat.com>
debe55
Date: Wed, 23 Oct 2019 17:45:59 -0400
debe55
Subject: [PATCH 1/2] Add cgroup v2 support to Interop.cgroups
debe55
debe55
Fix up code to adjust cgroup v1 assumptions and check cgroup v2 paths,
debe55
locations and values.
debe55
debe55
Continue using the older cgroup v1 terminology for APIs.
debe55
---
debe55
 .../Interop/Linux/cgroups/Interop.cgroups.cs  | 116 ++++++++++++++----
debe55
 src/Common/tests/Common.Tests.csproj          |   4 +
debe55
 .../tests/Tests/Interop/cgroupsTests.cs       | 107 ++++++++++++++++
debe55
 .../tests/DescriptionNameTests.cs             |   2 +-
debe55
 4 files changed, 206 insertions(+), 23 deletions(-)
debe55
 create mode 100644 src/Common/tests/Tests/Interop/cgroupsTests.cs
debe55
debe55
diff --git a/src/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs b/src/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs
debe55
index 0ffd4d7b7c03..186fe0516c5b 100644
debe55
--- a/src/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs
debe55
+++ b/src/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs
debe55
@@ -9,17 +9,22 @@
debe55
 
debe55
 internal static partial class Interop
debe55
 {
debe55
+    /// <summary>Provides access to some cgroup (v1 and v2) features</summary>
debe55
     internal static partial class cgroups
debe55
     {
debe55
+        // For cgroup v1, see https://www.kernel.org/doc/Documentation/cgroup-v1/
debe55
+        // For cgroup v2, see https://www.kernel.org/doc/Documentation/cgroup-v2.txt
debe55
+
debe55
+        /// <summary>The version of cgroup that's being used </summary>
debe55
+        internal enum CGroupVersion { None, CGroup1, CGroup2 };
debe55
+
debe55
         /// <summary>Path to mountinfo file in procfs for the current process.</summary>
debe55
         private const string ProcMountInfoFilePath = "/proc/self/mountinfo";
debe55
         /// <summary>Path to cgroup directory in procfs for the current process.</summary>
debe55
         private const string ProcCGroupFilePath = "/proc/self/cgroup";
debe55
 
debe55
-        /// <summary>Path to the found cgroup location, or null if it couldn't be found.</summary>
debe55
-        internal static readonly string s_cgroupMemoryPath = FindCGroupPath("memory");
debe55
-        /// <summary>Path to the found cgroup memory limit_in_bytes path, or null if it couldn't be found.</summary>
debe55
-        private static readonly string s_cgroupMemoryLimitPath = s_cgroupMemoryPath != null ? s_cgroupMemoryPath + "/memory.limit_in_bytes" : null;
debe55
+        /// <summary>Path to the found cgroup memory limit path, or null if it couldn't be found.</summary>
debe55
+        internal static readonly string s_cgroupMemoryLimitPath = FindCGroupMemoryLimitPath();
debe55
 
debe55
         /// <summary>Tries to read the memory limit from the cgroup memory location.</summary>
debe55
         /// <param name="limit">The read limit, or 0 if it couldn't be read.</param>
debe55
@@ -42,7 +47,7 @@ public static bool TryGetMemoryLimit(out ulong limit)
debe55
         /// <param name="path">The path to the file to parse.</param>
debe55
         /// <param name="result">The parsed result, or 0 if it couldn't be parsed.</param>
debe55
         /// <returns>true if the value was read successfully; otherwise, false.</returns>
debe55
-        private static bool TryReadMemoryValueFromFile(string path, out ulong result)
debe55
+        internal static bool TryReadMemoryValueFromFile(string path, out ulong result)
debe55
         {
debe55
             if (File.Exists(path))
debe55
             {
debe55
@@ -79,6 +84,11 @@ private static bool TryReadMemoryValueFromFile(string path, out ulong result)
debe55
                         result = checked(ulongValue * multiplier);
debe55
                         return true;
debe55
                     }
debe55
+
debe55
+                    // 'max' is also a possible valid value
debe55
+                    //
debe55
+                    // Treat this as 'no memory limit' and let the caller
debe55
+                    // fallback to reading the real limit via other means
debe55
                 }
debe55
                 catch (Exception e)
debe55
                 {
debe55
@@ -90,12 +100,35 @@ private static bool TryReadMemoryValueFromFile(string path, out ulong result)
debe55
             return false;
debe55
         }
debe55
 
debe55
+        /// <summary>Find the cgroup memory limit path.</summary>
debe55
+        /// <returns>The limit path if found; otherwise, null.</returns>
debe55
+        private static string FindCGroupMemoryLimitPath()
debe55
+        {
debe55
+            string cgroupMemoryPath = FindCGroupPath("memory", out CGroupVersion version);
debe55
+            if (cgroupMemoryPath != null)
debe55
+            {
debe55
+                if (version == CGroupVersion.CGroup1)
debe55
+                {
debe55
+                    return cgroupMemoryPath + "/memory.limit_in_bytes";
debe55
+                }
debe55
+
debe55
+                if (version == CGroupVersion.CGroup2)
debe55
+                {
debe55
+                    // 'memory.high' is a soft limit; the process may get throttled
debe55
+                    // 'memory.max' is where OOM killer kicks in
debe55
+                    return cgroupMemoryPath + "/memory.max";
debe55
+                }
debe55
+            }
debe55
+
debe55
+            return null;
debe55
+        }
debe55
+
debe55
         /// <summary>Find the cgroup path for the specified subsystem.</summary>
debe55
         /// <param name="subsystem">The subsystem, e.g. "memory".</param>
debe55
         /// <returns>The cgroup path if found; otherwise, null.</returns>
debe55
-        private static string FindCGroupPath(string subsystem)
debe55
+        private static string FindCGroupPath(string subsystem, out CGroupVersion version)
debe55
         {
debe55
-            if (TryFindHierarchyMount(subsystem, out string hierarchyRoot, out string hierarchyMount) &&
debe55
+            if (TryFindHierarchyMount(subsystem, out version, out string hierarchyRoot, out string hierarchyMount) &&
debe55
                 TryFindCGroupPathForSubsystem(subsystem, out string cgroupPathRelativeToMount))
debe55
             {
debe55
                 // For a host cgroup, we need to append the relative path.
debe55
@@ -113,19 +146,24 @@ private static string FindCGroupPath(string subsystem)
debe55
         /// <param name="root">The path of the directory in the filesystem which forms the root of this mount; null if not found.</param>
debe55
         /// <param name="path">The path of the mount point relative to the process's root directory; null if not found.</param>
debe55
         /// <returns>true if the mount was found; otherwise, null.</returns>
debe55
-        private static bool TryFindHierarchyMount(string subsystem, out string root, out string path)
debe55
+        private static bool TryFindHierarchyMount(string subsystem, out CGroupVersion version, out string root, out string path)
debe55
         {
debe55
-            if (File.Exists(ProcMountInfoFilePath))
debe55
+            return TryFindHierarchyMount(ProcMountInfoFilePath, subsystem, out version, out root, out path);
debe55
+        }
debe55
+
debe55
+        internal static bool TryFindHierarchyMount(string mountInfoFilePath, string subsystem, out CGroupVersion version, out string root, out string path)
debe55
+        {
debe55
+            if (File.Exists(mountInfoFilePath))
debe55
             {
debe55
                 try
debe55
                 {
debe55
-                    using (var reader = new StreamReader(ProcMountInfoFilePath))
debe55
+                    using (var reader = new StreamReader(mountInfoFilePath))
debe55
                     {
debe55
                         string line;
debe55
                         while ((line = reader.ReadLine()) != null)
debe55
                         {
debe55
                             // Look for an entry that has cgroup as the "filesystem type"
debe55
-                            // and that has options containing the specified subsystem.
debe55
+                            // and, for cgroup1, that has options containing the specified subsystem
debe55
                             // See man page for /proc/[pid]/mountinfo for details, e.g.:
debe55
                             //     (1)(2)(3)   (4)   (5)      (6)      (7)   (8) (9)   (10)         (11)
debe55
                             //     36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
debe55
@@ -148,17 +186,35 @@ private static bool TryFindHierarchyMount(string subsystem, out string root, out
debe55
                                 continue;
debe55
                             }
debe55
 
debe55
-                            if (postSeparatorlineParts[0] != "cgroup" ||
debe55
-                                Array.IndexOf(postSeparatorlineParts[2].Split(','), subsystem) < 0)
debe55
+                            bool validCGroup1Entry = ((postSeparatorlineParts[0] == "cgroup") &&
debe55
+                                    (Array.IndexOf(postSeparatorlineParts[2].Split(','), subsystem) >= 0));
debe55
+                            bool validCGroup2Entry = postSeparatorlineParts[0] == "cgroup2";
debe55
+
debe55
+                            if (!validCGroup1Entry && !validCGroup2Entry)
debe55
                             {
debe55
                                 // Not the relevant entry.
debe55
                                 continue;
debe55
                             }
debe55
 
debe55
-                            // Found the relevant entry.  Extract the mount root and path.
debe55
+                            // Found the relevant entry.  Extract the cgroup version, mount root and path.
debe55
+                            switch (postSeparatorlineParts[0])
debe55
+                            {
debe55
+                                case "cgroup":
debe55
+                                    version = CGroupVersion.CGroup1;
debe55
+                                    break;
debe55
+                                case "cgroup2":
debe55
+                                    version = CGroupVersion.CGroup2;
debe55
+                                    break;
debe55
+                                default:
debe55
+                                    version = CGroupVersion.None;
debe55
+                                    Debug.Fail($"invalid value for CGroupVersion \"{postSeparatorlineParts[0]}\"");
debe55
+                                    break;
debe55
+                            }
debe55
+
debe55
                             string[] lineParts = line.Substring(0, endOfOptionalFields).Split(' ');
debe55
                             root = lineParts[3];
debe55
                             path = lineParts[4];
debe55
+
debe55
                             return true;
debe55
                         }
debe55
                     }
debe55
@@ -169,6 +225,7 @@ private static bool TryFindHierarchyMount(string subsystem, out string root, out
debe55
                 }
debe55
             }
debe55
 
debe55
+            version = CGroupVersion.None;
debe55
             root = null;
debe55
             path = null;
debe55
             return false;
debe55
@@ -180,27 +237,42 @@ private static bool TryFindHierarchyMount(string subsystem, out string root, out
debe55
         /// <returns></returns>
debe55
         private static bool TryFindCGroupPathForSubsystem(string subsystem, out string path)
debe55
         {
debe55
-            if (File.Exists(ProcCGroupFilePath))
debe55
+            return TryFindCGroupPathForSubsystem(ProcCGroupFilePath, subsystem, out path);
debe55
+        }
debe55
+
debe55
+        internal static bool TryFindCGroupPathForSubsystem(string procCGroupFilePath, string subsystem, out string path)
debe55
+        {
debe55
+            if (File.Exists(procCGroupFilePath))
debe55
             {
debe55
                 try
debe55
                 {
debe55
-                    using (var reader = new StreamReader(ProcCGroupFilePath))
debe55
+                    using (var reader = new StreamReader(procCGroupFilePath))
debe55
                     {
debe55
                         string line;
debe55
                         while ((line = reader.ReadLine()) != null)
debe55
                         {
debe55
-                            // Find the first entry that has the subsystem listed in its controller
debe55
-                            // list. See man page for cgroups for /proc/[pid]/cgroups format, e.g:
debe55
-                            //     hierarchy-ID:controller-list:cgroup-path
debe55
-                            //     5:cpuacct,cpu,cpuset:/daemons
debe55
-
debe55
                             string[] lineParts = line.Split(':');
debe55
+
debe55
                             if (lineParts.Length != 3)
debe55
                             {
debe55
                                 // Malformed line.
debe55
                                 continue;
debe55
                             }
debe55
 
debe55
+                            // cgroup v2: Find the first entry that matches the cgroup v2 hierarchy:
debe55
+                            //     0::$PATH
debe55
+
debe55
+                            if ((lineParts[0] == "0") && (string.Empty == lineParts[1]))
debe55
+                            {
debe55
+                                path = lineParts[2];
debe55
+                                return true;
debe55
+                            }
debe55
+
debe55
+                            // cgroup v1: Find the first entry that has the subsystem listed in its controller
debe55
+                            // list. See man page for cgroups for /proc/[pid]/cgroups format, e.g:
debe55
+                            //     hierarchy-ID:controller-list:cgroup-path
debe55
+                            //     5:cpuacct,cpu,cpuset:/daemons
debe55
+
debe55
                             if (Array.IndexOf(lineParts[1].Split(','), subsystem) < 0)
debe55
                             {
debe55
                                 // Not the relevant entry.
debe55
@@ -214,7 +286,7 @@ private static bool TryFindCGroupPathForSubsystem(string subsystem, out string p
debe55
                 }
debe55
                 catch (Exception e)
debe55
                 {
debe55
-                    Debug.Fail($"Failed to read or parse \"{ProcMountInfoFilePath}\": {e}");
debe55
+                    Debug.Fail($"Failed to read or parse \"{procCGroupFilePath}\": {e}");
debe55
                 }
debe55
             }
debe55
 
debe55
diff --git a/src/Common/tests/Common.Tests.csproj b/src/Common/tests/Common.Tests.csproj
debe55
index a189d856348b..979c8dd7fbe6 100644
debe55
--- a/src/Common/tests/Common.Tests.csproj
debe55
+++ b/src/Common/tests/Common.Tests.csproj
debe55
@@ -12,6 +12,9 @@
debe55
     <Compile Include="$(CommonTestPath)\System\Security\Cryptography\ByteUtils.cs">
debe55
       <Link>Common\System\Security\Cryptography\ByteUtils.cs</Link>
debe55
     </Compile>
debe55
+    <Compile Include="$(CommonPath)\Interop\Linux\cgroups\Interop.cgroups.cs">
debe55
+      <Link>Common\Interop\Linux\cgroups\Interop.cgroups.cs</Link>
debe55
+    </Compile>
debe55
     <Compile Include="$(CommonPath)\Interop\Linux\procfs\Interop.ProcFsStat.cs">
debe55
       <Link>Common\Interop\Linux\procfs\Interop.ProcFsStat.cs</Link>
debe55
     </Compile>
debe55
@@ -69,6 +72,7 @@
debe55
     <Compile Include="$(CommonPath)\CoreLib\System\PasteArguments.cs">
debe55
       <Link>Common\CoreLib\System\PasteArguments.cs</Link>
debe55
     </Compile>
debe55
+    <Compile Include="Tests\Interop\cgroupsTests.cs" />
debe55
     <Compile Include="Tests\Interop\procfsTests.cs" />
debe55
     <Compile Include="Tests\System\CharArrayHelpersTests.cs" />
debe55
     <Compile Include="Tests\System\IO\PathInternal.Tests.cs" />
debe55
diff --git a/src/Common/tests/Tests/Interop/cgroupsTests.cs b/src/Common/tests/Tests/Interop/cgroupsTests.cs
debe55
new file mode 100644
debe55
index 000000000000..f16d9242879c
debe55
--- /dev/null
debe55
+++ b/src/Common/tests/Tests/Interop/cgroupsTests.cs
debe55
@@ -0,0 +1,107 @@
debe55
+// Licensed to the .NET Foundation under one or more agreements.
debe55
+// The .NET Foundation licenses this file to you under the MIT license.
debe55
+// See the LICENSE file in the project root for more information.
debe55
+
debe55
+using System;
debe55
+using System.IO;
debe55
+using System.Text;
debe55
+using Xunit;
debe55
+
debe55
+namespace Common.Tests
debe55
+{
debe55
+    public class cgroupsTests
debe55
+    {
debe55
+        [Theory]
debe55
+        [InlineData(true, "0",  0)]
debe55
+        [InlineData(false, "max",  0)]
debe55
+        [InlineData(true, "1k",  1024)]
debe55
+        [InlineData(true, "1K",  1024)]
debe55
+        public static void ValidateTryReadMemoryValue(bool expectedResult, string valueText, ulong expectedValue)
debe55
+        {
debe55
+            string path = Path.GetTempFileName();
debe55
+            try
debe55
+            {
debe55
+                File.WriteAllText(path, valueText);
debe55
+
debe55
+                bool result = Interop.cgroups.TryReadMemoryValueFromFile(path, out ulong val);
debe55
+
debe55
+                Assert.Equal(expectedResult, result);
debe55
+                if (result)
debe55
+                {
debe55
+                    Assert.Equal(expectedValue, val);
debe55
+                }
debe55
+            }
debe55
+            finally
debe55
+            {
debe55
+                File.Delete(path);
debe55
+            }
debe55
+        }
debe55
+
debe55
+        [Theory]
debe55
+        [InlineData(false, "0 0 0:0 / /foo ignore ignore - overlay overlay ignore", "ignore", 0, "/", "/")]
debe55
+        [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup2 cgroup2 ignore", "ignore", 2, "/", "/foo")]
debe55
+        [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup2 cgroup2 ignore", "memory", 2, "/", "/foo")]
debe55
+        [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup2 cgroup2 ignore", "cpu", 2, "/", "/foo")]
debe55
+        [InlineData(true, "0 0 0:0 / /foo ignore - cgroup2 cgroup2 ignore", "cpu", 2, "/", "/foo")]
debe55
+        [InlineData(true, "0 0 0:0 / /foo ignore ignore ignore - cgroup2 cgroup2 ignore", "cpu", 2, "/", "/foo")]
debe55
+        [InlineData(true, "0 0 0:0 / /foo-with-dashes ignore ignore - cgroup2 cgroup2 ignore", "ignore", 2, "/", "/foo-with-dashes")]
debe55
+        [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup cgroup memory", "memory", 1, "/", "/foo")]
debe55
+        [InlineData(true, "0 0 0:0 / /foo-with-dashes ignore ignore - cgroup cgroup memory", "memory", 1, "/", "/foo-with-dashes")]
debe55
+        [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup cgroup cpu,memory", "memory", 1, "/", "/foo")]
debe55
+        [InlineData(true, "0 0 0:0 / /foo ignore ignore - cgroup cgroup memory,cpu", "memory", 1, "/", "/foo")]
debe55
+        [InlineData(false, "0 0 0:0 / /foo ignore ignore - cgroup cgroup cpu", "memory", 0, "/", "/foo")]
debe55
+        public static void ParseValidateMountInfo(bool found, string procSelfMountInfoText, string subsystem, int expectedVersion, string expectedRoot, string expectedMount)
debe55
+        {
debe55
+            string path = Path.GetTempFileName();
debe55
+            try
debe55
+            {
debe55
+                File.WriteAllText(path, procSelfMountInfoText);
debe55
+
debe55
+                bool result = Interop.cgroups.TryFindHierarchyMount(path, subsystem,  out Interop.cgroups.CGroupVersion version, out string root, out string mount);
debe55
+
debe55
+                Assert.Equal(found, result);
debe55
+                if (found)
debe55
+                {
debe55
+                    Assert.Equal(expectedVersion, (int)version);
debe55
+                    Assert.Equal(expectedRoot, root);
debe55
+                    Assert.Equal(expectedMount, mount);
debe55
+                }
debe55
+            }
debe55
+            finally
debe55
+            {
debe55
+                File.Delete(path);
debe55
+            }
debe55
+        }
debe55
+
debe55
+        [Theory]
debe55
+        [InlineData(true, "0::/foo", "ignore", "/foo")]
debe55
+        [InlineData(true, "0::/bar", "ignore", "/bar")]
debe55
+        [InlineData(true, "0::frob", "ignore", "frob")]
debe55
+        [InlineData(false, "1::frob", "ignore", "ignore")]
debe55
+        [InlineData(true, "1:foo:bar", "foo", "bar")]
debe55
+        [InlineData(true, "2:foo:bar", "foo", "bar")]
debe55
+        [InlineData(false, "2:foo:bar", "bar", "ignore")]
debe55
+        [InlineData(true, "1:foo:bar\n2:eggs:spam", "foo", "bar")]
debe55
+        [InlineData(true, "1:foo:bar\n2:eggs:spam", "eggs", "spam")]
debe55
+        public static void ParseValidateProcCGroup(bool found, string procSelfCgroupText, string subsystem, string expectedMountPath)
debe55
+        {
debe55
+            string path = Path.GetTempFileName();
debe55
+            try
debe55
+            {
debe55
+                File.WriteAllText(path, procSelfCgroupText);
debe55
+
debe55
+                bool result = Interop.cgroups.TryFindCGroupPathForSubsystem(path, subsystem,  out string mountPath);
debe55
+
debe55
+                Assert.Equal(found, result);
debe55
+                if (found)
debe55
+                {
debe55
+                    Assert.Equal(expectedMountPath, mountPath);
debe55
+                }
debe55
+            }
debe55
+            finally
debe55
+            {
debe55
+                File.Delete(path);
debe55
+            }
debe55
+        }
debe55
+    }
debe55
+}
debe55
diff --git a/src/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs b/src/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs
debe55
index 910af2fd82b4..73f692898dbc 100644
debe55
--- a/src/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs
debe55
+++ b/src/System.Runtime.InteropServices.RuntimeInformation/tests/DescriptionNameTests.cs
debe55
@@ -40,7 +40,7 @@ public void DumpRuntimeInformationToConsole()
debe55
 
debe55
             Console.WriteLine($"### CURRENT DIRECTORY: {Environment.CurrentDirectory}");
debe55
 
debe55
-            string cgroupsLocation = Interop.cgroups.s_cgroupMemoryPath;
debe55
+            string cgroupsLocation = Interop.cgroups.s_cgroupMemoryLimitPath;
debe55
             if (cgroupsLocation != null)
debe55
             {
debe55
                 Console.WriteLine($"### CGROUPS MEMORY: {cgroupsLocation}");
debe55