mirror of
				https://github.com/containers/podman.git
				synced 2025-11-04 08:56:05 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			576 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			576 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
//go:build linux
 | 
						|
// +build linux
 | 
						|
 | 
						|
package cgroups
 | 
						|
 | 
						|
import (
 | 
						|
	"bufio"
 | 
						|
	"context"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"io/ioutil"
 | 
						|
	"math"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"github.com/containers/storage/pkg/unshare"
 | 
						|
	systemdDbus "github.com/coreos/go-systemd/v22/dbus"
 | 
						|
	"github.com/godbus/dbus/v5"
 | 
						|
	"github.com/opencontainers/runc/libcontainer/cgroups"
 | 
						|
	"github.com/opencontainers/runc/libcontainer/cgroups/fs2"
 | 
						|
	"github.com/opencontainers/runc/libcontainer/configs"
 | 
						|
	"github.com/sirupsen/logrus"
 | 
						|
)
 | 
						|
 | 
						|
var (
 | 
						|
	// ErrCgroupDeleted means the cgroup was deleted
 | 
						|
	ErrCgroupDeleted = errors.New("cgroup deleted")
 | 
						|
	// ErrCgroupV1Rootless means the cgroup v1 were attempted to be used in rootless environment
 | 
						|
	ErrCgroupV1Rootless = errors.New("no support for CGroups V1 in rootless environments")
 | 
						|
	ErrStatCgroup       = errors.New("no cgroup available for gathering user statistics")
 | 
						|
)
 | 
						|
 | 
						|
// CgroupControl controls a cgroup hierarchy
 | 
						|
type CgroupControl struct {
 | 
						|
	cgroup2 bool
 | 
						|
	config  *configs.Cgroup
 | 
						|
	systemd bool
 | 
						|
	// List of additional cgroup subsystems joined that
 | 
						|
	// do not have a custom handler.
 | 
						|
	additionalControllers []controller
 | 
						|
}
 | 
						|
 | 
						|
type controller struct {
 | 
						|
	name    string
 | 
						|
	symlink bool
 | 
						|
}
 | 
						|
 | 
						|
type controllerHandler interface {
 | 
						|
	Create(*CgroupControl) (bool, error)
 | 
						|
	Apply(*CgroupControl, *configs.Resources) error
 | 
						|
	Destroy(*CgroupControl) error
 | 
						|
	Stat(*CgroupControl, *cgroups.Stats) error
 | 
						|
}
 | 
						|
 | 
						|
const (
 | 
						|
	cgroupRoot = "/sys/fs/cgroup"
 | 
						|
	// CPU is the cpu controller
 | 
						|
	CPU = "cpu"
 | 
						|
	// CPUAcct is the cpuacct controller
 | 
						|
	CPUAcct = "cpuacct"
 | 
						|
	// CPUset is the cpuset controller
 | 
						|
	CPUset = "cpuset"
 | 
						|
	// Memory is the memory controller
 | 
						|
	Memory = "memory"
 | 
						|
	// Pids is the pids controller
 | 
						|
	Pids = "pids"
 | 
						|
	// Blkio is the blkio controller
 | 
						|
	Blkio = "blkio"
 | 
						|
)
 | 
						|
 | 
						|
var handlers map[string]controllerHandler
 | 
						|
 | 
						|
func init() {
 | 
						|
	handlers = make(map[string]controllerHandler)
 | 
						|
	handlers[CPU] = getCPUHandler()
 | 
						|
	handlers[CPUset] = getCpusetHandler()
 | 
						|
	handlers[Memory] = getMemoryHandler()
 | 
						|
	handlers[Pids] = getPidsHandler()
 | 
						|
	handlers[Blkio] = getBlkioHandler()
 | 
						|
}
 | 
						|
 | 
						|
// getAvailableControllers get the available controllers
 | 
						|
