package fs2 import ( "bufio" "errors" "math" "os" "strconv" "strings" "golang.org/x/sys/unix" "github.com/opencontainers/runc/libcontainer/cgroups" "github.com/opencontainers/runc/libcontainer/cgroups/fscommon" "github.com/opencontainers/runc/libcontainer/configs" ) // numToStr converts an int64 value to a string for writing to a // cgroupv2 files with .min, .max, .low, or .high suffix. // The value of -1 is converted to "max" for cgroupv1 compatibility // (which used to write -1 to remove the limit). func numToStr(value int64) (ret string) { switch { case value == 0: ret = "" case value == -1: ret = "max" default: ret = strconv.FormatInt(value, 10) } return ret } func isMemorySet(r *configs.Resources) bool { return r.MemoryReservation != 0 || r.Memory != 0 || r.MemorySwap != 0 } func setMemory(dirPath string, r *configs.Resources) error { if !isMemorySet(r) { return nil } swap, err := cgroups.ConvertMemorySwapToCgroupV2Value(r.MemorySwap, r.Memory) if err != nil { return err } swapStr := numToStr(swap) if swapStr == "" && swap == 0 && r.MemorySwap > 0 { // memory and memorySwap set to the same value -- disable swap swapStr = "0" } // never write empty string to `memory.swap.max`, it means set to 0. if swapStr != "" { if err := cgroups.WriteFile(dirPath, "memory.swap.max", swapStr); err != nil { return err } } if val := numToStr(r.Memory); val != "" { if err := cgroups.WriteFile(dirPath, "memory.max", val); err != nil { return err } } // cgroup.Resources.KernelMemory is ignored if val := numToStr(r.MemoryReservation); val != "" { if err := cgroups.WriteFile(dirPath, "memory.low", val); err != nil { return err } } return nil } func statMemory(dirPath string, stats *cgroups.Stats) error { const file = "memory.stat" statsFile, err := cgroups.OpenFile(dirPath, file, os.O_RDONLY) if err != nil { return err } defer statsFile.Close() sc := bufio.NewScanner(statsFile) for sc.Scan() { t, v, err := fscommon.ParseKeyValue(sc.Text()) if err != nil { return &parseError{Path: dirPath, File: file, Err: err} } stats.MemoryStats.Stats[t] = v } if err := sc.Err(); err != nil { return &parseError{Path: dirPath, File: file, Err: err} } 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,peak} // so emulate those using data from /proc/meminfo and // /sys/fs/cgroup/memory.stat return rootStatsFromMeminfo(stats) } return err } stats.MemoryStats.Usage = memoryUsage swapOnlyUsage, err := getMemoryDataV2(dirPath, "swap") if err != nil { return err } stats.MemoryStats.SwapOnlyUsage = swapOnlyUsage swapUsage := swapOnlyUsage // 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 } // The `MaxUsage` of mem+swap cannot simply combine mem with // swap. So set it to 0 for v1 compatibility. swapUsage.MaxUsage = 0 stats.MemoryStats.SwapUsage = swapUsage return nil } func getMemoryDataV2(path, name string) (cgroups.MemoryData, error) { memoryData := cgroups.MemoryData{} moduleName := "memory" if name != "" { moduleName = "memory." + name } usage := moduleName + ".current" limit := moduleName + ".max" maxUsage := moduleName + ".peak" value, err := fscommon.GetCgroupParamUint(path, usage) if err != nil { 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{}, err } memoryData.Usage = value value, err = fscommon.GetCgroupParamUint(path, limit) if err != nil { return cgroups.MemoryData{}, err } memoryData.Limit = value // `memory.peak` since kernel 5.19 // `memory.swap.peak` since kernel 6.5 value, err = fscommon.GetCgroupParamUint(path, maxUsage) if err != nil && !os.IsNotExist(err) { return cgroups.MemoryData{}, err } memoryData.MaxUsage = value return memoryData, nil } func rootStatsFromMeminfo(stats *cgroups.Stats) error { const file = "/proc/meminfo" f, err := os.Open(file) if err != nil { return err } defer f.Close() // Fields we are interested in. var ( swap_free uint64 swap_total uint64 ) mem := map[string]*uint64{ "SwapFree": &swap_free, "SwapTotal": &swap_total, } 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 &parseError{File: file, Err: errors.New("bad value for " + k)} } found++ if found == len(mem) { // Got everything we need -- skip the rest. break } } if err := sc.Err(); err != nil { return &parseError{Path: "", File: file, Err: err} } // cgroup v1 `usage_in_bytes` reports memory usage as the sum of // - rss (NR_ANON_MAPPED) // - cache (NR_FILE_PAGES) // cgroup v1 reports SwapUsage values as mem+swap combined // cgroup v2 reports rss and cache as anon and file. // sum `anon` + `file` to report the same value as `usage_in_bytes` in v1. // sum swap usage as combined mem+swap usage for consistency as well. stats.MemoryStats.Usage.Usage = stats.MemoryStats.Stats["anon"] + stats.MemoryStats.Stats["file"] stats.MemoryStats.Usage.Limit = math.MaxUint64 stats.MemoryStats.SwapUsage.Usage = (swap_total - swap_free) * 1024 stats.MemoryStats.SwapUsage.Limit = math.MaxUint64 stats.MemoryStats.SwapUsage.Usage += stats.MemoryStats.Usage.Usage return nil }