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 }