github.com/opencontainers/runc@v1.2.0-rc.1.0.20240520010911-492dc558cdd6/libcontainer/cgroups/fs2/memory.go (about) 1 package fs2 2 3 import ( 4 "bufio" 5 "errors" 6 "math" 7 "os" 8 "strconv" 9 "strings" 10 11 "golang.org/x/sys/unix" 12 13 "github.com/opencontainers/runc/libcontainer/cgroups" 14 "github.com/opencontainers/runc/libcontainer/cgroups/fscommon" 15 "github.com/opencontainers/runc/libcontainer/configs" 16 ) 17 18 // numToStr converts an int64 value to a string for writing to a 19 // cgroupv2 files with .min, .max, .low, or .high suffix. 20 // The value of -1 is converted to "max" for cgroupv1 compatibility 21 // (which used to write -1 to remove the limit). 22 func numToStr(value int64) (ret string) { 23 switch { 24 case value == 0: 25 ret = "" 26 case value == -1: 27 ret = "max" 28 default: 29 ret = strconv.FormatInt(value, 10) 30 } 31 32 return ret 33 } 34 35 func isMemorySet(r *configs.Resources) bool { 36 return r.MemoryReservation != 0 || r.Memory != 0 || r.MemorySwap != 0 37 } 38 39 func setMemory(dirPath string, r *configs.Resources) error { 40 if !isMemorySet(r) { 41 return nil 42 } 43 44 if err := CheckMemoryUsage(dirPath, r); err != nil { 45 return err 46 } 47 48 swap, err := cgroups.ConvertMemorySwapToCgroupV2Value(r.MemorySwap, r.Memory) 49 if err != nil { 50 return err 51 } 52 swapStr := numToStr(swap) 53 if swapStr == "" && swap == 0 && r.MemorySwap > 0 { 54 // memory and memorySwap set to the same value -- disable swap 55 swapStr = "0" 56 } 57 // never write empty string to `memory.swap.max`, it means set to 0. 58 if swapStr != "" { 59 if err := cgroups.WriteFile(dirPath, "memory.swap.max", swapStr); err != nil { 60 return err 61 } 62 } 63 64 if val := numToStr(r.Memory); val != "" { 65 if err := cgroups.WriteFile(dirPath, "memory.max", val); err != nil { 66 return err 67 } 68 } 69 70 // cgroup.Resources.KernelMemory is ignored 71 72 if val := numToStr(r.MemoryReservation); val != "" { 73 if err := cgroups.WriteFile(dirPath, "memory.low", val); err != nil { 74 return err 75 } 76 } 77 78 return nil 79 } 80 81 func statMemory(dirPath string, stats *cgroups.Stats) error { 82 const file = "memory.stat" 83 statsFile, err := cgroups.OpenFile(dirPath, file, os.O_RDONLY) 84 if err != nil { 85 return err 86 } 87 defer statsFile.Close() 88 89 sc := bufio.NewScanner(statsFile) 90 for sc.Scan() { 91 t, v, err := fscommon.ParseKeyValue(sc.Text()) 92 if err != nil { 93 return &parseError{Path: dirPath, File: file, Err: err} 94 } 95 stats.MemoryStats.Stats[t] = v 96 } 97 if err := sc.Err(); err != nil { 98 return &parseError{Path: dirPath, File: file, Err: err} 99 } 100 stats.MemoryStats.Cache = stats.MemoryStats.Stats["file"] 101 // Unlike cgroup v1 which has memory.use_hierarchy binary knob, 102 // cgroup v2 is always hierarchical. 103 stats.MemoryStats.UseHierarchy = true 104 105 memoryUsage, err := getMemoryDataV2(dirPath, "") 106 if err != nil { 107 if errors.Is(err, unix.ENOENT) && dirPath == UnifiedMountpoint { 108 // The root cgroup does not have memory.{current,max,peak} 109 // so emulate those using data from /proc/meminfo and 110 // /sys/fs/cgroup/memory.stat 111 return rootStatsFromMeminfo(stats) 112 } 113 return err 114 } 115 stats.MemoryStats.Usage = memoryUsage 116 swapOnlyUsage, err := getMemoryDataV2(dirPath, "swap") 117 if err != nil { 118 return err 119 } 120 stats.MemoryStats.SwapOnlyUsage = swapOnlyUsage 121 swapUsage := swapOnlyUsage 122 // As cgroup v1 reports SwapUsage values as mem+swap combined, 123 // while in cgroup v2 swap values do not include memory, 124 // report combined mem+swap for v1 compatibility. 125 swapUsage.Usage += memoryUsage.Usage 126 if swapUsage.Limit != math.MaxUint64 { 127 swapUsage.Limit += memoryUsage.Limit 128 } 129 // The `MaxUsage` of mem+swap cannot simply combine mem with 130 // swap. So set it to 0 for v1 compatibility. 131 swapUsage.MaxUsage = 0 132 stats.MemoryStats.SwapUsage = swapUsage 133 134 return nil 135 } 136 137 func getMemoryDataV2(path, name string) (cgroups.MemoryData, error) { 138 memoryData := cgroups.MemoryData{} 139 140 moduleName := "memory" 141 if name != "" { 142 moduleName = "memory." + name 143 } 144 usage := moduleName + ".current" 145 limit := moduleName + ".max" 146 maxUsage := moduleName + ".peak" 147 148 value, err := fscommon.GetCgroupParamUint(path, usage) 149 if err != nil { 150 if name != "" && os.IsNotExist(err) { 151 // Ignore EEXIST as there's no swap accounting 152 // if kernel CONFIG_MEMCG_SWAP is not set or 153 // swapaccount=0 kernel boot parameter is given. 154 return cgroups.MemoryData{}, nil 155 } 156 return cgroups.MemoryData{}, err 157 } 158 memoryData.Usage = value 159 160 value, err = fscommon.GetCgroupParamUint(path, limit) 161 if err != nil { 162 return cgroups.MemoryData{}, err 163 } 164 memoryData.Limit = value 165 166 // `memory.peak` since kernel 5.19 167 // `memory.swap.peak` since kernel 6.5 168 value, err = fscommon.GetCgroupParamUint(path, maxUsage) 169 if err != nil && !os.IsNotExist(err) { 170 return cgroups.MemoryData{}, err 171 } 172 memoryData.MaxUsage = value 173 174 return memoryData, nil 175 } 176 177 func rootStatsFromMeminfo(stats *cgroups.Stats) error { 178 const file = "/proc/meminfo" 179 f, err := os.Open(file) 180 if err != nil { 181 return err 182 } 183 defer f.Close() 184 185 // Fields we are interested in. 186 var ( 187 swap_free uint64 188 swap_total uint64 189 ) 190 mem := map[string]*uint64{ 191 "SwapFree": &swap_free, 192 "SwapTotal": &swap_total, 193 } 194 195 found := 0 196 sc := bufio.NewScanner(f) 197 for sc.Scan() { 198 parts := strings.SplitN(sc.Text(), ":", 3) 199 if len(parts) != 2 { 200 // Should not happen. 201 continue 202 } 203 k := parts[0] 204 p, ok := mem[k] 205 if !ok { 206 // Unknown field -- not interested. 207 continue 208 } 209 vStr := strings.TrimSpace(strings.TrimSuffix(parts[1], " kB")) 210 *p, err = strconv.ParseUint(vStr, 10, 64) 211 if err != nil { 212 return &parseError{File: file, Err: errors.New("bad value for " + k)} 213 } 214 215 found++ 216 if found == len(mem) { 217 // Got everything we need -- skip the rest. 218 break 219 } 220 } 221 if err := sc.Err(); err != nil { 222 return &parseError{Path: "", File: file, Err: err} 223 } 224 225 // cgroup v1 `usage_in_bytes` reports memory usage as the sum of 226 // - rss (NR_ANON_MAPPED) 227 // - cache (NR_FILE_PAGES) 228 // cgroup v1 reports SwapUsage values as mem+swap combined 229 // cgroup v2 reports rss and cache as anon and file. 230 // sum `anon` + `file` to report the same value as `usage_in_bytes` in v1. 231 // sum swap usage as combined mem+swap usage for consistency as well. 232 stats.MemoryStats.Usage.Usage = stats.MemoryStats.Stats["anon"] + stats.MemoryStats.Stats["file"] 233 stats.MemoryStats.Usage.Limit = math.MaxUint64 234 stats.MemoryStats.SwapUsage.Usage = (swap_total - swap_free) * 1024 235 stats.MemoryStats.SwapUsage.Limit = math.MaxUint64 236 stats.MemoryStats.SwapUsage.Usage += stats.MemoryStats.Usage.Usage 237 238 return nil 239 }