1 package fs
2
3 import (
4 "bufio"
5 "errors"
6 "fmt"
7 "math"
8 "os"
9 "path/filepath"
10 "strconv"
11 "strings"
12
13 "golang.org/x/sys/unix"
14
15 "github.com/opencontainers/runc/libcontainer/cgroups"
16 "github.com/opencontainers/runc/libcontainer/cgroups/fscommon"
17 "github.com/opencontainers/runc/libcontainer/configs"
18 )
19
20 const (
21 cgroupMemorySwapLimit = "memory.memsw.limit_in_bytes"
22 cgroupMemoryLimit = "memory.limit_in_bytes"
23 cgroupMemoryUsage = "memory.usage_in_bytes"
24 cgroupMemoryMaxUsage = "memory.max_usage_in_bytes"
25 )
26
27 type MemoryGroup struct{}
28
29 func (s *MemoryGroup) Name() string {
30 return "memory"
31 }
32
33 func (s *MemoryGroup) Apply(path string, _ *configs.Resources, pid int) error {
34 return apply(path, pid)
35 }
36
37 func setMemory(path string, val int64) error {
38 if val == 0 {
39 return nil
40 }
41
42 err := cgroups.WriteFile(path, cgroupMemoryLimit, strconv.FormatInt(val, 10))
43 if !errors.Is(err, unix.EBUSY) {
44 return err
45 }
46
47
48
49 usage, err := fscommon.GetCgroupParamUint(path, cgroupMemoryUsage)
50 if err != nil {
51 return err
52 }
53 max, err := fscommon.GetCgroupParamUint(path, cgroupMemoryMaxUsage)
54 if err != nil {
55 return err
56 }
57
58 return fmt.Errorf("unable to set memory limit to %d (current usage: %d, peak usage: %d)", val, usage, max)
59 }
60
61 func setSwap(path string, val int64) error {
62 if val == 0 {
63 return nil
64 }
65
66 return cgroups.WriteFile(path, cgroupMemorySwapLimit, strconv.FormatInt(val, 10))
67 }
68
69 func setMemoryAndSwap(path string, r *configs.Resources) error {
70
71
72 if r.Memory == -1 && r.MemorySwap == 0 {
73
74 if cgroups.PathExists(filepath.Join(path, cgroupMemorySwapLimit)) {
75 r.MemorySwap = -1
76 }
77 }
78
79
80
81 if r.Memory != 0 && r.MemorySwap != 0 {
82 curLimit, err := fscommon.GetCgroupParamUint(path, cgroupMemoryLimit)
83 if err != nil {
84 return err
85 }
86
87
88
89
90 if r.MemorySwap == -1 || curLimit < uint64(r.MemorySwap) {
91 if err := setSwap(path, r.MemorySwap); err != nil {
92 return err
93 }
94 if err := setMemory(path, r.Memory); err != nil {
95 return err
96 }
97 return nil
98 }
99 }
100
101 if err := setMemory(path, r.Memory); err != nil {
102 return err
103 }
104 if err := setSwap(path, r.MemorySwap); err != nil {
105 return err
106 }
107
108 return nil
109 }
110
111 func (s *MemoryGroup) Set(path string, r *configs.Resources) error {
112 if err := setMemoryAndSwap(path, r); err != nil {
113 return err
114 }
115
116
117
118 if r.MemoryReservation != 0 {
119 if err := cgroups.WriteFile(path, "memory.soft_limit_in_bytes", strconv.FormatInt(r.MemoryReservation, 10)); err != nil {
120 return err
121 }
122 }
123
124 if r.OomKillDisable {
125 if err := cgroups.WriteFile(path, "memory.oom_control", "1"); err != nil {
126 return err
127 }
128 }
129 if r.MemorySwappiness == nil || int64(*r.MemorySwappiness) == -1 {
130 return nil
131 } else if *r.MemorySwappiness <= 100 {
132 if err := cgroups.WriteFile(path, "memory.swappiness", strconv.FormatUint(*r.MemorySwappiness, 10)); err != nil {
133 return err
134 }
135 } else {
136 return fmt.Errorf("invalid memory swappiness value: %d (valid range is 0-100)", *r.MemorySwappiness)
137 }
138
139 return nil
140 }
141
142 func (s *MemoryGroup) GetStats(path string, stats *cgroups.Stats) error {
143 const file = "memory.stat"
144 statsFile, err := cgroups.OpenFile(path, file, os.O_RDONLY)
145 if err != nil {
146 if os.IsNotExist(err) {
147 return nil
148 }
149 return err
150 }
151 defer statsFile.Close()
152
153 sc := bufio.NewScanner(statsFile)
154 for sc.Scan() {
155 t, v, err := fscommon.ParseKeyValue(sc.Text())
156 if err != nil {
157 return &parseError{Path: path, File: file, Err: err}
158 }
159 stats.MemoryStats.Stats[t] = v
160 }
161 stats.MemoryStats.Cache = stats.MemoryStats.Stats["cache"]
162
163 memoryUsage, err := getMemoryData(path, "")
164 if err != nil {
165 return err
166 }
167 stats.MemoryStats.Usage = memoryUsage
168 swapUsage, err := getMemoryData(path, "memsw")
169 if err != nil {
170 return err
171 }
172 stats.MemoryStats.SwapUsage = swapUsage
173 stats.MemoryStats.SwapOnlyUsage = cgroups.MemoryData{
174 Usage: swapUsage.Usage - memoryUsage.Usage,
175 Failcnt: swapUsage.Failcnt - memoryUsage.Failcnt,
176 }
177 kernelUsage, err := getMemoryData(path, "kmem")
178 if err != nil {
179 return err
180 }
181 stats.MemoryStats.KernelUsage = kernelUsage
182 kernelTCPUsage, err := getMemoryData(path, "kmem.tcp")
183 if err != nil {
184 return err
185 }
186 stats.MemoryStats.KernelTCPUsage = kernelTCPUsage
187
188 value, err := fscommon.GetCgroupParamUint(path, "memory.use_hierarchy")
189 if err != nil {
190 return err
191 }
192 if value == 1 {
193 stats.MemoryStats.UseHierarchy = true
194 }
195
196 pagesByNUMA, err := getPageUsageByNUMA(path)
197 if err != nil {
198 return err
199 }
200 stats.MemoryStats.PageUsageByNUMA = pagesByNUMA
201
202 return nil
203 }
204
205 func getMemoryData(path, name string) (cgroups.MemoryData, error) {
206 memoryData := cgroups.MemoryData{}
207
208 moduleName := "memory"
209 if name != "" {
210 moduleName = "memory." + name
211 }
212 var (
213 usage = moduleName + ".usage_in_bytes"
214 maxUsage = moduleName + ".max_usage_in_bytes"
215 failcnt = moduleName + ".failcnt"
216 limit = moduleName + ".limit_in_bytes"
217 )
218
219 value, err := fscommon.GetCgroupParamUint(path, usage)
220 if err != nil {
221 if name != "" && os.IsNotExist(err) {
222
223
224 return cgroups.MemoryData{}, nil
225 }
226 return cgroups.MemoryData{}, err
227 }
228 memoryData.Usage = value
229 value, err = fscommon.GetCgroupParamUint(path, maxUsage)
230 if err != nil {
231 return cgroups.MemoryData{}, err
232 }
233 memoryData.MaxUsage = value
234 value, err = fscommon.GetCgroupParamUint(path, failcnt)
235 if err != nil {
236 return cgroups.MemoryData{}, err
237 }
238 memoryData.Failcnt = value
239 value, err = fscommon.GetCgroupParamUint(path, limit)
240 if err != nil {
241 if name == "kmem" && os.IsNotExist(err) {
242
243
244 return memoryData, nil
245 }
246
247 return cgroups.MemoryData{}, err
248 }
249 memoryData.Limit = value
250
251 return memoryData, nil
252 }
253
254 func getPageUsageByNUMA(path string) (cgroups.PageUsageByNUMA, error) {
255 const (
256 maxColumns = math.MaxUint8 + 1
257 file = "memory.numa_stat"
258 )
259 stats := cgroups.PageUsageByNUMA{}
260
261 fd, err := cgroups.OpenFile(path, file, os.O_RDONLY)
262 if os.IsNotExist(err) {
263 return stats, nil
264 } else if err != nil {
265 return stats, err
266 }
267 defer fd.Close()
268
269
270
271
272
273
274
275
276
277
278 scanner := bufio.NewScanner(fd)
279 for scanner.Scan() {
280 var field *cgroups.PageStats
281
282 line := scanner.Text()
283 columns := strings.SplitN(line, " ", maxColumns)
284 for i, column := range columns {
285 byNode := strings.SplitN(column, "=", 2)
286
287
288
289 if len(byNode) < 2 {
290 if i == 0 {
291
292 break
293 } else {
294
295
296 return stats, malformedLine(path, file, line)
297 }
298 }
299 key, val := byNode[0], byNode[1]
300 if i == 0 {
301 field = getNUMAField(&stats, key)
302 if field == nil {
303 break
304 }
305 field.Total, err = strconv.ParseUint(val, 0, 64)
306 if err != nil {
307 return stats, &parseError{Path: path, File: file, Err: err}
308 }
309 field.Nodes = map[uint8]uint64{}
310 } else {
311 if len(key) < 2 || key[0] != 'N' {
312
313 return stats, malformedLine(path, file, line)
314 }
315
316 n, err := strconv.ParseUint(key[1:], 10, 8)
317 if err != nil {
318 return stats, &parseError{Path: path, File: file, Err: err}
319 }
320
321 usage, err := strconv.ParseUint(val, 10, 64)
322 if err != nil {
323 return stats, &parseError{Path: path, File: file, Err: err}
324 }
325
326 field.Nodes[uint8(n)] = usage
327 }
328
329 }
330 }
331 if err := scanner.Err(); err != nil {
332 return cgroups.PageUsageByNUMA{}, &parseError{Path: path, File: file, Err: err}
333 }
334
335 return stats, nil
336 }
337
338 func getNUMAField(stats *cgroups.PageUsageByNUMA, name string) *cgroups.PageStats {
339 switch name {
340 case "total":
341 return &stats.Total
342 case "file":
343 return &stats.File
344 case "anon":
345 return &stats.Anon
346 case "unevictable":
347 return &stats.Unevictable
348 case "hierarchical_total":
349 return &stats.Hierarchical.Total
350 case "hierarchical_file":
351 return &stats.Hierarchical.File
352 case "hierarchical_anon":
353 return &stats.Hierarchical.Anon
354 case "hierarchical_unevictable":
355 return &stats.Hierarchical.Unevictable
356 }
357 return nil
358 }
359
View as plain text