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  }