github.com/minio/madmin-go/v2@v2.2.1/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  	"io/ioutil"
    33  	"os"
    34  	"os/exec"
    35  	"path/filepath"
    36  	"strconv"
    37  	"strings"
    38  )
    39  
    40  // DO NOT EDIT following constants are chosen defaults for any kernel
    41  // after 3.x, please open a GitHub issue https://github.com/minio/madmin-go/v2/issues
    42  // and discuss first if you wish to change this.
    43  const (
    44  	// Default string for looking for kernel memory param.
    45  	memoryLimitKernelParam = "memory.limit_in_bytes"
    46  
    47  	// Points to sys path memory path.
    48  	cgroupMemSysPath = "/sys/fs/cgroup/memory"
    49  
    50  	// Default docker prefix.
    51  	dockerPrefixName = "/docker/"
    52  
    53  	// Proc controller group path.
    54  	cgroupFileTemplate = "/proc/%d/cgroup"
    55  )
    56  
    57  // CGEntries - represents all the entries in a process cgroup file
    58  // at /proc/<pid>/cgroup as key value pairs.
    59  type CGEntries map[string]string
    60  
    61  // GetEntries reads and parses all the cgroup entries for a given process.
    62  func GetEntries(pid int) (CGEntries, error) {
    63  	r, err := os.Open(fmt.Sprintf(cgroupFileTemplate, pid))
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	defer r.Close()
    68  	return parseProcCGroup(r)
    69  }
    70  
    71  // parseProcCGroup - cgroups are always in the following
    72  // format once enabled you need to know the pid of the
    73  // application you are looking for so that the the
    74  // following parsing logic only parses the file located
    75  // at /proc/<pid>/cgroup.
    76  //
    77  // CGROUP entries id, component and path are always in
    78  // the following format. “ID:COMPONENT:PATH“
    79  //
    80  // Following code block parses this information and
    81  // returns a procCGroup which is a parsed list of all
    82  // the line by line entires from /proc/<pid>/cgroup.
    83  func parseProcCGroup(r io.Reader) (CGEntries, error) {
    84  	cgEntries := CGEntries{}
    85  
    86  	// Start reading cgroup categories line by line
    87  	// and process them into procCGroup structure.
    88  	scanner := bufio.NewScanner(r)
    89  	for scanner.Scan() {
    90  		line := scanner.Text()
    91  
    92  		tokens := strings.SplitN(line, ":", 3)
    93  		if len(tokens) < 3 {
    94  			continue
    95  		}
    96  
    97  		name, path := tokens[1], tokens[2]
    98  		for _, token := range strings.Split(name, ",") {
    99  			name = strings.TrimPrefix(token, "name=")
   100  			cgEntries[name] = path
   101  		}
   102  	}
   103  
   104  	// Return upon any error while reading the cgroup categories.
   105  	if err := scanner.Err(); err != nil {
   106  		return nil, err
   107  	}
   108  
   109  	return cgEntries, nil
   110  }
   111  
   112  // Fetch value of the cgroup kernel param from the cgroup manager,
   113  // if cgroup manager is configured we should just rely on `cgm` cli
   114  // to fetch all the values for us.
   115  func getManagerKernValue(cname, path, kernParam string) (limit uint64, err error) {
   116  	cmd := exec.Command("cgm", "getvalue", cname, path, kernParam)
   117  	var out bytes.Buffer
   118  	cmd.Stdout = &out
   119  	if err = cmd.Run(); err != nil {
   120  		return 0, err
   121  	}
   122  
   123  	// Parse the cgm output.
   124  	limit, err = strconv.ParseUint(strings.TrimSpace(out.String()), 10, 64)
   125  	return limit, err
   126  }
   127  
   128  // Get cgroup memory limit file path.
   129  func getMemoryLimitFilePath(cgPath string) string {
   130  	path := cgroupMemSysPath
   131  
   132  	// Docker generates weird cgroup paths that don't
   133  	// really exist on the file system.
   134  	//
   135  	// For example on regular Linux OS :
   136  	// `/user.slice/user-1000.slice/session-1.scope`
   137  	//
   138  	// But they exist as a bind mount on Docker and
   139  	// are not accessible :  `/docker/<hash>`
   140  	//
   141  	// We we will just ignore if there is `/docker` in the
   142  	// path ignore and fall back to :
   143  	// `/sys/fs/cgroup/memory/memory.limit_in_bytes`
   144  	if !strings.HasPrefix(cgPath, dockerPrefixName) {
   145  		path = filepath.Join(path, cgPath)
   146  	}
   147  
   148  	// Final path.
   149  	return filepath.Join(path, memoryLimitKernelParam)
   150  }
   151  
   152  // GetMemoryLimit - Fetches cgroup memory limit either from
   153  // a file path at '/sys/fs/cgroup/memory', if path fails then
   154  // fallback to querying cgroup manager.
   155  func GetMemoryLimit(pid int) (limit uint64, err error) {
   156  	var cg CGEntries
   157  	cg, err = GetEntries(pid)
   158  	if err != nil {
   159  		return 0, err
   160  	}
   161  
   162  	path := cg["memory"]
   163  
   164  	limit, err = getManagerKernValue("memory", path, memoryLimitKernelParam)
   165  	if err != nil {
   166  
   167  		// Upon any failure returned from `cgm`, on some systems cgm
   168  		// might not be installed. We fallback to using the the sysfs
   169  		// path instead to lookup memory limits.
   170  		var b []byte
   171  		b, err = ioutil.ReadFile(getMemoryLimitFilePath(path))
   172  		if err != nil {
   173  			return 0, err
   174  		}
   175  
   176  		limit, err = strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64)
   177  	}
   178  
   179  	return limit, err
   180  }