diff --git a/libcontainer/cgroups/fs/memory.go b/libcontainer/cgroups/fs/memory.go index 981757b94..2007f18a2 100644 --- a/libcontainer/cgroups/fs/memory.go +++ b/libcontainer/cgroups/fs/memory.go @@ -224,7 +224,9 @@ func getMemoryData(path, name string) (cgroups.MemoryData, error) { value, err := fscommon.GetCgroupParamUint(path, usage) if err != nil { - if moduleName != "memory" && os.IsNotExist(err) { + if name != "" && os.IsNotExist(err) { + // Ignore ENOENT as swap and kmem controllers + // are optional in the kernel. return cgroups.MemoryData{}, nil } return cgroups.MemoryData{}, fmt.Errorf("failed to parse %s - %v", usage, err) @@ -232,25 +234,16 @@ func getMemoryData(path, name string) (cgroups.MemoryData, error) { memoryData.Usage = value value, err = fscommon.GetCgroupParamUint(path, maxUsage) if err != nil { - if moduleName != "memory" && os.IsNotExist(err) { - return cgroups.MemoryData{}, nil - } return cgroups.MemoryData{}, fmt.Errorf("failed to parse %s - %v", maxUsage, err) } memoryData.MaxUsage = value value, err = fscommon.GetCgroupParamUint(path, failcnt) if err != nil { - if moduleName != "memory" && os.IsNotExist(err) { - return cgroups.MemoryData{}, nil - } return cgroups.MemoryData{}, fmt.Errorf("failed to parse %s - %v", failcnt, err) } memoryData.Failcnt = value value, err = fscommon.GetCgroupParamUint(path, limit) if err != nil { - if moduleName != "memory" && os.IsNotExist(err) { - return cgroups.MemoryData{}, nil - } return cgroups.MemoryData{}, fmt.Errorf("failed to parse %s - %v", limit, err) } memoryData.Limit = value diff --git a/libcontainer/cgroups/fs2/fs2.go b/libcontainer/cgroups/fs2/fs2.go index 255c2d1b0..2db442a33 100644 --- a/libcontainer/cgroups/fs2/fs2.go +++ b/libcontainer/cgroups/fs2/fs2.go @@ -103,43 +103,27 @@ func (m *manager) GetStats() (*cgroups.Stats, error) { ) st := cgroups.NewStats() - if err := m.getControllers(); err != nil { - return st, err - } // pids (since kernel 4.5) - if _, ok := m.controllers["pids"]; ok { - if err := statPids(m.dirPath, st); err != nil { - errs = append(errs, err) - } - } else { - if err := statPidsWithoutController(m.dirPath, st); err != nil { - errs = append(errs, err) - } + if err := statPids(m.dirPath, st); err != nil { + errs = append(errs, err) } // memory (since kernel 4.5) - if _, ok := m.controllers["memory"]; ok { - if err := statMemory(m.dirPath, st); err != nil { - errs = append(errs, err) - } + if err := statMemory(m.dirPath, st); err != nil && !os.IsNotExist(err) { + errs = append(errs, err) } // io (since kernel 4.5) - if _, ok := m.controllers["io"]; ok { - if err := statIo(m.dirPath, st); err != nil { - errs = append(errs, err) - } + if err := statIo(m.dirPath, st); err != nil && !os.IsNotExist(err) { + errs = append(errs, err) } // cpu (since kernel 4.15) - if _, ok := m.controllers["cpu"]; ok { - if err := statCpu(m.dirPath, st); err != nil { - errs = append(errs, err) - } + // Note cpu.stat is available even if the controller is not enabled. + if err := statCpu(m.dirPath, st); err != nil && !os.IsNotExist(err) { + errs = append(errs, err) } // hugetlb (since kernel 5.6) - if _, ok := m.controllers["hugetlb"]; ok { - if err := statHugeTlb(m.dirPath, st); err != nil { - errs = append(errs, err) - } + if err := statHugeTlb(m.dirPath, st); err != nil && !os.IsNotExist(err) { + errs = append(errs, err) } if len(errs) > 0 && !m.rootless { return st, errors.Errorf("error while statting cgroup v2: %+v", errs) diff --git a/libcontainer/cgroups/fs2/memory.go b/libcontainer/cgroups/fs2/memory.go index cc8eaeb2e..3d1284a7c 100644 --- a/libcontainer/cgroups/fs2/memory.go +++ b/libcontainer/cgroups/fs2/memory.go @@ -4,13 +4,16 @@ package fs2 import ( "bufio" + "math" "os" "strconv" + "strings" "github.com/opencontainers/runc/libcontainer/cgroups" "github.com/opencontainers/runc/libcontainer/cgroups/fscommon" "github.com/opencontainers/runc/libcontainer/configs" "github.com/pkg/errors" + "golang.org/x/sys/unix" ) // numToStr converts an int64 value to a string for writing to a @@ -89,9 +92,17 @@ func statMemory(dirPath string, stats *cgroups.Stats) error { stats.MemoryStats.Stats[t] = v } stats.MemoryStats.Cache = stats.MemoryStats.Stats["file"] + // Unlike cgroup v1 which has memory.use_hierarchy binary knob, + // cgroup v2 is always hierarchical. + stats.MemoryStats.UseHierarchy = true memoryUsage, err := getMemoryDataV2(dirPath, "") if err != nil { + if errors.Is(err, unix.ENOENT) && dirPath == UnifiedMountpoint { + // The root cgroup does not have memory.{current,max} + // so emulate those using data from /proc/meminfo. + return statsFromMeminfo(stats) + } return err } stats.MemoryStats.Usage = memoryUsage @@ -99,9 +110,15 @@ func statMemory(dirPath string, stats *cgroups.Stats) error { if err != nil { return err } + // As cgroup v1 reports SwapUsage values as mem+swap combined, + // while in cgroup v2 swap values do not include memory, + // report combined mem+swap for v1 compatibility. + swapUsage.Usage += memoryUsage.Usage + if swapUsage.Limit != math.MaxUint64 { + swapUsage.Limit += memoryUsage.Limit + } stats.MemoryStats.SwapUsage = swapUsage - stats.MemoryStats.UseHierarchy = true return nil } @@ -117,7 +134,10 @@ func getMemoryDataV2(path, name string) (cgroups.MemoryData, error) { value, err := fscommon.GetCgroupParamUint(path, usage) if err != nil { - if moduleName != "memory" && os.IsNotExist(err) { + if name != "" && os.IsNotExist(err) { + // Ignore EEXIST as there's no swap accounting + // if kernel CONFIG_MEMCG_SWAP is not set or + // swapaccount=0 kernel boot parameter is given. return cgroups.MemoryData{}, nil } return cgroups.MemoryData{}, errors.Wrapf(err, "failed to parse %s", usage) @@ -126,12 +146,69 @@ func getMemoryDataV2(path, name string) (cgroups.MemoryData, error) { value, err = fscommon.GetCgroupParamUint(path, limit) if err != nil { - if moduleName != "memory" && os.IsNotExist(err) { - return cgroups.MemoryData{}, nil - } return cgroups.MemoryData{}, errors.Wrapf(err, "failed to parse %s", limit) } memoryData.Limit = value return memoryData, nil } + +func statsFromMeminfo(stats *cgroups.Stats) error { + f, err := os.Open("/proc/meminfo") + if err != nil { + return err + } + defer f.Close() + + // Fields we are interested in. + var ( + swap_free uint64 + swap_total uint64 + main_total uint64 + main_free uint64 + ) + mem := map[string]*uint64{ + "SwapFree": &swap_free, + "SwapTotal": &swap_total, + "MemTotal": &main_total, + "MemFree": &main_free, + } + + found := 0 + sc := bufio.NewScanner(f) + for sc.Scan() { + parts := strings.SplitN(sc.Text(), ":", 3) + if len(parts) != 2 { + // Should not happen. + continue + } + k := parts[0] + p, ok := mem[k] + if !ok { + // Unknown field -- not interested. + continue + } + vStr := strings.TrimSpace(strings.TrimSuffix(parts[1], " kB")) + *p, err = strconv.ParseUint(vStr, 10, 64) + if err != nil { + return errors.Wrap(err, "parsing /proc/meminfo "+k) + } + + found++ + if found == len(mem) { + // Got everything we need -- skip the rest. + break + } + } + if sc.Err() != nil { + return sc.Err() + } + + stats.MemoryStats.SwapUsage.Usage = (swap_total - swap_free) * 1024 + stats.MemoryStats.SwapUsage.Limit = math.MaxUint64 + + stats.MemoryStats.Usage.Usage = (main_total - main_free) * 1024 + stats.MemoryStats.Usage.Limit = math.MaxUint64 + + return nil +} diff --git a/libcontainer/cgroups/fs2/pids.go b/libcontainer/cgroups/fs2/pids.go index 16e1c2195..366212347 100644 --- a/libcontainer/cgroups/fs2/pids.go +++ b/libcontainer/cgroups/fs2/pids.go @@ -3,6 +3,7 @@ package fs2 import ( + "os" "path/filepath" "strings" @@ -30,7 +31,7 @@ func setPids(dirPath string, cgroup *configs.Cgroup) error { return nil } -func statPidsWithoutController(dirPath string, stats *cgroups.Stats) error { +func statPidsFromCgroupProcs(dirPath string, stats *cgroups.Stats) error { // if the controller is not enabled, let's read PIDS from cgroups.procs // (or threads if cgroup.threads is enabled) contents, err := fscommon.ReadFile(dirPath, "cgroup.procs") @@ -40,13 +41,8 @@ func statPidsWithoutController(dirPath string, stats *cgroups.Stats) error { if err != nil { return err } - pids := make(map[string]string) - for _, i := range strings.Split(contents, "\n") { - if i != "" { - pids[i] = i - } - } - stats.PidsStats.Current = uint64(len(pids)) + pids := strings.Count(contents, "\n") + stats.PidsStats.Current = uint64(pids) stats.PidsStats.Limit = 0 return nil } @@ -54,6 +50,9 @@ func statPidsWithoutController(dirPath string, stats *cgroups.Stats) error { func statPids(dirPath string, stats *cgroups.Stats) error { current, err := fscommon.GetCgroupParamUint(dirPath, "pids.current") if err != nil { + if os.IsNotExist(err) { + return statPidsFromCgroupProcs(dirPath, stats) + } return errors.Wrap(err, "failed to parse pids.current") }