diff --git a/docs/systemd.md b/docs/systemd.md index c74e2e2f407..a119d935a41 100644 --- a/docs/systemd.md +++ b/docs/systemd.md @@ -91,6 +91,7 @@ The following tables summarize which properties are translated. | cpu.mems | AllowedMemoryNodes | v244 | | unified.cpu.max | CPUQuota, CPUQuotaPeriodSec | v242 | | unified.cpu.weight | CPUWeight | | +| unified.cpu.idle | CPUWeight | v252 | | unified.cpuset.cpus | AllowedCPUs | v244 | | unified.cpuset.mems | AllowedMemoryNodes | v244 | | unified.memory.high | MemoryHigh | | diff --git a/libcontainer/cgroups/systemd/systemd_test.go b/libcontainer/cgroups/systemd/systemd_test.go index 40584f78eba..8e333a00649 100644 --- a/libcontainer/cgroups/systemd/systemd_test.go +++ b/libcontainer/cgroups/systemd/systemd_test.go @@ -2,8 +2,10 @@ package systemd import ( "os" + "reflect" "testing" + systemdDbus "github.com/coreos/go-systemd/v22/dbus" "github.com/opencontainers/runc/libcontainer/cgroups" "github.com/opencontainers/runc/libcontainer/configs" ) @@ -95,3 +97,85 @@ func TestUnitExistsIgnored(t *testing.T) { } } } + +func TestUnifiedResToSystemdProps(t *testing.T) { + if !IsRunningSystemd() { + t.Skip("Test requires systemd.") + } + if !cgroups.IsCgroup2UnifiedMode() { + t.Skip("cgroup v2 is required") + } + + cm := newDbusConnManager(os.Geteuid() != 0) + + testCases := []struct { + name string + minVer int + res map[string]string + expError bool + expProps []systemdDbus.Property + }{ + { + name: "empty map", + res: map[string]string{}, + }, + { + name: "only cpu.idle=1", + minVer: cpuIdleSupportedVersion, + res: map[string]string{ + "cpu.idle": "1", + }, + expProps: []systemdDbus.Property{ + newProp("CPUWeight", uint64(0)), + }, + }, + { + name: "only cpu.idle=0", + minVer: cpuIdleSupportedVersion, + res: map[string]string{ + "cpu.idle": "0", + }, + }, + { + name: "cpu.idle=1 and cpu.weight=1000", + minVer: cpuIdleSupportedVersion, + res: map[string]string{ + "cpu.idle": "1", + "cpu.weight": "1000", + }, + expProps: []systemdDbus.Property{ + newProp("CPUWeight", uint64(0)), + }, + }, + { + name: "cpu.idle=0 and cpu.weight=1000", + minVer: cpuIdleSupportedVersion, + res: map[string]string{ + "cpu.idle": "0", + "cpu.weight": "1000", + }, + expProps: []systemdDbus.Property{ + newProp("CPUWeight", uint64(1000)), + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + if tc.minVer != 0 && systemdVersion(cm) < tc.minVer { + t.Skipf("requires systemd >= %d", tc.minVer) + } + props, err := unifiedResToSystemdProps(cm, tc.res) + if err != nil && !tc.expError { + t.Fatalf("expected no error, got: %v", err) + } + if err == nil && tc.expError { + t.Fatal("expected error, got nil") + } + if !reflect.DeepEqual(tc.expProps, props) { + t.Errorf("wrong properties (exp %+v, got %+v)", tc.expProps, props) + } + }) + } +} diff --git a/libcontainer/cgroups/systemd/v2.go b/libcontainer/cgroups/systemd/v2.go index 29d629afccf..bccc62dd7c4 100644 --- a/libcontainer/cgroups/systemd/v2.go +++ b/libcontainer/cgroups/systemd/v2.go @@ -20,6 +20,10 @@ import ( "github.com/opencontainers/runc/libcontainer/configs" ) +const ( + cpuIdleSupportedVersion = 252 +) + type UnifiedManager struct { mu sync.Mutex cgroups *configs.Cgroup @@ -60,6 +64,13 @@ func NewUnifiedManager(config *configs.Cgroup, path string) (*UnifiedManager, er func unifiedResToSystemdProps(cm *dbusConnManager, res map[string]string) (props []systemdDbus.Property, _ error) { var err error + shouldSetIdle := func(v string) bool { + // The only valid values for cpu.idle is 0 or 1. Since it is + // not possible to directly set cpu.idle to 0 via systemd, + // ignore 0. + return v == "1" && systemdVersion(cm) >= cpuIdleSupportedVersion + } + for k, v := range res { if strings.Contains(k, "/") { return nil, fmt.Errorf("unified resource %q must be a file name (no slashes)", k) @@ -97,6 +108,11 @@ func unifiedResToSystemdProps(cm *dbusConnManager, res map[string]string) (props addCpuQuota(cm, &props, quota, period) case "cpu.weight": + if shouldSetIdle(strings.TrimSpace(res["cpu.idle"])) { + // Do not add duplicate CPUWeight property + // (see case "cpu.idle" below). + continue + } num, err := strconv.ParseUint(v, 10, 64) if err != nil { return nil, fmt.Errorf("unified resource %q value conversion error: %w", k, err) @@ -104,6 +120,14 @@ func unifiedResToSystemdProps(cm *dbusConnManager, res map[string]string) (props props = append(props, newProp("CPUWeight", num)) + case "cpu.idle": + if shouldSetIdle(v) { + // Setting CPUWeight to 0 tells systemd + // to set cpu.idle to 1. + props = append(props, + newProp("CPUWeight", uint64(0))) + } + case "cpuset.cpus", "cpuset.mems": bits, err := RangeToBits(v) if err != nil { @@ -212,7 +236,17 @@ func genV2ResourcesProperties(dirPath string, r *configs.Resources, cm *dbusConn newProp("MemorySwapMax", uint64(swap))) } - if r.CpuWeight != 0 { + CPUWeightSet := false + if r.CPUIdle != nil { + sdVer := systemdVersion(cm) + if sdVer >= cpuIdleSupportedVersion && *r.CPUIdle != 0 { + properties = append(properties, + newProp("CPUWeight", uint64(0))) + CPUWeightSet = true + } + } + // Ignore CpuWeight if CPUIdle is set. + if !CPUWeightSet && r.CpuWeight != 0 { properties = append(properties, newProp("CPUWeight", r.CpuWeight)) } diff --git a/tests/integration/update.bats b/tests/integration/update.bats index 62f80793836..867e45a928b 100644 --- a/tests/integration/update.bats +++ b/tests/integration/update.bats @@ -462,6 +462,55 @@ EOF check_cgroup_value "cpu.idle" "1" } +@test "update cgroup cpu.idle via systemd v252+" { + requires systemd cgroups_cpu_idle + [ $EUID -ne 0 ] && requires rootless_cgroup + if [ "$(systemd_version)" -lt 252 ]; then + skip "requires systemd >= v252" + fi + + runc run -d --console-socket "$CONSOLE_SOCKET" test_update + [ "$status" -eq 0 ] + check_cgroup_value "cpu.idle" "0" + + runc update --cpu-idle 1 test_update + check_cgroup_value "cpu.idle" "1" + + runc update --cpu-idle 0 test_update + check_cgroup_value "cpu.idle" "0" + + # If cpu-idle is set, cpu-period is ignored. + runc update --cpu-period 10000 --cpu-idle 1 test_update + check_cgroup_value "cpu.idle" "1" + + # Setting any cpu-period should reset cpu.idle to 0. + runc update --cpu-period 20000 test_update + check_cgroup_value "cpu.idle" "0" + + # Setting values via unified map. + + # If cpu.idle is set, cpu.weight is ignored. + runc update -r - test_update <