github.com/jaypipes/ghw@v0.21.1/pkg/memory/memory_linux.go (about)

     1  // Use and distribution licensed under the Apache license version 2.
     2  //
     3  // See the COPYING file in the root project directory for full text.
     4  //
     5  
     6  package memory
     7  
     8  import (
     9  	"bufio"
    10  	"compress/gzip"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"path"
    15  	"path/filepath"
    16  	"regexp"
    17  	"strconv"
    18  	"strings"
    19  
    20  	"github.com/jaypipes/ghw/pkg/context"
    21  	"github.com/jaypipes/ghw/pkg/linuxpath"
    22  	"github.com/jaypipes/ghw/pkg/unitutil"
    23  	"github.com/jaypipes/ghw/pkg/util"
    24  )
    25  
    26  const (
    27  	warnCannotDeterminePhysicalMemory = `
    28  Could not determine total physical bytes of memory. This may
    29  be due to the host being a virtual machine or container with no
    30  /var/log/syslog file or /sys/devices/system/memory directory, or
    31  the current user may not have necessary privileges to read the syslog.
    32  We are falling back to setting the total physical amount of memory to
    33  the total usable amount of memory
    34  `
    35  )
    36  
    37  var (
    38  	// System log lines will look similar to the following:
    39  	// ... kernel: [0.000000] Memory: 24633272K/25155024K ...
    40  	regexSyslogMemline = regexp.MustCompile(`Memory:\s+\d+K\/(\d+)K`)
    41  	// regexMemoryBlockDirname matches a subdirectory in either
    42  	// /sys/devices/system/memory or /sys/devices/system/node/nodeX that
    43  	// represents information on a specific memory cell/block
    44  	regexMemoryBlockDirname = regexp.MustCompile(`memory\d+$`)
    45  )
    46  
    47  func (i *Info) load() error {
    48  	paths := linuxpath.New(i.ctx)
    49  	tub := memTotalUsableBytes(paths)
    50  	if tub < 1 {
    51  		return fmt.Errorf("Could not determine total usable bytes of memory")
    52  	}
    53  	i.TotalUsableBytes = tub
    54  	tpb := memTotalPhysicalBytes(paths)
    55  	i.TotalPhysicalBytes = tpb
    56  	if tpb < 1 {
    57  		i.ctx.Warn(warnCannotDeterminePhysicalMemory)
    58  		i.TotalPhysicalBytes = tub
    59  	}
    60  	i.SupportedPageSizes, _ = memorySupportedPageSizes(paths.SysKernelMMHugepages)
    61  	i.DefaultHugePageSize, _ = memoryDefaultHPSizeFromPath(paths.ProcMeminfo)
    62  	i.TotalHugePageBytes, _ = memoryHugeTLBFromPath(paths.ProcMeminfo)
    63  	hugePageAmounts := make(map[uint64]*HugePageAmounts)
    64  	for _, p := range i.SupportedPageSizes {
    65  		info, err := memoryHPInfo(paths.SysKernelMMHugepages, p)
    66  		if err != nil {
    67  			return err
    68  		}
    69  		hugePageAmounts[p] = info
    70  	}
    71  	i.HugePageAmountsBySize = hugePageAmounts
    72  	return nil
    73  }
    74  
    75  func AreaForNode(ctx *context.Context, nodeID int) (*Area, error) {
    76  	paths := linuxpath.New(ctx)
    77  	path := filepath.Join(
    78  		paths.SysDevicesSystemNode,
    79  		fmt.Sprintf("node%d", nodeID),
    80  	)
    81  
    82  	var err error
    83  	var blockSizeBytes uint64
    84  	var totPhys int64
    85  	var totUsable int64
    86  
    87  	totUsable, err = memoryTotalUsableBytesFromPath(filepath.Join(path, "meminfo"))
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  
    92  	blockSizeBytes, err = memoryBlockSizeBytes(paths.SysDevicesSystemMemory)
    93  	if err == nil {
    94  		totPhys, err = memoryTotalPhysicalBytesFromPath(path, blockSizeBytes)
    95  		if err != nil {
    96  			return nil, err
    97  		}
    98  	} else {
    99  		// NOTE(jaypipes): Some platforms (e.g. ARM) will not have a
   100  		// /sys/device/system/memory/block_size_bytes file. If this is the
   101  		// case, we set physical bytes equal to either the physical memory
   102  		// determined from syslog or the usable bytes
   103  		//
   104  		// see: https://bugzilla.redhat.com/show_bug.cgi?id=1794160
   105  		// see: https://github.com/jaypipes/ghw/issues/336
   106  		totPhys = memTotalPhysicalBytesFromSyslog(paths)
   107  	}
   108  
   109  	supportedHP, err := memorySupportedPageSizes(filepath.Join(path, "hugepages"))
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	defHPSize, err := memoryDefaultHPSizeFromPath(paths.ProcMeminfo)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	totHPSize, err := memoryHugeTLBFromPath(paths.ProcMeminfo)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	hugePageAmounts := make(map[uint64]*HugePageAmounts)
   125  	for _, p := range supportedHP {
   126  		info, err := memoryHPInfo(filepath.Join(path, "hugepages"), p)
   127  		if err != nil {
   128  			return nil, err
   129  		}
   130  		hugePageAmounts[p] = info
   131  	}
   132  
   133  	return &Area{
   134  		TotalPhysicalBytes:    totPhys,
   135  		TotalUsableBytes:      totUsable,
   136  		SupportedPageSizes:    supportedHP,
   137  		DefaultHugePageSize:   defHPSize,
   138  		TotalHugePageBytes:    totHPSize,
   139  		HugePageAmountsBySize: hugePageAmounts,
   140  	}, nil
   141  }
   142  
   143  func memoryBlockSizeBytes(dir string) (uint64, error) {
   144  	// get the memory block size in byte in hexadecimal notation
   145  	blockSize := filepath.Join(dir, "block_size_bytes")
   146  
   147  	d, err := os.ReadFile(blockSize)
   148  	if err != nil {
   149  		return 0, err
   150  	}
   151  	return strconv.ParseUint(strings.TrimSpace(string(d)), 16, 64)
   152  }
   153  
   154  func memTotalPhysicalBytes(paths *linuxpath.Paths) (total int64) {
   155  	defer func() {
   156  		// fallback to the syslog file approach in case of error
   157  		if total < 0 {
   158  			total = memTotalPhysicalBytesFromSyslog(paths)
   159  		}
   160  	}()
   161  
   162  	// detect physical memory from /sys/devices/system/memory
   163  	dir := paths.SysDevicesSystemMemory
   164  	blockSizeBytes, err := memoryBlockSizeBytes(dir)
   165  	if err != nil {
   166  		total = -1
   167  		return total
   168  	}
   169  
   170  	total, err = memoryTotalPhysicalBytesFromPath(dir, blockSizeBytes)
   171  	if err != nil {
   172  		total = -1
   173  	}
   174  	return total
   175  }
   176  
   177  // memoryTotalPhysicalBytesFromPath accepts a directory -- either
   178  // /sys/devices/system/memory (for the entire system) or
   179  // /sys/devices/system/node/nodeX (for a specific NUMA node) -- and a block
   180  // size in bytes and iterates over the sysfs memory block subdirectories,
   181  // accumulating blocks that are "online" to determine a total physical memory
   182  // size in bytes
   183  func memoryTotalPhysicalBytesFromPath(dir string, blockSizeBytes uint64) (int64, error) {
   184  	var total int64
   185  	files, err := os.ReadDir(dir)
   186  	if err != nil {
   187  		return -1, err
   188  	}
   189  	// There are many subdirectories of /sys/devices/system/memory or
   190  	// /sys/devices/system/node/nodeX that are named memory{cell} where {cell}
   191  	// is a 0-based index of the memory block. These subdirectories contain a
   192  	// state file (e.g. /sys/devices/system/memory/memory64/state that will
   193  	// contain the string "online" if that block is active.
   194  	for _, file := range files {
   195  		fname := file.Name()
   196  		// NOTE(jaypipes): we cannot rely on file.IsDir() here because the
   197  		// memory{cell} sysfs directories are not actual directories.
   198  		if !regexMemoryBlockDirname.MatchString(fname) {
   199  			continue
   200  		}
   201  		s, err := os.ReadFile(filepath.Join(dir, fname, "state"))
   202  		if err != nil {
   203  			return -1, err
   204  		}
   205  		// if the memory block state is 'online' we increment the total with
   206  		// the memory block size to determine the amount of physical
   207  		// memory available on this system.
   208  		if strings.TrimSpace(string(s)) != "online" {
   209  			continue
   210  		}
   211  		total += int64(blockSizeBytes)
   212  	}
   213  	return total, nil
   214  }
   215  
   216  func memTotalPhysicalBytesFromSyslog(paths *linuxpath.Paths) int64 {
   217  	// In Linux, the total physical memory can be determined by looking at the
   218  	// output of dmidecode, however dmidecode requires root privileges to run,
   219  	// so instead we examine the system logs for startup information containing
   220  	// total physical memory and cache the results of this.
   221  	findPhysicalKb := func(line string) int64 {
   222  		matches := regexSyslogMemline.FindStringSubmatch(line)
   223  		if len(matches) == 2 {
   224  			i, err := strconv.Atoi(matches[1])
   225  			if err != nil {
   226  				return -1
   227  			}
   228  			return int64(i * 1024)
   229  		}
   230  		return -1
   231  	}
   232  
   233  	// /var/log will contain a file called syslog and 0 or more files called
   234  	// syslog.$NUMBER or syslog.$NUMBER.gz containing system log records. We
   235  	// search each, stopping when we match a system log record line that
   236  	// contains physical memory information.
   237  	logDir := paths.VarLog
   238  	logFiles, err := os.ReadDir(logDir)
   239  	if err != nil {
   240  		return -1
   241  	}
   242  	for _, file := range logFiles {
   243  		if strings.HasPrefix(file.Name(), "syslog") {
   244  			fullPath := filepath.Join(logDir, file.Name())
   245  			unzip := filepath.Ext(file.Name()) == ".gz"
   246  			var r io.ReadCloser
   247  			r, err = os.Open(fullPath)
   248  			if err != nil {
   249  				return -1
   250  			}
   251  			defer util.SafeClose(r)
   252  			if unzip {
   253  				r, err = gzip.NewReader(r)
   254  				if err != nil {
   255  					return -1
   256  				}
   257  			}
   258  
   259  			scanner := bufio.NewScanner(r)
   260  			for scanner.Scan() {
   261  				line := scanner.Text()
   262  				size := findPhysicalKb(line)
   263  				if size > 0 {
   264  					return size
   265  				}
   266  			}
   267  		}
   268  	}
   269  	return -1
   270  }
   271  
   272  func memTotalUsableBytes(paths *linuxpath.Paths) int64 {
   273  	amount, err := memoryTotalUsableBytesFromPath(paths.ProcMeminfo)
   274  	if err != nil {
   275  		return -1
   276  	}
   277  	return amount
   278  }
   279  
   280  func memorySupportedPageSizes(hpDir string) ([]uint64, error) {
   281  	// In Linux, /sys/kernel/mm/hugepages contains a directory per page size
   282  	// supported by the kernel. The directory name corresponds to the pattern
   283  	// 'hugepages-{pagesize}kb'
   284  	out := make([]uint64, 0)
   285  
   286  	files, err := os.ReadDir(hpDir)
   287  	if err != nil {
   288  		return out, err
   289  	}
   290  	for _, file := range files {
   291  		parts := strings.Split(file.Name(), "-")
   292  		sizeStr := parts[1]
   293  		// Cut off the 'kb'
   294  		sizeStr = sizeStr[0 : len(sizeStr)-2]
   295  		size, err := strconv.Atoi(sizeStr)
   296  		if err != nil {
   297  			return out, err
   298  		}
   299  		out = append(out, uint64(size*int(unitutil.KB)))
   300  	}
   301  	return out, nil
   302  }
   303  
   304  func memoryHPInfo(hpDir string, sizeBytes uint64) (*HugePageAmounts, error) {
   305  	// In linux huge page info can be obtained in several places
   306  	// /sys/kernel/mm/hugepages/hugepages-{pagesize}kb/ directory, which contains
   307  	//		nr_hugepages
   308  	//		nr_hugepages_mempolicy
   309  	//		nr_overcommit_hugepages
   310  	//		free_hugepages
   311  	//		resv_hugepages
   312  	//		surplus_hugepages
   313  	// or NUMA specific data /sys/devices/system/node/node[0-9]*/hugepages/hugepages-{pagesize}kb/, which contains
   314  	//		nr_hugepages
   315  	//		free_hugepages
   316  	//		surplus_hugepages
   317  	targetPath := filepath.Join(hpDir, fmt.Sprintf("hugepages-%vkB", sizeBytes/uint64(unitutil.KB)))
   318  	files, err := os.ReadDir(targetPath)
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  
   323  	var (
   324  		total    int64
   325  		free     int64
   326  		surplus  int64
   327  		reserved int64
   328  	)
   329  
   330  	for _, f := range files {
   331  		switch f.Name() {
   332  		case "nr_hugepages":
   333  			count, err := readFileToInt64(path.Join(targetPath, f.Name()))
   334  			if err != nil {
   335  				return nil, err
   336  			}
   337  			total = count
   338  		case "free_hugepages":
   339  			count, err := readFileToInt64(path.Join(targetPath, f.Name()))
   340  			if err != nil {
   341  				return nil, err
   342  			}
   343  			free = count
   344  		case "surplus_hugepages":
   345  			count, err := readFileToInt64(path.Join(targetPath, f.Name()))
   346  			if err != nil {
   347  				return nil, err
   348  			}
   349  			surplus = count
   350  		case "resv_hugepages":
   351  			count, err := readFileToInt64(path.Join(targetPath, f.Name()))
   352  			if err != nil {
   353  				return nil, err
   354  			}
   355  			reserved = count
   356  		}
   357  	}
   358  
   359  	return &HugePageAmounts{
   360  		Total:    total,
   361  		Free:     free,
   362  		Surplus:  surplus,
   363  		Reserved: reserved,
   364  	}, nil
   365  }
   366  
   367  func memoryTotalUsableBytesFromPath(meminfoPath string) (int64, error) {
   368  	const key = "MemTotal"
   369  	return getMemInfoField(meminfoPath, key)
   370  }
   371  
   372  func memoryDefaultHPSizeFromPath(meminfoPath string) (uint64, error) {
   373  	const key = "Hugepagesize"
   374  	got, err := getMemInfoField(meminfoPath, key)
   375  	if err != nil {
   376  		return 0, err
   377  	}
   378  	return uint64(got), nil
   379  }
   380  
   381  func memoryHugeTLBFromPath(meminfoPath string) (int64, error) {
   382  	const key = "Hugetlb"
   383  	return getMemInfoField(meminfoPath, key)
   384  }
   385  
   386  func getMemInfoField(meminfoPath string, wantKey string) (int64, error) {
   387  	// In Linux, /proc/meminfo or its close relative
   388  	// /sys/devices/system/node/node*/meminfo
   389  	// contains a set of memory-related amounts, with
   390  	// lines looking like the following:
   391  	//
   392  	// $ cat /proc/meminfo
   393  	// MemTotal:       24677596 kB
   394  	// MemFree:        21244356 kB
   395  	// MemAvailable:   22085432 kB
   396  	// ...
   397  	// HugePages_Total:       0
   398  	// HugePages_Free:        0
   399  	// HugePages_Rsvd:        0
   400  	// HugePages_Surp:        0
   401  	// ...
   402  	//
   403  	// It's worth noting that /proc/meminfo returns exact information, not
   404  	// "theoretical" information. For instance, on the above system, I have
   405  	// 24GB of RAM but MemTotal is indicating only around 23GB. This is because
   406  	// MemTotal contains the exact amount of *usable* memory after accounting
   407  	// for the kernel's resident memory size and a few reserved bits.
   408  	// Please note GHW cares about the subset of lines shared between system-wide
   409  	// and per-NUMA-node meminfos. For more information, see:
   410  	//
   411  	//  https://www.kernel.org/doc/Documentation/filesystems/proc.txt
   412  	r, err := os.Open(meminfoPath)
   413  	if err != nil {
   414  		return -1, err
   415  	}
   416  	defer util.SafeClose(r)
   417  
   418  	scanner := bufio.NewScanner(r)
   419  	for scanner.Scan() {
   420  		line := scanner.Text()
   421  		parts := strings.Split(line, ":")
   422  		key := parts[0]
   423  		if !strings.Contains(key, wantKey) {
   424  			continue
   425  		}
   426  		rawValue := parts[1]
   427  		inKb := strings.HasSuffix(rawValue, "kB")
   428  		value, err := strconv.Atoi(strings.TrimSpace(strings.TrimSuffix(rawValue, "kB")))
   429  		if err != nil {
   430  			return -1, err
   431  		}
   432  		if inKb {
   433  			value = value * int(unitutil.KB)
   434  		}
   435  		return int64(value), nil
   436  	}
   437  	return -1, fmt.Errorf("failed to find '%s' entry in path %q", wantKey, meminfoPath)
   438  }
   439  
   440  func readFileToInt64(filename string) (int64, error) {
   441  	data, err := os.ReadFile(filename)
   442  	if err != nil {
   443  		return -1, err
   444  	}
   445  	return strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
   446  }