github.com/minio/madmin-go/v3@v3.0.51/cgroup/linux.go (about)

     1  //go:build linux
     2  // +build linux
     3  
     4  //
     5  // Copyright (c) 2015-2022 MinIO, Inc.
     6  //
     7  // This file is part of MinIO Object Storage stack
     8  //
     9  // This program is free software: you can redistribute it and/or modify
    10  // it under the terms of the GNU Affero General Public License as
    11  // published by the Free Software Foundation, either version 3 of the
    12  // License, or (at your option) any later version.
    13  //
    14  // This program is distributed in the hope that it will be useful,
    15  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    16  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    17  // GNU Affero General Public License for more details.
    18  //
    19  // You should have received a copy of the GNU Affero General Public License
    20  // along with this program. If not, see <http://www.gnu.org/licenses/>.
    21  //
    22  
    23  // Package cgroup implements parsing for all the cgroup
    24  // categories and functionality in a simple way.
    25  package cgroup
    26  
    27  import (
    28  	"bufio"
    29  	"bytes"
    30  	"fmt"
    31  	"io"
    32  	"os"
    33  	"os/exec"
    34  	"path/filepath"
    35  	"strconv"
    36  	"strings"
    37  )
    38  
    39  // DO NOT EDIT following constants are chosen defaults for any kernel
    40  // after 3.x, please open a GitHub issue https://github.com/minio/madmin-go/v2/issues
    41  // and discuss first if you wish to change this.
    42  const (
    43  	// Default string for looking for kernel memory param.
    44  	memoryLimitKernelParam = "memory.limit_in_bytes"
    45  
    46  	// Points to sys path memory path.
    47  	cgroupMemSysPath = "/sys/fs/cgroup/memory"
    48  
    49  	// Default docker prefix.
    50  	dockerPrefixName = "/docker/"
    51  
    52  	// Proc controller group path.
    53  	cgroupFileTemplate = "/proc/%d/cgroup"
    54  )
    55  
    56  // CGEntries - represents all the entries in a process cgroup file
    57  // at /proc/<pid>/cgroup as key value pairs.
    58  type CGEntries map[string]string
    59  
    60  // GetEntries reads and parses all the cgroup entries for a given process.
    61  func GetEntries(pid int) (CGEntries, error) {
    62  	r, err := os.Open(fmt.Sprintf(cgroupFileTemplate, pid))
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  	defer r.Close()
    67  	return parseProcCGroup(r)
    68  }
    69  
    70  // parseProcCGroup - cgroups are always in the following
    71  // format once enabled you need to know the pid of the
    72  // application you are looking for so that the the
    73  // following parsing logic only parses the file located
    74  // at /proc/<pid>/cgroup.
    75  //
    76  // CGROUP entries id, component and path are always in
    77  // the following format. “ID:COMPONENT:PATH“
    78  //
    79  // Following code block parses this information and
    80  // returns a procCGroup which is a parsed list of all
    81  // the line by line entires from /proc/<pid>/cgroup.
    82  func parseProcCGroup(r io.Reader) (CGEntries, error) {
    83  	cgEntries := CGEntries{}
    84  
    85  	// Start reading cgroup categories line by line
    86  	// and process them into procCGroup structure.
    87  	scanner := bufio.NewScanner(r)
    88  	for scanner.Scan() {
    89  		line := scanner.Text()
    90  
    91  		tokens := strings.SplitN(line, ":", 3)
    92  		if len(tokens) < 3 {
    93  			continue
    94  		}
    95  
    96  		name, path := tokens[1], tokens[2]
    97  		for _, token := range strings.Split(name, ",") {
    98  			name = strings.TrimPrefix(token, "name=")
    99  			cgEntries[name] = path
   100  		}
   101  	}
   102  
   103  	// Return upon any error while reading the cgroup categories.
   104  	if err := scanner.Err(); err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	return cgEntries, nil
   109  }
   110  
   111  // Fetch value of the cgroup kernel param from the cgroup manager,
   112  // if cgroup manager is configured we should just rely on `cgm` cli
   113  // to fetch all the values for us.
   114  func getManagerKernValue(cname, path, kernParam string) (limit uint64, err error) {
   115  	cmd := exec.Command("cgm", "getvalue", cname, path, kernParam)
   116  	var out bytes.Buffer
   117  	cmd.Stdout = &out
   118  	if err = cmd.Run(); err != nil {
   119  		return 0, err
   120  	}
   121  
   122  	// Parse the cgm output.
   123  	limit, err = strconv.ParseUint(strings.TrimSpace(out.String()), 10, 64)
   124  	return limit, err
   125  }
   126  
   127  // Get cgroup memory limit file path.
   128  func getMemoryLimitFilePath(cgPath string) string {
   129  	path := cgroupMemSysPath
   130  
   131  	// Docker generates weird cgroup paths that don't
   132  	// really exist on the file system.
   133  	//
   134  	// For example on regular Linux OS :
   135  	// `/user.slice/user-1000.slice/session-1.scope`
   136  	//
   137  	// But they exist as a bind mount on Docker and
   138  	// are not accessible :  `/docker/<hash>`
   139  	//
   140  	// We we will just ignore if there is `/docker` in the
   141  	// path ignore and fall back to :
   142  	// `/sys/fs/cgroup/memory/memory.limit_in_bytes`
   143  	if !strings.HasPrefix(cgPath, dockerPrefixName) {
   144  		path = filepath.Join(path, cgPath)
   145  	}
   146  
   147  	// Final path.
   148  	return filepath.Join(path, memoryLimitKernelParam)
   149  }
   150  
   151  // GetMemoryLimit - Fetches cgroup memory limit either from
   152  // a file path at '/sys/fs/cgroup/memory', if path fails then
   153  // fallback to querying cgroup manager.
   154  func GetMemoryLimit(pid int) (limit uint64, err error) {
   155  	var cg CGEntries
   156  	cg, err = GetEntries(pid)
   157  	if err != nil {
   158  		return 0, err
   159  	}
   160  
   161  	path := cg["memory"]
   162  
   163  	limit, err = getManagerKernValue("memory", path, memoryLimitKernelParam)
   164  	if err != nil {
   165  
   166  		// Upon any failure returned from `cgm`, on some systems cgm
   167  		// might not be installed. We fallback to using the the sysfs
   168  		// path instead to lookup memory limits.
   169  		var b []byte
   170  		b, err = os.ReadFile(getMemoryLimitFilePath(path))
   171  		if err != nil {
   172  			return 0, err
   173  		}
   174  
   175  		limit, err = strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64)
   176  	}
   177  
   178  	return limit, err
   179  }