diff --git a/.gitignore b/.gitignore index 528cd5b3..54f3c5db 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ example/example +cmd/cgctl diff --git a/cmd/cgctl/main.go b/cmd/cgctl/main.go new file mode 100644 index 00000000..46cffa54 --- /dev/null +++ b/cmd/cgctl/main.go @@ -0,0 +1,141 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "fmt" + "os" + + v2 "github.com/containerd/cgroups/v2" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + app.Name = "cgctl" + app.Version = "1" + app.Usage = "cgroup v2 management tool" + app.Flags = []cli.Flag{ + cli.BoolFlag{ + Name: "debug", + Usage: "enable debug output in the logs", + }, + cli.StringFlag{ + Name: "mountpoint", + Usage: "cgroup mountpoint", + Value: "/sys/fs/cgroup", + }, + } + app.Commands = []cli.Command{ + newCommand, + delCommand, + listCommand, + statCommand, + } + app.Before = func(clix *cli.Context) error { + if clix.GlobalBool("debug") { + logrus.SetLevel(logrus.DebugLevel) + } + return nil + } + if err := app.Run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +var newCommand = cli.Command{ + Name: "new", + Usage: "create a new cgroup", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "enable", + Usage: "enable the controllers for the group", + }, + }, + Action: func(clix *cli.Context) error { + path := clix.Args().First() + c, err := v2.NewManager(clix.GlobalString("mountpoint"), path, nil) + if err != nil { + return err + } + if clix.Bool("enable") { + controllers, err := c.ListControllers() + if err != nil { + return err + } + if err := c.ToggleControllers(controllers, v2.Enable); err != nil { + return err + } + } + return nil + }, +} + +var delCommand = cli.Command{ + Name: "del", + Usage: "delete a cgroup", + Action: func(clix *cli.Context) error { + path := clix.Args().First() + c, err := v2.LoadManager(clix.GlobalString("mountpoint"), path) + if err != nil { + return err + } + return c.Delete() + }, +} + +var listCommand = cli.Command{ + Name: "list", + Usage: "list processes in a cgroup", + Action: func(clix *cli.Context) error { + path := clix.Args().First() + c, err := v2.LoadManager(clix.GlobalString("mountpoint"), path) + if err != nil { + return err + } + procs, err := c.Procs(true) + if err != nil { + return err + } + for _, p := range procs { + fmt.Println(p) + } + return nil + }, +} + +var statCommand = cli.Command{ + Name: "stat", + Usage: "stat a cgroup", + Action: func(clix *cli.Context) error { + path := clix.Args().First() + c, err := v2.LoadManager(clix.GlobalString("mountpoint"), path) + if err != nil { + return err + } + stats, err := c.Stat() + if err != nil { + return err + } + for k, v := range stats { + fmt.Printf("%s->%d\n", k, v) + } + return nil + }, +} diff --git a/cmd/cgroups-playground/main.go b/cmd/cgroups-playground/main.go deleted file mode 100644 index 534852c0..00000000 --- a/cmd/cgroups-playground/main.go +++ /dev/null @@ -1,117 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package main - -import ( - "github.com/containerd/cgroups/v2" - stats2 "github.com/containerd/cgroups/v2/stats" - "github.com/opencontainers/runtime-spec/specs-go" - "github.com/sirupsen/logrus" - "os" -) - -func main() { - if err := xmain(); err != nil { - logrus.Fatalf("%+v", err) - } -} - -func xmain() error { - pid := os.Getpid() - g, err := v2.PidGroupPath(pid) - if err != nil { - return err - } - unifiedMountpoint := "/sys/fs/cgroup" - logrus.Infof("Loading V2 for %q (PID %d), mountpoint=%q", g, pid, unifiedMountpoint) - cg, err := v2.Load(unifiedMountpoint, g) - if err != nil { - return err - } - processes, err := cg.Processes(true) - if err != nil { - return err - } - logrus.Infof("Has %d processes (recursively)", len(processes)) - for i, s := range processes { - logrus.Infof("Process %d: %d", i, s.Pid) - } - subsystems := cg.Subsystems() - logrus.Infof("Has %d subsystems", len(subsystems)) - for i, s := range subsystems { - logrus.Infof("Subsystem %d: %q", i, s.Name()) - } - - cpuCgroup, err := v2.NewCpu(unifiedMountpoint) - if err != nil { - return err - } - var period, shares uint64 = 1000, 5000 - resources := specs.LinuxResources{ - CPU: &specs.LinuxCPU{Period: &period, Shares: &shares}, - } - err = cpuCgroup.Create(g, &resources) - if err != nil { - return err - } - stats := stats2.Metrics{ - CPU: &stats2.CPUStat{ - Usage: &stats2.CPUUsage{}, - }, - } - err = cpuCgroup.Stat(g, &stats) - if err != nil { - return err - } - logrus.Infof("CPU usage stats: usage in kernel mode - %d", stats.CPU.Usage.Kernel) - - err = memoryTest(unifiedMountpoint, g) - if err != nil { - return err - } - - return nil -} - -func memoryTest(unifiedMountpoint string, g v2.GroupPath) error { - memoryCgroup, err := v2.NewMemory(unifiedMountpoint) - if err != nil { - return err - } - var limit int64 = 10000 - resources := specs.LinuxResources{ - Memory: &specs.LinuxMemory{Limit: &limit}, - } - err = memoryCgroup.Create(g, &resources) - if err != nil { - return err - } - stats := stats2.Metrics{ - Memory: &stats2.MemoryStat{ - Usage: &stats2.MemoryEntry{}, - }, - } - err = memoryCgroup.Stat(g, &stats) - if err != nil { - return err - } - logrus.Infof("Memory usage stats: usage limit - %d", stats.Memory.Usage.Limit) - logrus.Infof("Memory usage stats: usage - %d", stats.Memory.Usage.Usage) - logrus.Infof("Memory usage stats: cache - %d", stats.Memory.Cache) - - return nil -} diff --git a/v2/cgroup.go b/v2/cgroup.go deleted file mode 100644 index 75bab4d9..00000000 --- a/v2/cgroup.go +++ /dev/null @@ -1,362 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package v2 - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strconv" - "strings" - "sync" - - statsv2 "github.com/containerd/cgroups/v2/stats" - specs "github.com/opencontainers/runtime-spec/specs-go" - "github.com/pkg/errors" -) - -// New returns a new control via the cgroup cgroups interface. -// -// unifiedMountpoint should be either "/sys/fs/cgroup" (pure v2) or -// "/sys/fs/cgroup/unified" (hybrid). -func New(unifiedMountpoint string, g GroupPath, resources *specs.LinuxResources, opts ...InitOpts) (Cgroup, error) { - if err := VerifyGroupPath(g); err != nil { - return nil, err - } - config := &initConfig{} - for _, o := range opts { - if err := o(config); err != nil { - return nil, err - } - } - subsystems, errs := defaults(unifiedMountpoint) - if len(subsystems) == 0 { - return nil, errors.Errorf("cannot detect any subsystem under %q: %+v", unifiedMountpoint, errs) - } - for _, s := range subsystems { - if c, ok := s.(Creator); ok { - if err := c.Create(g, resources); err != nil { - return nil, err - } - } - } - return &cgroup{ - g: g, - unifiedMountpoint: unifiedMountpoint, - subsystems: subsystems, - }, nil -} - -// Load will load an existing cgroup and allow it to be controlled -func Load(unifiedMountpoint string, g GroupPath, opts ...InitOpts) (Cgroup, error) { - if err := VerifyGroupPath(g); err != nil { - return nil, err - } - config := &initConfig{} - for _, o := range opts { - if err := o(config); err != nil { - return nil, err - } - } - subsystems, _ := defaults(unifiedMountpoint) - if len(subsystems) == 0 { - return nil, ErrCgroupDeleted - } - - return &cgroup{ - g: g, - unifiedMountpoint: unifiedMountpoint, - subsystems: subsystems, - }, nil -} - -type cgroup struct { - g GroupPath - unifiedMountpoint string - - subsystems []Subsystem - mu sync.Mutex - err error -} - -// New returns a new sub cgroup -func (c *cgroup) GroupPath() GroupPath { - return c.g -} - -// New returns a new sub cgroup -func (c *cgroup) New(name string, resources *specs.LinuxResources) (Cgroup, error) { - if strings.HasPrefix(name, "/") { - return nil, errors.New("name must be relative") - } - c.mu.Lock() - defer c.mu.Unlock() - if c.err != nil { - return nil, c.err - } - g := GroupPath(filepath.Join(string(c.g), name)) - var subsystems []Subsystem - for _, s := range c.subsystems { - if ok, _ := s.Available(g); ok { - subsystems = append(subsystems, s) - if c, ok := s.(Creator); ok { - if err := c.Create(g, resources); err != nil { - return nil, err - } - } - } - } - - return &cgroup{ - g: g, - unifiedMountpoint: c.unifiedMountpoint, - subsystems: subsystems, - }, nil -} - -// Subsystems returns all the subsystems that are currently being -// consumed by the group -func (c *cgroup) Subsystems() []Subsystem { - return c.subsystems -} - -// Add moves the provided process into the new cgroup -func (c *cgroup) Add(process Process) error { - if process.Pid <= 0 { - return ErrInvalidPid - } - c.mu.Lock() - defer c.mu.Unlock() - if c.err != nil { - return c.err - } - return c.add(process) -} - -func (c *cgroup) add(process Process) error { - if err := ioutil.WriteFile( - filepath.Join(c.unifiedMountpoint, string(c.g), cgroupProcs), - []byte(strconv.Itoa(process.Pid)), - defaultFilePerm, - ); err != nil { - return err - } - return nil -} - -// Delete will remove the control group from each of the subsystems registered -func (c *cgroup) Delete() error { - c.mu.Lock() - defer c.mu.Unlock() - if c.err != nil { - return c.err - } - var errors []string - for _, s := range c.subsystems { - if d, ok := s.(Deleter); ok { - if err := d.Delete(c.g); err != nil { - errors = append(errors, err.Error()) - } - continue - } - } - path := filepath.Join(c.unifiedMountpoint, string(c.g)) - if err := remove(path); err != nil { - errors = append(errors, err.Error()) - } - if len(errors) > 0 { - return fmt.Errorf("cgroups: unable to remove %q: %s", path, strings.Join(errors, ", ")) - } - c.err = ErrCgroupDeleted - return nil -} - -// Stat returns the current metrics for the cgroup -func (c *cgroup) Stat(handlers ...ErrorHandler) (*statsv2.Metrics, error) { - c.mu.Lock() - defer c.mu.Unlock() - if c.err != nil { - return nil, c.err - } - if len(handlers) == 0 { - handlers = append(handlers, errPassthrough) - } - var ( - stats = &statsv2.Metrics{} - wg = &sync.WaitGroup{} - errs = make(chan error, len(c.subsystems)) - ) - for _, s := range c.subsystems { - if ss, ok := s.(Stater); ok { - wg.Add(1) - go func() { - defer wg.Done() - if err := ss.Stat(c.g, stats); err != nil { - for _, eh := range handlers { - if herr := eh(err); herr != nil { - errs <- herr - } - } - } - }() - } - } - wg.Wait() - close(errs) - for err := range errs { - return nil, err - } - return stats, nil -} - -// Update updates the cgroup with the new resource values provided -// -// Be prepared to handle EBUSY when trying to update a cgroup with -// live processes and other operations like Stats being performed at the -// same time -func (c *cgroup) Update(resources *specs.LinuxResources) error { - c.mu.Lock() - defer c.mu.Unlock() - if c.err != nil { - return c.err - } - for _, s := range c.subsystems { - if u, ok := s.(Updater); ok { - if err := u.Update(c.g, resources); err != nil { - return err - } - } - } - return nil -} - -// Processes returns the processes running inside the cgroup along -// with the pid -func (c *cgroup) Processes(recursive bool) ([]Process, error) { - c.mu.Lock() - defer c.mu.Unlock() - if c.err != nil { - return nil, c.err - } - return c.processes(recursive) -} - -func (c *cgroup) processes(recursive bool) ([]Process, error) { - path := filepath.Join(c.unifiedMountpoint, string(c.g)) - var processes []Process - err := filepath.Walk(path, func(p string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !recursive && info.IsDir() { - if p == path { - return nil - } - return filepath.SkipDir - } - _, name := filepath.Split(p) - if name != cgroupProcs { - return nil - } - procs, err := parseCgroupProcsFile(p) - if err != nil { - return err - } - processes = append(processes, procs...) - return nil - }) - return processes, err -} - -// Freeze freezes the entire cgroup and all the processes inside it -func (c *cgroup) Freeze() error { - c.mu.Lock() - defer c.mu.Unlock() - if c.err != nil { - return c.err - } - s := c.getSubsystem(Freezer) - if s == nil { - return ErrFreezerNotSupported - } - return s.(*freezerController).Freeze(c.g) -} - -// Thaw thaws out the cgroup and all the processes inside it -func (c *cgroup) Thaw() error { - c.mu.Lock() - defer c.mu.Unlock() - if c.err != nil { - return c.err - } - s := c.getSubsystem(Freezer) - if s == nil { - return ErrFreezerNotSupported - } - return s.(*freezerController).Thaw(c.g) -} - -// State returns the state of the cgroup and its processes -func (c *cgroup) State() State { - c.mu.Lock() - defer c.mu.Unlock() - if c.err != nil && c.err == ErrCgroupDeleted { - return Deleted - } - s := c.getSubsystem(Freezer) - if s == nil { - return Thawed - } - state, err := s.(*freezerController).state(c.g) - if err != nil { - return Unknown - } - return state -} - -// MoveTo does a recursive move subsystem by subsystem of all the processes -// inside the group -func (c *cgroup) MoveTo(destination Cgroup) error { - c.mu.Lock() - defer c.mu.Unlock() - if c.err != nil { - return c.err - } - processes, err := c.processes(true) - if err != nil { - return err - } - for _, p := range processes { - if err := destination.Add(p); err != nil { - if strings.Contains(err.Error(), "no such process") { - continue - } - return err - } - } - return nil -} - -func (c *cgroup) getSubsystem(n Name) Subsystem { - for _, s := range c.subsystems { - if s.Name() == n { - return s - } - } - return nil -} diff --git a/v2/control.go b/v2/control.go deleted file mode 100644 index 88e863d1..00000000 --- a/v2/control.go +++ /dev/null @@ -1,70 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package v2 - -import ( - "os" - - statsv2 "github.com/containerd/cgroups/v2/stats" - specs "github.com/opencontainers/runtime-spec/specs-go" -) - -const ( - cgroupProcs = "cgroup.procs" - defaultDirPerm = 0755 -) - -// defaultFilePerm is a var so that the test framework can change the filemode -// of all files created when the tests are running. The difference between the -// tests and real world use is that files like "cgroup.procs" will exist when writing -// to a read cgroup filesystem and do not exist prior when running in the tests. -// this is set to a non 0 value in the test code -var defaultFilePerm = os.FileMode(0) - -type Process struct { - // Pid is the process id of the process - Pid int -} - -// Cgroup handles interactions with the individual groups to perform -// actions on them as them main interface to this cgroup package -type Cgroup interface { - GroupPath() GroupPath - // New creates a new cgroup under the calling cgroup - New(string, *specs.LinuxResources) (Cgroup, error) - // Add adds a process to the cgroup (cgroup.procs) - Add(Process) error - // Delete removes the cgroup as a whole - Delete() error - // MoveTo moves all the processes under the calling cgroup to the provided one - // subsystems are moved one at a time - MoveTo(Cgroup) error - // Stat returns the stats for all subsystems in the cgroup - Stat(...ErrorHandler) (*statsv2.Metrics, error) - // Update updates all the subsystems with the provided resource changes - Update(resources *specs.LinuxResources) error - // Processes returns all the processes in a select subsystem for the cgroup - Processes(bool) ([]Process, error) - // Freeze freezes or pauses all processes inside the cgroup - Freeze() error - // Thaw thaw or resumes all processes inside the cgroup - Thaw() error - // State returns the cgroups current state - State() State - // Subsystems returns all the subsystems in the cgroup - Subsystems() []Subsystem -} diff --git a/v2/cpu.go b/v2/cpu.go index bb6275e1..1203354c 100644 --- a/v2/cpu.go +++ b/v2/cpu.go @@ -16,108 +16,23 @@ package v2 -import ( - "bufio" - "io/ioutil" - "os" - "path/filepath" - "strconv" - - statsv2 "github.com/containerd/cgroups/v2/stats" - specs "github.com/opencontainers/runtime-spec/specs-go" -) - -func NewCpu(unifiedMountpoint string) (*cpuController, error) { - c := &cpuController{ - unifiedMountpoint: unifiedMountpoint, - } - - ok, err := c.Available("/") - if err != nil { - return nil, err - } - if !ok { - return nil, ErrCPUNotSupported - } - return c, nil -} - -type cpuController struct { - unifiedMountpoint string -} - -func (c *cpuController) Name() Name { - return Cpu -} - -func (c *cpuController) path(g GroupPath) string { - return filepath.Join(c.unifiedMountpoint, string(g)) +type CPU struct { + Weight *uint64 + Max *uint64 } -func (c *cpuController) Create(g GroupPath, resources *specs.LinuxResources) error { - if err := os.MkdirAll(c.path(g), defaultDirPerm); err != nil { - return err +func (r *CPU) Values() (o []Value) { + if r.Weight != nil { + o = append(o, Value{ + filename: "cpu.weight", + value: *r.Weight, + }) } - if cpuShares := resources.CPU.Shares; cpuShares != nil { - // Converting cgroups configuration from v1 to v2 - // more here https://github.com/containers/crun/blob/master/crun.1.md#cgroup-v2 - convertedWeight := (1 + ((*cpuShares-2)*9999)/262142) - weight := []byte(strconv.FormatUint(convertedWeight, 10)) - if err := ioutil.WriteFile( - filepath.Join(c.path(g), "cpu.weight"), - weight, - defaultFilePerm, - ); err != nil { - return err - } + if r.Max != nil { + o = append(o, Value{ + filename: "cpu.max", + value: *r.Max, + }) } - - if cpuPeriod := resources.CPU.Period; cpuPeriod != nil { - max := []byte(strconv.FormatUint(*cpuPeriod, 10)) - if err := ioutil.WriteFile( - filepath.Join(c.path(g), "cpu.max"), - max, - defaultFilePerm, - ); err != nil { - return err - } - } - - return nil -} - -func (c *cpuController) Update(g GroupPath, resources *specs.LinuxResources) error { - return c.Create(g, resources) -} - -func (c *cpuController) Stat(g GroupPath, stats *statsv2.Metrics) error { - f, err := os.Open(filepath.Join(c.path(g), "cpu.stat")) - if err != nil { - return err - } - defer f.Close() - // get or create the cpu field because cpuacct can also set values on this struct - sc := bufio.NewScanner(f) - for sc.Scan() { - if err := sc.Err(); err != nil { - return err - } - key, v, err := parseKV(sc.Text()) - if err != nil { - return err - } - switch key { - case "usage_usec": - stats.CPU.Usage.Total = v - case "user_usec": - stats.CPU.Usage.User = v - case "system_usec": - stats.CPU.Usage.Kernel = v - } - } - return nil -} - -func (c *cpuController) Available(g GroupPath) (bool, error) { - return available(c.unifiedMountpoint, g, Cpu) + return o } diff --git a/v2/errors.go b/v2/errors.go index 60af72dd..46d2d9c2 100644 --- a/v2/errors.go +++ b/v2/errors.go @@ -31,8 +31,7 @@ var ( ErrCPUNotSupported = errors.New("cgroups: cpu cgroup (v2) not supported on this system") ErrCgroupDeleted = errors.New("cgroups: cgroup deleted") ErrNoCgroupMountDestination = errors.New("cgroups: cannot find cgroup mount destination") - - ErrInvalidGroupPath = errors.New("cgroups: group path format must be compatible with /proc/PID/cgroup") + ErrInvalidGroupPath = errors.New("cgroups: invalid group path") ) // ErrorHandler is a function that handles and acts on errors diff --git a/v2/freezer.go b/v2/freezer.go deleted file mode 100644 index 13c81dc2..00000000 --- a/v2/freezer.go +++ /dev/null @@ -1,112 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package v2 - -import ( - "io/ioutil" - "path/filepath" - "strings" - "time" - - "github.com/pkg/errors" -) - -func NewFreezer(unifiedMountpoint string) (*freezerController, error) { - f := &freezerController{ - unifiedMountpoint: unifiedMountpoint, - } - ok, err := f.Available("/") - if err != nil { - return nil, err - } - if !ok { - return nil, ErrFreezerNotSupported - } - return f, nil -} - -type freezerController struct { - unifiedMountpoint string -} - -func (f *freezerController) Name() Name { - return Freezer -} - -func (f *freezerController) path(g GroupPath) string { - return filepath.Join(f.unifiedMountpoint, string(g)) -} - -func (f *freezerController) Available(g GroupPath) (bool, error) { - return available(f.unifiedMountpoint, g, Freezer) -} - -func (f *freezerController) Freeze(g GroupPath) error { - return f.waitState(g, Frozen) -} - -func (f *freezerController) Thaw(g GroupPath) error { - return f.waitState(g, Thawed) -} - -func (f *freezerController) changeState(g GroupPath, state State) error { - desiredState := "" - switch state { - case Frozen: - desiredState = "1" - case Thawed: - desiredState = "0" - default: - return errors.Errorf("unknown state %q", state) - } - return ioutil.WriteFile( - filepath.Join(f.path(g), "cgroup.freeze"), - []byte(strings.ToUpper(string(desiredState))), - defaultFilePerm, - ) -} - -func (f *freezerController) state(g GroupPath) (State, error) { - current, err := ioutil.ReadFile(filepath.Join(f.path(g), "cgroup.freeze")) - if err != nil { - return "", err - } - switch strings.TrimSpace(string(current)) { - case "1": - return Frozen, nil - case "0": - return Thawed, nil - default: - return "", nil - } -} - -func (f *freezerController) waitState(g GroupPath, state State) error { - for { - if err := f.changeState(g, state); err != nil { - return err - } - current, err := f.state(g) - if err != nil { - return err - } - if current == state { - return nil - } - time.Sleep(1 * time.Millisecond) - } -} diff --git a/v2/manager.go b/v2/manager.go new file mode 100644 index 00000000..40b1ed15 --- /dev/null +++ b/v2/manager.go @@ -0,0 +1,310 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package v2 + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" +) + +const ( + subtreeControl = "cgroup.subtree_control" + controllersFile = "cgroup.controllers" +) + +type cgValuer interface { + Values() []Value +} + +// Resources for a cgroups v2 unified hierarchy +type Resources struct { + CPU *CPU + Memory *Memory + Pids *Pids +} + +// Values returns the raw filenames and values that +// can be written to the unified hierarchy +func (r *Resources) Values() (o []Value) { + values := []cgValuer{ + r.CPU, + r.Memory, + r.Pids, + } + for _, v := range values { + if v == nil { + continue + } + o = append(o, v.Values()...) + } + return o +} + +// Value of a cgroup setting +type Value struct { + filename string + value interface{} +} + +// write the value to the full, absolute path, of a unified hierarchy +func (c *Value) write(path string, perm os.FileMode) error { + var data []byte + switch t := c.value.(type) { + case uint64: + data = []byte(strconv.FormatUint(t, 10)) + case int64: + data = []byte(strconv.FormatInt(t, 10)) + case []byte: + data = t + case string: + data = []byte(t) + default: + return ErrInvalidFormat + } + return ioutil.WriteFile( + filepath.Join(path, c.filename), + data, + perm, + ) +} + +func writeValues(path string, values []Value) error { + for _, o := range values { + if err := o.write(path, defaultFilePerm); err != nil { + return err + } + } + return nil +} + +func NewManager(mountpoint string, group string, resources *Resources) (*Manager, error) { + if group == "" { + return nil, ErrInvalidGroupPath + } + path := filepath.Join(mountpoint, group) + if err := os.MkdirAll(path, defaultDirPerm); err != nil { + return nil, err + } + if resources != nil { + if err := writeValues(path, resources.Values()); err != nil { + // clean up cgroup dir on failure + os.Remove(path) + return nil, err + } + } + return &Manager{ + unifiedMountpoint: mountpoint, + path: path, + }, nil +} + +func LoadManager(mountpoint string, group string) (*Manager, error) { + if group == "" { + return nil, ErrInvalidGroupPath + } + path := filepath.Join(mountpoint, group) + return &Manager{ + unifiedMountpoint: mountpoint, + path: path, + }, nil +} + +type Manager struct { + unifiedMountpoint string + path string +} + +func (c *Manager) ListControllers() ([]string, error) { + f, err := os.Open(filepath.Join(c.path, controllersFile)) + if err != nil { + return nil, err + } + defer f.Close() + + var ( + out []string + s = bufio.NewScanner(f) + ) + s.Split(bufio.ScanWords) + for s.Scan() { + if err := s.Err(); err != nil { + return nil, err + } + out = append(out, s.Text()) + } + return out, nil +} + +type ControllerToggle int + +const ( + Enable ControllerToggle = iota + 1 + Disable +) + +func toggleFunc(controllers []string, prefix string) []string { + out := make([]string, len(controllers)) + for i, c := range controllers { + out[i] = prefix + c + } + return out +} + +func (c *Manager) ToggleControllers(controllers []string, t ControllerToggle) error { + f, err := os.OpenFile(filepath.Join(c.path, subtreeControl), os.O_WRONLY, 0) + if err != nil { + return err + } + defer f.Close() + switch t { + case Enable: + controllers = toggleFunc(controllers, "+") + case Disable: + controllers = toggleFunc(controllers, "-") + } + _, err = f.WriteString(strings.Join(controllers, " ")) + return err +} + +func (c *Manager) NewChild(name string, resources *Resources) (*Manager, error) { + if strings.HasPrefix(name, "/") { + return nil, errors.New("name must be relative") + } + path := filepath.Join(c.path, name) + if err := os.MkdirAll(path, defaultDirPerm); err != nil { + return nil, err + } + if err := writeValues(path, resources.Values()); err != nil { + // clean up cgroup dir on failure + os.Remove(path) + return nil, err + } + return &Manager{ + unifiedMountpoint: c.unifiedMountpoint, + path: path, + }, nil +} + +func (c *Manager) AddProc(pid uint64) error { + v := Value{ + filename: cgroupProcs, + value: pid, + } + return writeValues(c.path, []Value{v}) +} + +func (c *Manager) Delete() error { + return remove(c.path) +} + +func (c *Manager) Procs(recursive bool) ([]uint64, error) { + var processes []uint64 + err := filepath.Walk(c.path, func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !recursive && info.IsDir() { + if p == c.path { + return nil + } + return filepath.SkipDir + } + _, name := filepath.Split(p) + if name != cgroupProcs { + return nil + } + procs, err := parseCgroupProcsFile(p) + if err != nil { + return err + } + processes = append(processes, procs...) + return nil + }) + return processes, err +} + +func (c *Manager) Stat() (map[string]uint64, error) { + controllers, err := c.ListControllers() + if err != nil { + return nil, err + } + out := make(map[string]uint64) + for _, controller := range controllers { + filename := fmt.Sprintf("%s.stat", controller) + if err := readStatsFile(c.path, filename, out); err != nil { + if os.IsNotExist(err) { + continue + } + return nil, err + } + } + return out, nil +} + +func readStatsFile(path string, file string, out map[string]uint64) error { + f, err := os.Open(filepath.Join(path, file)) + if err != nil { + return err + } + defer f.Close() + + s := bufio.NewScanner(f) + for s.Scan() { + if err := s.Err(); err != nil { + return err + } + name, value, err := parseKV(s.Text()) + if err != nil { + return err + } + out[name] = value + } + return nil +} + +func (c *Manager) Freeze() error { + return c.freeze(c.path, Frozen) +} + +func (c *Manager) Thaw() error { + return c.freeze(c.path, Thawed) +} + +func (c *Manager) freeze(path string, state State) error { + values := state.Values() + for { + if err := writeValues(path, values); err != nil { + return err + } + current, err := fetchState(path) + if err != nil { + return err + } + if current == state { + return nil + } + time.Sleep(1 * time.Millisecond) + } +} diff --git a/v2/memory.go b/v2/memory.go index 1ecb7573..584ac2db 100644 --- a/v2/memory.go +++ b/v2/memory.go @@ -16,174 +16,30 @@ package v2 -import ( - "bufio" - "io/ioutil" - "os" - "path/filepath" - "strconv" - "strings" - - statsv2 "github.com/containerd/cgroups/v2/stats" - specs "github.com/opencontainers/runtime-spec/specs-go" -) - -// NewMemory returns a Memory controller given the root folder of cgroups. -func NewMemory(unifiedMountpoint string) (*memoryController, error) { - mc := &memoryController{ - unifiedMountpoint: unifiedMountpoint, - } - ok, err := mc.Available("/") - if err != nil { - return nil, err - } - if !ok { - return nil, ErrMemoryNotSupported - } - return mc, nil -} - -type memoryController struct { - unifiedMountpoint string -} - -func (m *memoryController) Name() Name { - return Memory -} - -func (m *memoryController) path(g GroupPath) string { - return filepath.Join(m.unifiedMountpoint, string(g)) -} - -func (m *memoryController) Create(g GroupPath, resources *specs.LinuxResources) error { - if err := os.MkdirAll(m.path(g), defaultDirPerm); err != nil { - return err - } - if resources.Memory == nil { - return nil - } - if resources.Memory.Kernel != nil { - // Check if kernel memory is enabled - // We have to limit the kernel memory here as it won't be accounted at all - // until a limit is set on the cgroup and limit cannot be set once the - // cgroup has children, or if there are already tasks in the cgroup. - for _, i := range []int64{1, -1} { - if err := ioutil.WriteFile( - filepath.Join(m.path(g), "memory.kmem.limit_in_bytes"), - []byte(strconv.FormatInt(i, 10)), - defaultFilePerm, - ); err != nil { - return err - } - } - } - // According to the crun docs v1 cgroups memory.swap can be directly converted to memory.swap_max in v2 - // https://github.com/containers/crun/blob/master/crun.1.md#cgroup-v2 - if mSwap := resources.Memory.Swap; mSwap != nil { - if err := ioutil.WriteFile( - filepath.Join(m.path(g), "memory.swap_max"), - []byte(strconv.FormatInt(*mSwap, 10)), - defaultFilePerm, - ); err != nil { - return err - } - } - - // According to the crun docs v1 cgroups memory.limit can be directly converted to memory.max in v2 - if mMax := resources.Memory.Limit; mMax != nil { - if err := ioutil.WriteFile( - filepath.Join(m.path(g), "memory.max"), - []byte(strconv.FormatInt(*mMax, 10)), - defaultFilePerm, - ); err != nil { - return err - } - } - - // According to the crun docs v1 cgroups memory.reservation can be directly converted to memory.high in v2 - if mHigh := resources.Memory.Reservation; mHigh != nil { - if err := ioutil.WriteFile( - filepath.Join(m.path(g), "memory.high"), - []byte(strconv.FormatInt(*mHigh, 10)), - defaultFilePerm, - ); err != nil { - return err - } - } - return nil -} - -func (m *memoryController) Update(g GroupPath, resources *specs.LinuxResources) error { - return m.Create(g, resources) -} - -func (m *memoryController) Stat(g GroupPath, stats *statsv2.Metrics) error { - f, err := os.Open(filepath.Join(m.path(g), "memory.stat")) - if err != nil { - return err - } - defer f.Close() - stats.Memory = &statsv2.MemoryStat{ - Usage: &statsv2.MemoryEntry{}, - Swap: &statsv2.MemoryEntry{}, - } - sc := bufio.NewScanner(f) - for sc.Scan() { - if err := sc.Err(); err != nil { - return err - } - key, v, err := parseKV(sc.Text()) - if err != nil { - return err - } - if key == "cache" { - stats.Memory.Cache = v - break - } - } - - for _, t := range []struct { - module string - entry *statsv2.MemoryEntry - }{ - { - module: "", - entry: stats.Memory.Usage, - }, - { - module: "memsw", - entry: stats.Memory.Swap, - }, - } { - - for _, tt := range []struct { - name string - value *uint64 - }{ - { - name: "usage_in_bytes", - value: &t.entry.Usage, - }, - { - name: "limit_in_bytes", - value: &t.entry.Limit, - }, - } { - parts := []string{"memory"} - if t.module != "" { - parts = append(parts, t.module) - } - parts = append(parts, tt.name) - v, err := readUint(filepath.Join(m.path(g), strings.Join(parts, "."))) - if err != nil { - return err - } - *tt.value = v - } - } - return nil -} - -func (m *memoryController) Available(g GroupPath) (bool, error) { - return available(m.unifiedMountpoint, g, Memory) +type Memory struct { + Swap *int64 + Max *int64 + High *int64 +} + +func (r *Memory) Values() (o []Value) { + if r.Swap != nil { + o = append(o, Value{ + filename: "memory.swap_max", + value: *r.Swap, + }) + } + if r.Max != nil { + o = append(o, Value{ + filename: "memory.max", + value: *r.Max, + }) + } + if r.High != nil { + o = append(o, Value{ + filename: "memory.high", + value: *r.High, + }) + } + return o } diff --git a/v2/opts.go b/v2/opts.go deleted file mode 100644 index e00c4e84..00000000 --- a/v2/opts.go +++ /dev/null @@ -1,25 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package v2 - -// InitOpts allows configuration for the creation or loading of a cgroup -type InitOpts func(*initConfig) error - -// initConfig provides configuration options for the creation -// or loading of a cgroup and its subsystems -type initConfig struct { -} diff --git a/v2/paths.go b/v2/paths.go index 054f73e0..171e45bd 100644 --- a/v2/paths.go +++ b/v2/paths.go @@ -19,42 +19,21 @@ package v2 import ( "fmt" "path/filepath" - "strings" ) -// GroupPath is a string that appears as the third field in /proc/PID/cgroup. -// e.g. "/user.slice/user-1001.slice/session-1.scope" -// -// GroupPath must not contain "/sys/fs/cgroup" prefix. -// GroupPath must be a absolute path starts with "/". -type GroupPath string - // NestedGroupPath will nest the cgroups based on the calling processes cgroup // placing its child processes inside its own path -func NestedGroupPath(suffix string) (GroupPath, error) { +func NestedGroupPath(suffix string) (string, error) { path, err := parseCgroupFile("/proc/self/cgroup") if err != nil { return "", err } - return GroupPath(filepath.Join(string(path), suffix)), nil + return filepath.Join(string(path), suffix), nil } // PidGroupPath will return the correct cgroup paths for an existing process running inside a cgroup // This is commonly used for the Load function to restore an existing container -func PidGroupPath(pid int) (GroupPath, error) { +func PidGroupPath(pid int) (string, error) { p := fmt.Sprintf("/proc/%d/cgroup", pid) return parseCgroupFile(p) } - -// VerifyGroupPath verifies the format of g. -// VerifyGroupPath doesn't verify whether g actually exists on the system. -func VerifyGroupPath(g GroupPath) error { - s := string(g) - if !strings.HasPrefix(s, "/") { - return ErrInvalidGroupPath - } - if strings.HasPrefix(s, "/sys/fs/cgroup") { - return ErrInvalidGroupPath - } - return nil -} diff --git a/v2/paths_test.go b/v2/paths_test.go index fbf2e59e..d3650099 100644 --- a/v2/paths_test.go +++ b/v2/paths_test.go @@ -30,7 +30,7 @@ func TestVerifyGroupPath(t *testing.T) { "/sys/fs/cgroup/unified/foo": false, } for s, valid := range valids { - err := VerifyGroupPath(GroupPath(s)) + err := VerifyGroupPath(s) if valid { if err != nil { t.Error(err) diff --git a/v2/pids.go b/v2/pids.go index 0f748402..0b5aa0c3 100644 --- a/v2/pids.go +++ b/v2/pids.go @@ -16,83 +16,22 @@ package v2 -import ( - "io/ioutil" - "os" - "path/filepath" - "strconv" - "strings" +import "strconv" - statsv2 "github.com/containerd/cgroups/v2/stats" - specs "github.com/opencontainers/runtime-spec/specs-go" -) - -func NewPids(unifiedMountpoint string) (*pidsController, error) { - p := &pidsController{ - unifiedMountpoint: unifiedMountpoint, - } - ok, err := p.Available("/") - if err != nil { - return nil, err - } - if !ok { - return nil, ErrPidsNotSupported - } - return p, nil -} - -type pidsController struct { - unifiedMountpoint string +type Pids struct { + Max int64 } -func (p *pidsController) Name() Name { - return Pids -} - -func (p *pidsController) path(g GroupPath) string { - return filepath.Join(p.unifiedMountpoint, string(g)) -} - -func (p *pidsController) Available(g GroupPath) (bool, error) { - return available(p.unifiedMountpoint, g, Pids) -} - -func (p *pidsController) Create(g GroupPath, resources *specs.LinuxResources) error { - if err := os.MkdirAll(p.path(g), defaultDirPerm); err != nil { - return err - } - if resources.Pids != nil && resources.Pids.Limit > 0 { - return ioutil.WriteFile( - filepath.Join(p.path(g), "pids.max"), - []byte(strconv.FormatInt(resources.Pids.Limit, 10)), - defaultFilePerm, - ) - } - return nil -} - -func (p *pidsController) Update(g GroupPath, resources *specs.LinuxResources) error { - return p.Create(g, resources) -} - -func (p *pidsController) Stat(g GroupPath, stats *statsv2.Metrics) error { - current, err := readUint(filepath.Join(p.path(g), "pids.current")) - if err != nil { - return err - } - var max uint64 - maxData, err := ioutil.ReadFile(filepath.Join(p.path(g), "pids.max")) - if err != nil { - return err - } - if maxS := strings.TrimSpace(string(maxData)); maxS != "max" { - if max, err = parseUint(maxS, 10, 64); err != nil { - return err +func (r *Pids) Values() (o []Value) { + if r.Max != 0 { + limit := "max" + if r.Max > 0 { + limit = strconv.FormatInt(r.Max, 10) } + o = append(o, Value{ + filename: "pids.max", + value: limit, + }) } - stats.Pids = &statsv2.PidsStat{ - Current: current, - Limit: max, - } - return nil + return o } diff --git a/v2/state.go b/v2/state.go index b7af3395..09b75b6c 100644 --- a/v2/state.go +++ b/v2/state.go @@ -16,6 +16,12 @@ package v2 +import ( + "io/ioutil" + "path/filepath" + "strings" +) + // State is a type that represents the state of the current cgroup type State string @@ -24,4 +30,36 @@ const ( Thawed State = "thawed" Frozen State = "frozen" Deleted State = "deleted" + + cgroupFreeze = "cgroup.freeze" ) + +func (s State) Values() []Value { + v := Value{ + filename: cgroupFreeze, + } + switch s { + case Frozen: + v.value = "1" + case Thawed: + v.value = "0" + } + return []Value{ + v, + } +} + +func fetchState(path string) (State, error) { + current, err := ioutil.ReadFile(filepath.Join(path, cgroupFreeze)) + if err != nil { + return Unknown, err + } + switch strings.TrimSpace(string(current)) { + case "1": + return Frozen, nil + case "0": + return Thawed, nil + default: + return Unknown, nil + } +} diff --git a/v2/subsystem.go b/v2/subsystem.go deleted file mode 100644 index ed3c21ab..00000000 --- a/v2/subsystem.go +++ /dev/null @@ -1,111 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package v2 - -import ( - "io/ioutil" - "path/filepath" - "strings" - - statsv2 "github.com/containerd/cgroups/v2/stats" - specs "github.com/opencontainers/runtime-spec/specs-go" -) - -// Name is a typed name for a cgroup subsystem. -// Corresponds to cgroup.controllers format. -type Name string - -const ( - // Devices is a pseudo-controller, implemented since kernel 4.15 - Devices Name = "devices" - // Hugetlb is not implemented in upstream kernel (patch available) - // Hugetlb Name = "hugetlb" - // Freezer is a pseudo-controller, implemented since kernel 5.2 - Freezer Name = "freezer" - // Pids is implemented since kernel 4.5 - Pids Name = "pids" - // PerfEvent is implemented since kernel 4.11 - PerfEvent Name = "perf_event" - // Cpuset is implemented since kernel 5.0 - Cpuset Name = "cpuset" - // Cpu is implemented since kernel 4.15 - Cpu Name = "cpu" - // Memory is implemented since kernel 4.5 - Memory Name = "memory" - // Io is implemented since kernel 4.5 - Io Name = "io" - // Rdma is implemented since kernel 4.11 - Rdma Name = "rdma" -) - -// Subsystems returns available subsystems -func Subsystems(unifiedMountpoint string, g GroupPath) ([]Name, error) { - if err := VerifyGroupPath(g); err != nil { - return nil, err - } - path := filepath.Join(unifiedMountpoint, string(g), "cgroup.controllers") - b, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - subsystems := []Name{ - Devices, - Freezer, - } - for _, s := range strings.Fields(string(b)) { - subsystems = append(subsystems, Name(s)) - } - return subsystems, nil -} - -func available(unifiedMountpoint string, g GroupPath, name Name) (bool, error) { - names, err := Subsystems(unifiedMountpoint, g) - if err != nil { - return false, err - } - for _, n := range names { - if n == name { - return true, nil - } - } - return false, nil -} - -type Subsystem interface { - Name() Name - Available(g GroupPath) (bool, error) -} - -type Creator interface { - Subsystem - Create(g GroupPath, resources *specs.LinuxResources) error -} - -type Deleter interface { - Subsystem - Delete(g GroupPath) error -} - -type Stater interface { - Subsystem - Stat(g GroupPath, stats *statsv2.Metrics) error -} - -type Updater interface { - Subsystem - Update(g GroupPath, resources *specs.LinuxResources) error -} diff --git a/v2/utils.go b/v2/utils.go index cc648fb5..018f669f 100644 --- a/v2/utils.go +++ b/v2/utils.go @@ -29,34 +29,17 @@ import ( "github.com/pkg/errors" ) -func defaults(unifiedMountpoint string) ([]Subsystem, map[Name]error) { - var subsystems []Subsystem - unavailables := make(map[Name]error, 0) - if x, err := NewPids(unifiedMountpoint); err != nil { - unavailables[Pids] = err - } else { - subsystems = append(subsystems, x) - } - - if x, err := NewFreezer(unifiedMountpoint); err != nil { - unavailables[Freezer] = err - } else { - subsystems = append(subsystems, x) - } - - if x, err := NewCpu(unifiedMountpoint); err != nil { - unavailables[Cpu] = err - } else { - subsystems = append(subsystems, x) - } +const ( + cgroupProcs = "cgroup.procs" + defaultDirPerm = 0755 +) - if x, err := NewMemory(unifiedMountpoint); err != nil { - unavailables[Memory] = err - } else { - subsystems = append(subsystems, x) - } - return subsystems, unavailables -} +// defaultFilePerm is a var so that the test framework can change the filemode +// of all files created when the tests are running. The difference between the +// tests and real world use is that files like "cgroup.procs" will exist when writing +// to a read cgroup filesystem and do not exist prior when running in the tests. +// this is set to a non 0 value in the test code +var defaultFilePerm = os.FileMode(0) // remove will remove a cgroup path handling EAGAIN and EBUSY errors and // retrying the remove after a exp timeout @@ -76,25 +59,23 @@ func remove(path string) error { } // parseCgroupProcsFile parses /sys/fs/cgroup/$GROUPPATH/cgroup.procs -func parseCgroupProcsFile(path string) ([]Process, error) { +func parseCgroupProcsFile(path string) ([]uint64, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() var ( - out []Process + out []uint64 s = bufio.NewScanner(f) ) for s.Scan() { if t := s.Text(); t != "" { - pid, err := strconv.Atoi(t) + pid, err := strconv.ParseUint(t, 10, 0) if err != nil { return nil, err } - out = append(out, Process{ - Pid: pid, - }) + out = append(out, pid) } } return out, nil @@ -140,8 +121,8 @@ func parseUint(s string, base, bitSize int) (uint64, error) { return v, nil } -// parseCgroupFile parses /proc/PID/cgroup file and return GroupPath -func parseCgroupFile(path string) (GroupPath, error) { +// parseCgroupFile parses /proc/PID/cgroup file and return string +func parseCgroupFile(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err @@ -150,7 +131,7 @@ func parseCgroupFile(path string) (GroupPath, error) { return parseCgroupFromReader(f) } -func parseCgroupFromReader(r io.Reader) (GroupPath, error) { +func parseCgroupFromReader(r io.Reader) (string, error) { var ( s = bufio.NewScanner(r) ) @@ -160,15 +141,13 @@ func parseCgroupFromReader(r io.Reader) (GroupPath, error) { } var ( text = s.Text() - parts = strings.SplitN(text, ":", 3) + parts = strings.SplitN(text, "::", 2) ) - if len(parts) < 3 { + if len(parts) < 2 { return "", fmt.Errorf("invalid cgroup entry: %q", text) } // text is like "0::/user.slice/user-1001.slice/session-1.scope" - if parts[0] == "0" && parts[1] == "" { - return GroupPath(parts[2]), nil - } + return parts[1], nil } return "", fmt.Errorf("cgroup path not found") }