mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 14:13:58 +08:00
825335b2b7
This builds on top of recently introduced fscommon.ParseError. Errors returned from parsers (mostly ones used by GetStats()) are all different, and many are incomplete. For example, in many cases errors from strconv.ParseUint are returned as is, meaning there is no context telling which file we were reading. Similarly, errors from fscommon.ParseKeyValue should be wrapped to add more context. Same is true for scanner.Err(). OTOH, errors from fscommon.GetCgroup* do have enough context and there is no need to wrap them. Fix all the above. While at it, add missing scanner.Err() checks. [v2: use parseError, not ParseError] Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
218 lines
5.3 KiB
Go
218 lines
5.3 KiB
Go
// +build linux
|
|
|
|
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
|
|
// 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}
|
|
// so emulate those using data from /proc/meminfo.
|
|
return statsFromMeminfo(stats)
|
|
}
|
|
return err
|
|
}
|
|
stats.MemoryStats.Usage = memoryUsage
|
|
swapUsage, err := getMemoryDataV2(dirPath, "swap")
|
|
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
|
|
|
|
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"
|
|
|
|
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
|
|
|
|
return memoryData, nil
|
|
}
|
|
|
|
func statsFromMeminfo(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
|
|
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 &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}
|
|
}
|
|
|
|
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
|
|
}
|