github.com/jaypipes/ghw@v0.21.1/pkg/cpu/cpu_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 cpu
     7  
     8  import (
     9  	"bufio"
    10  	"errors"
    11  	"fmt"
    12  	"os"
    13  	"path/filepath"
    14  	"regexp"
    15  	"sort"
    16  	"strconv"
    17  	"strings"
    18  
    19  	"github.com/jaypipes/ghw/pkg/context"
    20  	"github.com/jaypipes/ghw/pkg/linuxpath"
    21  	"github.com/jaypipes/ghw/pkg/util"
    22  )
    23  
    24  var (
    25  	regexForCpulCore = regexp.MustCompile("^cpu([0-9]+)$")
    26  	onlineFile       = "online"
    27  )
    28  
    29  func (i *Info) load() error {
    30  	i.Processors = processorsGet(i.ctx)
    31  	var totCores uint32
    32  	var totThreads uint32
    33  	for _, p := range i.Processors {
    34  		totCores += p.TotalCores
    35  		totThreads += p.TotalHardwareThreads
    36  	}
    37  	i.TotalCores = totCores
    38  	i.TotalHardwareThreads = totThreads
    39  	// TODO(jaypipes): Remove TotalThreads before v1.0
    40  	i.TotalThreads = totThreads
    41  	return nil
    42  }
    43  
    44  func processorsGet(ctx *context.Context) []*Processor {
    45  	paths := linuxpath.New(ctx)
    46  
    47  	lps := logicalProcessorsFromProcCPUInfo(ctx)
    48  	// keyed by processor ID (physical_package_id)
    49  	procs := map[int]*Processor{}
    50  
    51  	// /sys/devices/system/cpu pseudodir contains N number of pseudodirs with
    52  	// information about the logical processors on the host. These logical
    53  	// processor pseudodirs are of the pattern /sys/devices/system/cpu/cpu{N}
    54  	fnames, err := os.ReadDir(paths.SysDevicesSystemCPU)
    55  	if err != nil {
    56  		ctx.Warn("failed to read /sys/devices/system/cpu: %s", err)
    57  		return []*Processor{}
    58  	}
    59  	for _, fname := range fnames {
    60  		matches := regexForCpulCore.FindStringSubmatch(fname.Name())
    61  		if len(matches) < 2 {
    62  			continue
    63  		}
    64  
    65  		lpID, err := strconv.Atoi(matches[1])
    66  		if err != nil {
    67  			ctx.Warn("failed to find numeric logical processor ID: %s", err)
    68  			continue
    69  		}
    70  
    71  		onlineFilePath := filepath.Join(paths.SysDevicesSystemCPU, fmt.Sprintf("cpu%d", lpID), onlineFile)
    72  		if _, err := os.Stat(onlineFilePath); err == nil {
    73  			if util.SafeIntFromFile(ctx, onlineFilePath) == 0 {
    74  				continue
    75  			}
    76  		} else if errors.Is(err, os.ErrNotExist) {
    77  			// Assume the CPU is online if the online state file doesn't exist
    78  			// (as is the case with older snapshots)
    79  		}
    80  		procID := processorIDFromLogicalProcessorID(ctx, lpID)
    81  		proc, found := procs[procID]
    82  		if !found {
    83  			proc = &Processor{ID: procID}
    84  			lp, ok := lps[lpID]
    85  			if !ok {
    86  				ctx.Warn(
    87  					"failed to find attributes for logical processor %d",
    88  					lpID,
    89  				)
    90  				continue
    91  			}
    92  
    93  			// Assumes /proc/cpuinfo is in order of logical processor id, then
    94  			// lps[lpID] describes logical processor `lpID`.
    95  			// Once got a more robust way of fetching the following info,
    96  			// can we drop /proc/cpuinfo.
    97  			if len(lp.Attrs["flags"]) != 0 { // x86
    98  				proc.Capabilities = strings.Split(lp.Attrs["flags"], " ")
    99  			} else if len(lp.Attrs["Features"]) != 0 { // ARM64
   100  				proc.Capabilities = strings.Split(lp.Attrs["Features"], " ")
   101  			}
   102  			if len(lp.Attrs["model name"]) != 0 {
   103  				proc.Model = lp.Attrs["model name"]
   104  			} else if len(lp.Attrs["Processor"]) != 0 { // ARM
   105  				proc.Model = lp.Attrs["Processor"]
   106  			} else if len(lp.Attrs["cpu model"]) != 0 { // MIPS, ARM
   107  				proc.Model = lp.Attrs["cpu model"]
   108  			} else if len(lp.Attrs["Model Name"]) != 0 { // LoongArch
   109  				proc.Model = lp.Attrs["Model Name"]
   110  			} else if len(lp.Attrs["uarch"]) != 0 { // SiFive
   111  				proc.Model = lp.Attrs["uarch"]
   112  			}
   113  			if len(lp.Attrs["vendor_id"]) != 0 {
   114  				proc.Vendor = lp.Attrs["vendor_id"]
   115  			} else if len(lp.Attrs["isa"]) != 0 { // RISCV64
   116  				proc.Vendor = lp.Attrs["isa"]
   117  			} else if lp.Attrs["CPU implementer"] == "0x41" { // ARM
   118  				proc.Vendor = "ARM"
   119  			}
   120  			procs[procID] = proc
   121  		}
   122  
   123  		coreID := coreIDFromLogicalProcessorID(ctx, lpID)
   124  		core := proc.CoreByID(coreID)
   125  		if core == nil {
   126  			core = &ProcessorCore{
   127  				ID:                   coreID,
   128  				TotalHardwareThreads: 1,
   129  				// TODO(jaypipes): Remove NumThreads before v1.0
   130  				NumThreads: 1,
   131  			}
   132  			proc.Cores = append(proc.Cores, core)
   133  			proc.TotalCores += 1
   134  			// TODO(jaypipes): Remove NumCores before v1.0
   135  			proc.NumCores += 1
   136  		} else {
   137  			core.TotalHardwareThreads += 1
   138  			// TODO(jaypipes) Remove NumThreads before v1.0
   139  			core.NumThreads += 1
   140  		}
   141  		proc.TotalHardwareThreads += 1
   142  		// TODO(jaypipes) Remove NumThreads before v1.0
   143  		proc.NumThreads += 1
   144  		core.LogicalProcessors = append(core.LogicalProcessors, lpID)
   145  	}
   146  	res := []*Processor{}
   147  	for _, p := range procs {
   148  		for _, c := range p.Cores {
   149  			sort.Ints(c.LogicalProcessors)
   150  		}
   151  		res = append(res, p)
   152  	}
   153  	return res
   154  }
   155  
   156  // processorIDFromLogicalProcessorID returns the processor physical package ID
   157  // for the supplied logical processor ID
   158  func processorIDFromLogicalProcessorID(ctx *context.Context, lpID int) int {
   159  	paths := linuxpath.New(ctx)
   160  	// Fetch CPU ID
   161  	path := filepath.Join(
   162  		paths.SysDevicesSystemCPU,
   163  		fmt.Sprintf("cpu%d", lpID),
   164  		"topology", "physical_package_id",
   165  	)
   166  	return util.SafeIntFromFile(ctx, path)
   167  }
   168  
   169  // coreIDFromLogicalProcessorID returns the core ID for the supplied logical
   170  // processor ID
   171  func coreIDFromLogicalProcessorID(ctx *context.Context, lpID int) int {
   172  	paths := linuxpath.New(ctx)
   173  	// Fetch CPU ID
   174  	path := filepath.Join(
   175  		paths.SysDevicesSystemCPU,
   176  		fmt.Sprintf("cpu%d", lpID),
   177  		"topology", "core_id",
   178  	)
   179  	return util.SafeIntFromFile(ctx, path)
   180  }
   181  
   182  func CoresForNode(ctx *context.Context, nodeID int) ([]*ProcessorCore, error) {
   183  	// The /sys/devices/system/node/nodeX directory contains a subdirectory
   184  	// called 'cpuX' for each logical processor assigned to the node. Each of
   185  	// those subdirectories contains a topology subdirectory which has a
   186  	// core_id file that indicates the 0-based identifier of the physical core
   187  	// the logical processor (hardware thread) is on.
   188  	paths := linuxpath.New(ctx)
   189  	path := filepath.Join(
   190  		paths.SysDevicesSystemNode,
   191  		fmt.Sprintf("node%d", nodeID),
   192  	)
   193  	cores := make([]*ProcessorCore, 0)
   194  
   195  	findCoreByID := func(coreID int) *ProcessorCore {
   196  		for _, c := range cores {
   197  			if c.ID == coreID {
   198  				return c
   199  			}
   200  		}
   201  
   202  		c := &ProcessorCore{
   203  			ID:                coreID,
   204  			LogicalProcessors: []int{},
   205  		}
   206  		cores = append(cores, c)
   207  		return c
   208  	}
   209  
   210  	files, err := os.ReadDir(path)
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  	for _, file := range files {
   215  		filename := file.Name()
   216  		if !strings.HasPrefix(filename, "cpu") {
   217  			continue
   218  		}
   219  		if filename == "cpumap" || filename == "cpulist" {
   220  			// There are two files in the node directory that start with 'cpu'
   221  			// but are not subdirectories ('cpulist' and 'cpumap'). Ignore
   222  			// these files.
   223  			continue
   224  		}
   225  		// Grab the logical processor ID by cutting the integer from the
   226  		// /sys/devices/system/node/nodeX/cpuX filename
   227  		cpuPath := filepath.Join(path, filename)
   228  		procID, err := strconv.Atoi(filename[3:])
   229  		if err != nil {
   230  			ctx.Warn(
   231  				"failed to determine procID from %s. Expected integer after 3rd char.",
   232  				filename,
   233  			)
   234  			continue
   235  		}
   236  		onlineFilePath := filepath.Join(cpuPath, onlineFile)
   237  		if _, err := os.Stat(onlineFilePath); err == nil {
   238  			if util.SafeIntFromFile(ctx, onlineFilePath) == 0 {
   239  				continue
   240  			}
   241  		} else if errors.Is(err, os.ErrNotExist) {
   242  			// Assume the CPU is online if the online state file doesn't exist
   243  			// (as is the case with older snapshots)
   244  		}
   245  		coreIDPath := filepath.Join(cpuPath, "topology", "core_id")
   246  		coreID := util.SafeIntFromFile(ctx, coreIDPath)
   247  		core := findCoreByID(coreID)
   248  		core.LogicalProcessors = append(
   249  			core.LogicalProcessors,
   250  			procID,
   251  		)
   252  	}
   253  
   254  	for _, c := range cores {
   255  		c.TotalHardwareThreads = uint32(len(c.LogicalProcessors))
   256  		// TODO(jaypipes): Remove NumThreads before v1.0
   257  		c.NumThreads = c.TotalHardwareThreads
   258  	}
   259  
   260  	return cores, nil
   261  }
   262  
   263  // logicalProcessor contains information about a single logical processor
   264  // on the host.
   265  type logicalProcessor struct {
   266  	// This is the logical processor ID assigned by the host. In /proc/cpuinfo,
   267  	// this is the zero-based index of the logical processor as it appears in
   268  	// the /proc/cpuinfo file and matches the "processor" attribute. In
   269  	// /sys/devices/system/cpu/cpu{N} pseudodir entries, this is the N value.
   270  	ID int
   271  	// The entire collection of string attribute name/value pairs for the
   272  	// logical processor.
   273  	Attrs map[string]string
   274  }
   275  
   276  // logicalProcessorsFromProcCPUInfo reads the `/proc/cpuinfo` pseudofile and
   277  // returns a map, keyed by logical processor ID, of logical processor structs.
   278  //
   279  // `/proc/cpuinfo` files look like the following:
   280  //
   281  // ```
   282  // processor	: 0
   283  // vendor_id	: AuthenticAMD
   284  // cpu family	: 23
   285  // model		: 8
   286  // model name	: AMD Ryzen 7 2700X Eight-Core Processor
   287  // stepping	: 2
   288  // microcode	: 0x800820d
   289  // cpu MHz		: 2200.000
   290  // cache size	: 512 KB
   291  // physical id	: 0
   292  // siblings	: 16
   293  // core id		: 0
   294  // cpu cores	: 8
   295  // apicid		: 0
   296  // initial apicid	: 0
   297  // fpu		: yes
   298  // fpu_exception	: yes
   299  // cpuid level	: 13
   300  // wp		: yes
   301  // flags		: fpu vme de pse tsc msr pae mce <snip...>
   302  // bugs		: sysret_ss_attrs null_seg spectre_v1 spectre_v2 spec_store_bypass retbleed smt_rsb
   303  // bogomips	: 7386.41
   304  // TLB size	: 2560 4K pages
   305  // clflush size	: 64
   306  // cache_alignment	: 64
   307  // address sizes	: 43 bits physical, 48 bits virtual
   308  // power management: ts ttp tm hwpstate cpb eff_freq_ro [13] [14]
   309  //
   310  // processor	: 1
   311  // vendor_id	: AuthenticAMD
   312  // cpu family	: 23
   313  // model		: 8
   314  // model name	: AMD Ryzen 7 2700X Eight-Core Processor
   315  // stepping	: 2
   316  // microcode	: 0x800820d
   317  // cpu MHz		: 1885.364
   318  // cache size	: 512 KB
   319  // physical id	: 0
   320  // siblings	: 16
   321  // core id		: 1
   322  // cpu cores	: 8
   323  // apicid		: 2
   324  // initial apicid	: 2
   325  // fpu		: yes
   326  // fpu_exception	: yes
   327  // cpuid level	: 13
   328  // wp		: yes
   329  // flags		: fpu vme de pse tsc msr pae mce <snip...>
   330  // bugs		: sysret_ss_attrs null_seg spectre_v1 spectre_v2 spec_store_bypass retbleed smt_rsb
   331  // bogomips	: 7386.41
   332  // TLB size	: 2560 4K pages
   333  // clflush size	: 64
   334  // cache_alignment	: 64
   335  // address sizes	: 43 bits physical, 48 bits virtual
   336  // power management: ts ttp tm hwpstate cpb eff_freq_ro [13] [14]
   337  // ```
   338  //
   339  // with blank line-separated blocks of colon-delimited attribute name/value
   340  // pairs for a specific logical processor on the host.
   341  func logicalProcessorsFromProcCPUInfo(
   342  	ctx *context.Context,
   343  ) map[int]*logicalProcessor {
   344  	paths := linuxpath.New(ctx)
   345  	r, err := os.Open(paths.ProcCpuinfo)
   346  	if err != nil {
   347  		return nil
   348  	}
   349  	defer util.SafeClose(r)
   350  
   351  	lps := map[int]*logicalProcessor{}
   352  
   353  	// A map of attributes describing the logical processor
   354  	lpAttrs := map[string]string{}
   355  
   356  	scanner := bufio.NewScanner(r)
   357  	for scanner.Scan() {
   358  		line := strings.TrimSpace(scanner.Text())
   359  		if line == "" {
   360  			// Output of /proc/cpuinfo has a blank newline to separate logical
   361  			// processors, so here we collect up all the attributes we've
   362  			// collected for this logical processor block
   363  			lpIDstr, ok := lpAttrs["processor"]
   364  			if !ok {
   365  				ctx.Warn("expected to find 'processor' key in /proc/cpuinfo attributes")
   366  				continue
   367  			}
   368  			lpID, _ := strconv.Atoi(lpIDstr)
   369  			lp := &logicalProcessor{
   370  				ID:    lpID,
   371  				Attrs: lpAttrs,
   372  			}
   373  			lps[lpID] = lp
   374  			// Reset the current set of processor attributes...
   375  			lpAttrs = map[string]string{}
   376  			continue
   377  		}
   378  		parts := strings.Split(line, ":")
   379  		key := strings.TrimSpace(parts[0])
   380  		value := strings.TrimSpace(parts[1])
   381  		lpAttrs[key] = value
   382  	}
   383  	return lps
   384  }