func getAvailableControllers(exclude map[string]controllerHandler, cgroup2 bool) ([]controller, error) {
 | 
						|
	if cgroup2 {
 | 
						|
		controllers := []controller{}
 | 
						|
		controllersFile := cgroupRoot + "/cgroup.controllers"
 | 
						|
		// rootless cgroupv2: check available controllers for current user, systemd or servicescope will inherit
 | 
						|
		if unshare.IsRootless() {
 | 
						|
			userSlice, err := getCgroupPathForCurrentProcess()
 | 
						|
			if err != nil {
 | 
						|
				return controllers, err
 | 
						|
			}
 | 
						|
			// userSlice already contains '/' so not adding here
 | 
						|
			basePath := cgroupRoot + userSlice
 | 
						|
			controllersFile = fmt.Sprintf("%s/cgroup.controllers", basePath)
 | 
						|
		}
 | 
						|
		controllersFileBytes, err := ioutil.ReadFile(controllersFile)
 | 
						|
		if err != nil {
 | 
						|
			return nil, fmt.Errorf("failed while reading controllers for cgroup v2 from %q: %w", controllersFile, err)
 | 
						|
		}
 | 
						|
		for _, controllerName := range strings.Fields(string(controllersFileBytes)) {
 | 
						|
			c := controller{
 | 
						|
				name:    controllerName,
 | 
						|
				symlink: false,
 | 
						|
			}
 | 
						|
			controllers = append(controllers, c)
 | 
						|
		}
 | 
						|
		return controllers, nil
 | 
						|
	}
 | 
						|
 | 
						|
	subsystems, _ := cgroupV1GetAllSubsystems()
 | 
						|
	controllers := []controller{}
 | 
						|
	// cgroupv1 and rootless: No subsystem is available: delegation is unsafe.
 | 
						|
	if unshare.IsRootless() {
 | 
						|
		return controllers, nil
 | 
						|
	}
 | 
						|
 | 
						|
	for _, name := range subsystems {
 | 
						|
		if _, found := exclude[name]; found {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		fileInfo, err := os.Stat(cgroupRoot + "/" + name)
 | 
						|
		if err != nil {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		c := controller{
 | 
						|
			name:    name,
 | 
						|
			symlink: !fileInfo.IsDir(),
 | 
						|
		}
 | 
						|
		controllers = append(controllers, c)
 | 
						|
	}
 | 
						|
 | 
						|
	return controllers, nil
 | 
						|
}
 | 
						|
 | 
						|
// GetAvailableControllers get string:bool map of all the available controllers
 | 
						|
func GetAvailableControllers(exclude map[string]controllerHandler, cgroup2 bool) ([]string, error) {
 | 
						|
	availableControllers, err := getAvailableControllers(exclude, cgroup2)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	controllerList := []string{}
 | 
						|
	for _, controller := range availableControllers {
 | 
						|
		controllerList = append(controllerList, controller.name)
 | 
						|
	}
 | 
						|
 | 
						|
	return controllerList, nil
 | 
						|
}
 | 
						|
 | 
						|
func cgroupV1GetAllSubsystems() ([]string, error) {
 | 
						|
	f, err := os.Open("/proc/cgroups")
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	defer f.Close()
 | 
						|
 | 
						|
	subsystems := []string{}
 | 
						|
 | 
						|
	s := bufio.NewScanner(f)
 | 
						|
	for s.Scan() {
 | 
						|
		text := s.Text()
 | 
						|
		if text[0] != '#' {
 | 
						|
			parts := strings.Fields(text)
 | 
						|
			if len(parts) >= 4 && parts[3] != "0" {
 | 
						|
				subsystems = append(subsystems, parts[0])
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if err := s.Err(); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return subsystems, nil
 | 
						|
}
 | 
						|
 | 
						|
func getCgroupPathForCurrentProcess() (string, error) {
 | 
						|
	path := fmt.Sprintf("/proc/%d/cgroup", os.Getpid())
 | 
						|
	f, err := os.Open(path)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
	defer f.Close()
 | 
						|
 | 
						|
	cgroupPath := ""
 | 
						|
	s := bufio.NewScanner(f)
 | 
						|
	for s.Scan() {
 | 
						|
		text := s.Text()
 | 
						|
		procEntries := strings.SplitN(text, "::", 2)
 | 
						|
		// set process cgroupPath only if entry is valid
 | 
						|
		if len(procEntries) > 1 {
 | 
						|
			cgroupPath = procEntries[1]
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if err := s.Err(); err != nil {
 | 
						|
		return cgroupPath, err
 | 
						|
	}
 | 
						|
	return cgroupPath, nil
 | 
						|
}
 | 
						|
 | 
						|
// getCgroupv1Path is a helper function to get the cgroup v1 path
 | 
						|
func (c *CgroupControl) getCgroupv1Path(name string) string {
 | 
						|
	return filepath.Join(cgroupRoot, name, c.config.Path)
 | 
						|
}
 | 
						|
 | 
						|
// initialize initializes the specified hierarchy
 | 
						|
func (c *CgroupControl) initialize() (err error) {
 | 
						|
	createdSoFar := map[string]controllerHandler{}
 | 
						|
	defer func() {
 | 
						|
		if err != nil {
 | 
						|
			for name, ctr := range createdSoFar {
 | 
						|
				if err := ctr.Destroy(c); err != nil {
 | 
						|
					logrus.Warningf("error cleaning up controller %s for %s", name, c.config.Path)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}()
 | 
						|
	if c.cgroup2 {
 | 
						|
		if err := createCgroupv2Path(filepath.Join(cgroupRoot, c.config.Path)); err != nil {
 | 
						|
			return fmt.Errorf("error creating cgroup path %s: %w", c.config.Path, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	for name, handler := range handlers {
 | 
						|
		created, err := handler.Create(c)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		if created {
 | 
						|
			createdSoFar[name] = handler
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if !c.cgroup2 {
 | 
						|
		// We won't need to do this for cgroup v2
 | 
						|
		for _, ctr := range c.additionalControllers {
 | 
						|
			if ctr.symlink {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			path := c.getCgroupv1Path(ctr.name)
 | 
						|
			if err := os.MkdirAll(path, 0o755); err != nil {
 | 
						|
				return fmt.Errorf("error creating cgroup path for %s: %w", ctr.name, err)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func readFileAsUint64(path string) (uint64, error) {
 | 
						|
	data, err := ioutil.ReadFile(path)
 | 
						|
	if err != nil {
 | 
						|
		return 0, err
 | 
						|
	}
 | 
						|
	v := cleanString(string(data))
 | 
						|
	if v == "max" {
 | 
						|
		return math.MaxUint64, nil
 | 
						|
	}
 | 
						|
	ret, err := strconv.ParseUint(v, 10, 64)
 | 
						|
	if err != nil {
 | 
						|
		return ret, fmt.Errorf("parse %s from %s: %w", v, path, err)
 | 
						|
	}
 | 
						|
	return ret, nil
 | 
						|
}
 | 
						|
 | 
						|
func readFileByKeyAsUint64(path, key string) (uint64, error) {
 | 
						|
	content, err := ioutil.ReadFile(path)
 | 
						|
	if err != nil {
 | 
						|
		return 0, err
 | 
						|
	}
 | 
						|
	for _, line := range strings.Split(string(content), "\n") {
 | 
						|
		fields := strings.SplitN(line, " ", 2)
 | 
						|
		if fields[0] == key {
 | 
						|
			v := cleanString(string(fields[1]))
 | 
						|
			if v == "max" {
 | 
						|
				return math.MaxUint64, nil
 | 
						|
			}
 | 
						|
			ret, err := strconv.ParseUint(v, 10, 64)
 | 
						|
			if err != nil {
 | 
						|
				return ret, fmt.Errorf("parse %s from %s: %w", v, path, err)
 | 
						|
			}
 | 
						|
			return ret, nil
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return 0, fmt.Errorf("no key named %s from %s", key, path)
 | 
						|
}
 | 
						|
 | 
						|
// New creates a new cgroup control
 | 
						|
func New(path string, resources *configs.Resources) (*CgroupControl, error) {
 | 
						|
	cgroup2, err := IsCgroup2UnifiedMode()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	control := &CgroupControl{
 | 
						|
		cgroup2: cgroup2,
 | 
						|
		config: &configs.Cgroup{
 | 
						|
			Path:      path,
 | 
						|
			Resources: resources,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	if !cgroup2 {
 | 
						|
		controllers, err := getAvailableControllers(handlers, false)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		control.additionalControllers = controllers
 | 
						|
	}
 | 
						|
 | 
						|
	if err := control.initialize(); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return control, nil
 | 
						|
}
 | 
						|
 | 
						|
// NewSystemd creates a new cgroup control
 | 
						|
func NewSystemd(path string, resources *configs.Resources) (*CgroupControl, error) {
 | 
						|
	cgroup2, err := IsCgroup2UnifiedMode()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	control := &CgroupControl{
 | 
						|
		cgroup2: cgroup2,
 | 
						|
		systemd: true,
 | 
						|
		config: &configs.Cgroup{
 | 
						|
			Path:      path,
 | 
						|
			Resources: resources,
 | 
						|
			Rootless:  unshare.IsRootless(),
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	return control, nil
 | 
						|
}
 | 
						|
 | 
						|
// Load loads an existing cgroup control
 | 
						|
func Load(path string) (*CgroupControl, error) {
 | 
						|
	cgroup2, err := IsCgroup2UnifiedMode()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	control := &CgroupControl{
 | 
						|
		cgroup2: cgroup2,
 | 
						|
		systemd: false,
 | 
						|
		config: &configs.Cgroup{
 | 
						|
			Path: path,
 | 
						|
		},
 | 
						|
	}
 | 
						|
	if !cgroup2 {
 | 
						|
		controllers, err := getAvailableControllers(handlers, false)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		control.additionalControllers = controllers
 | 
						|
	}
 | 
						|
	if !cgroup2 {
 | 
						|
		oneExists := false
 | 
						|
		// check that the cgroup exists at least under one controller
 | 
						|
		for name := range handlers {
 | 
						|
			p := control.getCgroupv1Path(name)
 | 
						|
			if _, err := os.Stat(p); err == nil {
 | 
						|
				oneExists = true
 | 
						|
				break
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		// if there is no controller at all, raise an error
 | 
						|
		if !oneExists {
 | 
						|
			if unshare.IsRootless() {
 | 
						|
				return nil, ErrCgroupV1Rootless
 | 
						|
			}
 | 
						|
			// compatible with the error code
 | 
						|
			// used by containerd/cgroups
 | 
						|
			return nil, ErrCgroupDeleted
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return control, nil
 | 
						|
}
 | 
						|
 | 
						|
// CreateSystemdUnit creates the systemd cgroup
 | 
						|
func (c *CgroupControl) CreateSystemdUnit(path string) error {
 | 
						|
	if !c.systemd {
 | 
						|
		return fmt.Errorf("the cgroup controller is not using systemd")
 | 
						|
	}
 | 
						|
 | 
						|
	conn, err := systemdDbus.NewWithContext(context.TODO())
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer conn.Close()
 | 
						|
 | 
						|
	return systemdCreate(c.config.Resources, path, conn)
 | 
						|
}
 | 
						|
 | 
						|
// GetUserConnection returns an user connection to D-BUS
 | 
						|
func GetUserConnection(uid int) (*systemdDbus.Conn, error) {
 | 
						|
	return systemdDbus.NewConnection(func() (*dbus.Conn, error) {
 | 
						|
		return dbusAuthConnection(uid, dbus.SessionBusPrivate)
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// CreateSystemdUserUnit creates the systemd cgroup for the specified user
 | 
						|
func (c *CgroupControl) CreateSystemdUserUnit(path string, uid int) error {
 | 
						|
	if !c.systemd {
 | 
						|
		return fmt.Errorf("the cgroup controller is not using systemd")
 | 
						|
	}
 | 
						|
 | 
						|
	conn, err := GetUserConnection(uid)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer conn.Close()
 | 
						|
 | 
						|
	return systemdCreate(c.config.Resources, path, conn)
 | 
						|
}
 | 
						|
 | 
						|
func dbusAuthConnection(uid int, createBus func(opts ...dbus.ConnOption) (*dbus.Conn, error)) (*dbus.Conn, error) {
 | 
						|
	conn, err := createBus()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(uid))}
 | 
						|
 | 
						|
	err = conn.Auth(methods)
 | 
						|
	if err != nil {
 | 
						|
		conn.Close()
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	if err := conn.Hello(); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return conn, nil
 | 
						|
}
 | 
						|
 | 
						|
// Delete cleans a cgroup
 | 
						|
func (c *CgroupControl) Delete() error {
 | 
						|
	return c.DeleteByPath(c.config.Path)
 | 
						|
}
 | 
						|
 | 
						|
// DeleteByPathConn deletes the specified cgroup path using the specified
 | 
						|
// dbus connection if needed.
 | 
						|
func (c *CgroupControl) DeleteByPathConn(path string, conn *systemdDbus.Conn) error {
 | 
						|
	if c.systemd {
 | 
						|
		return systemdDestroyConn(path, conn)
 | 
						|
	}
 | 
						|
	if c.cgroup2 {
 | 
						|
		return rmDirRecursively(filepath.Join(cgroupRoot, c.config.Path))
 | 
						|
	}
 | 
						|
	var lastError error
 | 
						|
	for _, h := range handlers {
 | 
						|
		if err := h.Destroy(c); err != nil {
 | 
						|
			lastError = err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	for _, ctr := range c.additionalControllers {
 | 
						|
		if ctr.symlink {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		p := c.getCgroupv1Path(ctr.name)
 | 
						|
		if err := rmDirRecursively(p); err != nil {
 | 
						|
			lastError = fmt.Errorf("remove %s: %w", p, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return lastError
 | 
						|
}
 | 
						|
 | 
						|
// DeleteByPath deletes the specified cgroup path
 | 
						|
func (c *CgroupControl) DeleteByPath(path string) error {
 | 
						|
	if c.systemd {
 | 
						|
		conn, err := systemdDbus.NewWithContext(context.TODO())
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		defer conn.Close()
 | 
						|
		return c.DeleteByPathConn(path, conn)
 | 
						|
	}
 | 
						|
	return c.DeleteByPathConn(path, nil)
 | 
						|
}
 | 
						|
 | 
						|
// Update updates the cgroups
 | 
						|
func (c *CgroupControl) Update(resources *configs.Resources) error {
 | 
						|
	for _, h := range handlers {
 | 
						|
		if err := h.Apply(c, resources); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// AddPid moves the specified pid to the cgroup
 | 
						|
func (c *CgroupControl) AddPid(pid int) error {
 | 
						|
	pidString := []byte(fmt.Sprintf("%d\n", pid))
 | 
						|
 | 
						|
	if c.cgroup2 {
 | 
						|
		path := filepath.Join(cgroupRoot, c.config.Path)
 | 
						|
		return fs2.CreateCgroupPath(path, c.config)
 | 
						|
	}
 | 
						|
 | 
						|
	names := make([]string, 0, len(handlers))
 | 
						|
	for n := range handlers {
 | 
						|
		names = append(names, n)
 | 
						|
	}
 | 
						|
 | 
						|
	for _, c := range c.additionalControllers {
 | 
						|
		if !c.symlink {
 | 
						|
			names = append(names, c.name)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	for _, n := range names {
 | 
						|
		// If we aren't using cgroup2, we won't write correctly to unified hierarchy
 | 
						|
		if !c.cgroup2 && n == "unified" {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		p := filepath.Join(c.getCgroupv1Path(n), "tasks")
 | 
						|
		if err := ioutil.WriteFile(p, pidString, 0o644); err != nil {
 | 
						|
			return fmt.Errorf("write %s: %w", p, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// Stat returns usage statistics for the cgroup
 | 
						|
func (c *CgroupControl) Stat() (*cgroups.Stats, error) {
 | 
						|
	m := cgroups.Stats{}
 | 
						|
	found := false
 | 
						|
	for _, h := range handlers {
 | 
						|
		if err := h.Stat(c, &m); err != nil {
 | 
						|
			if !errors.Is(err, os.ErrNotExist) {
 | 
						|
				return nil, err
 | 
						|
			}
 | 
						|
			logrus.Warningf("Failed to retrieve cgroup stats: %v", err)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		found = true
 | 
						|
	}
 | 
						|
	if !found {
 | 
						|
		return nil, ErrStatCgroup
 | 
						|
	}
 | 
						|
	return &m, nil
 | 
						|
}
 | 
						|
 | 
						|
func readCgroup2MapPath(path string) (map[string][]string, error) {
 | 
						|
	ret := map[string][]string{}
 | 
						|
	f, err := os.Open(path)
 | 
						|
	if err != nil {
 | 
						|
		if errors.Is(err, os.ErrNotExist) {
 | 
						|
			return ret, nil
 | 
						|
		}
 | 
						|
		return nil, fmt.Errorf("open file %s: %w", path, err)
 | 
						|
	}
 | 
						|
	defer f.Close()
 | 
						|
	scanner := bufio.NewScanner(f)
 | 
						|
	for scanner.Scan() {
 | 
						|
		line := scanner.Text()
 | 
						|
		parts := strings.Fields(line)
 | 
						|
		if len(parts) < 2 {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		ret[parts[0]] = parts[1:]
 | 
						|
	}
 | 
						|
	if err := scanner.Err(); err != nil {
 | 
						|
		return nil, fmt.Errorf("parsing file %s: %w", path, err)
 | 
						|
	}
 | 
						|
	return ret, nil
 | 
						|
}
 | 
						|
 | 
						|
func readCgroup2MapFile(ctr *CgroupControl, name string) (map[string][]string, error) {
 | 
						|
	p := filepath.Join(cgroupRoot, ctr.config.Path, name)
 | 
						|
 | 
						|
	return readCgroup2MapPath(p)
 | 
						|
}
 |