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 }