Skip to content

Commit b8c92ac

Browse files
committed
multiplatform support for cpuinfo
This adds basic handling of reading /proc/cpuinfo for ARM, PPC, and s390x. Signed-off-by: Paul Gier <[email protected]>
1 parent 436b8d6 commit b8c92ac

File tree

3 files changed

+400
-6
lines changed

3 files changed

+400
-6
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
steps:
3535
- checkout
3636
- run: sudo pip install codespell
37-
- run: codespell --skip=".git,./vendor,ttar,go.mod,go.sum" -L uint,packages\'
37+
- run: codespell --skip=".git,./vendor,ttar,go.mod,go.sum" -L uint,packages\',te
3838

3939
workflows:
4040
version: 2

cpuinfo.go

Lines changed: 239 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package procfs
1616
import (
1717
"bufio"
1818
"bytes"
19+
"errors"
20+
"regexp"
1921
"strconv"
2022
"strings"
2123

@@ -52,6 +54,41 @@ type CPUInfo struct {
5254
PowerManagement string
5355
}
5456

57+
const (
58+
platformX86 = "x86"
59+
platformARM = "arm"
60+
platformS390X = "s390x"
61+
platformPPC = "ppc"
62+
platformUnknown = "unknown"
63+
)
64+
65+
var (
66+
cpuinfoX86Regexp = regexp.MustCompile(`(?m)^\s*processor\s*:\s*\d+\s*vendor`)
67+
cpuinfoARMRegexp = regexp.MustCompile(`^\s*Processor\s*:\s*ARM`)
68+
cpuinfoS390XRegexp = regexp.MustCompile(`^\s*vendor_id\s*:\s*IBM/S390`)
69+
cpuinfoPPCRegexp = regexp.MustCompile(`(?m)^\s*processor\s*:\s*\d+\s+cpu\s+:\s+POWER`)
70+
71+
cpuinfoClockRegexp = regexp.MustCompile(`([\d.]+)`)
72+
cpuinfoS390XProcessorRegexp = regexp.MustCompile(`^processor\s+(\d+):.*`)
73+
)
74+
75+
// cpuinfoDetectFormat attempts to determine the format used by the cpuinfo.
76+
// This format corresponds to the platform generating the /proc/cpuinfo file.
77+
// Returns "unknown"
78+
func cpuinfoDetectFormat(info []byte) string {
79+
switch {
80+
case cpuinfoX86Regexp.Match(info):
81+
return platformX86
82+
case cpuinfoARMRegexp.Match(info):
83+
return platformARM
84+
case cpuinfoPPCRegexp.Match(info):
85+
return platformPPC
86+
case cpuinfoS390XRegexp.Match(info):
87+
return platformS390X
88+
}
89+
return platformUnknown
90+
}
91+
5592
// CPUInfo returns information about current system CPUs.
5693
// See https://www.kernel.org/doc/Documentation/filesystems/proc.txt
5794
func (fs FS) CPUInfo() ([]CPUInfo, error) {
@@ -64,12 +101,40 @@ func (fs FS) CPUInfo() ([]CPUInfo, error) {
64101

65102
// parseCPUInfo parses data from /proc/cpuinfo
66103
func parseCPUInfo(info []byte) ([]CPUInfo, error) {
67-
cpuinfo := []CPUInfo{}
68-
i := -1
104+
platform := cpuinfoDetectFormat(info)
105+
switch platform {
106+
case platformX86:
107+
return parseCPUInfoX86(info)
108+
case platformARM:
109+
return parseCPUInfoARM(info)
110+
case platformS390X:
111+
return parseCPUInfoS390X(info)
112+
case platformPPC:
113+
return parseCPUInfoPPC(info)
114+
}
115+
return nil, errors.New("unable to determine format of 'cpuinfo'")
116+
}
117+
118+
func parseCPUInfoX86(info []byte) ([]CPUInfo, error) {
69119
scanner := bufio.NewScanner(bytes.NewReader(info))
120+
121+
// find the first "processor" line
122+
firstLine := firstNonEmptyLine(scanner)
123+
if !strings.HasPrefix(firstLine, "processor") || !strings.Contains(firstLine, ":") {
124+
return nil, errors.New("invalid cpuinfo file: " + firstLine)
125+
}
126+
field := strings.SplitN(firstLine, ": ", 2)
127+
v, err := strconv.ParseUint(field[1], 0, 32)
128+
if err != nil {
129+
return nil, err
130+
}
131+
firstcpu := CPUInfo{Processor: uint(v)}
132+
cpuinfo := []CPUInfo{firstcpu}
133+
i := 0
134+
70135
for scanner.Scan() {
71136
line := scanner.Text()
72-
if strings.TrimSpace(line) == "" {
137+
if !strings.Contains(line, ":") {
73138
continue
74139
}
75140
field := strings.SplitN(line, ": ", 2)
@@ -82,7 +147,7 @@ func parseCPUInfo(info []byte) ([]CPUInfo, error) {
82147
return nil, err
83148
}
84149
cpuinfo[i].Processor = uint(v)
85-
case "vendor_id":
150+
case "vendor", "vendor_id":
86151
cpuinfo[i].VendorID = field[1]
87152
case "cpu family":
88153
cpuinfo[i].CPUFamily = field[1]
@@ -163,5 +228,175 @@ func parseCPUInfo(info []byte) ([]CPUInfo, error) {
163228
}
164229
}
165230
return cpuinfo, nil
231+
}
232+
233+
func parseCPUInfoARM(info []byte) ([]CPUInfo, error) {
234+
scanner := bufio.NewScanner(bytes.NewReader(info))
235+
236+
firstLine := firstNonEmptyLine(scanner)
237+
if !strings.HasPrefix(firstLine, "Processor") || !strings.Contains(firstLine, ":") {
238+
return nil, errors.New("invalid cpuinfo file: " + firstLine)
239+
}
240+
field := strings.SplitN(firstLine, ": ", 2)
241+
commonCPUInfo := CPUInfo{VendorID: field[1]}
242+
243+
cpuinfo := []CPUInfo{}
244+
i := -1
245+
featuresLine := ""
246+
247+
for scanner.Scan() {
248+
line := scanner.Text()
249+
if !strings.Contains(line, ":") {
250+
continue
251+
}
252+
field := strings.SplitN(line, ": ", 2)
253+
switch strings.TrimSpace(field[0]) {
254+
case "processor":
255+
cpuinfo = append(cpuinfo, commonCPUInfo) // start of the next processor
256+
i++
257+
v, err := strconv.ParseUint(field[1], 0, 32)
258+
if err != nil {
259+
return nil, err
260+
}
261+
cpuinfo[i].Processor = uint(v)
262+
case "BogoMIPS":
263+
v, err := strconv.ParseFloat(field[1], 64)
264+
if err != nil {
265+
return nil, err
266+
}
267+
cpuinfo[i].BogoMips = v
268+
case "Features":
269+
featuresLine = line
270+
}
271+
}
272+
fields := strings.SplitN(featuresLine, ": ", 2)
273+
for i := range cpuinfo {
274+
cpuinfo[i].Flags = strings.Fields(fields[1])
275+
}
276+
return cpuinfo, nil
277+
}
278+
279+
func parseCPUInfoS390X(info []byte) ([]CPUInfo, error) {
280+
scanner := bufio.NewScanner(bytes.NewReader(info))
281+
282+
firstLine := firstNonEmptyLine(scanner)
283+
if !strings.HasPrefix(firstLine, "vendor_id") || !strings.Contains(firstLine, ":") {
284+
return nil, errors.New("invalid cpuinfo file: " + firstLine)
285+
}
286+
field := strings.SplitN(firstLine, ": ", 2)
287+
cpuinfo := []CPUInfo{}
288+
commonCPUInfo := CPUInfo{VendorID: field[1]}
166289

290+
for scanner.Scan() {
291+
line := scanner.Text()
292+
if !strings.Contains(line, ":") {
293+
continue
294+
}
295+
field := strings.SplitN(line, ": ", 2)
296+
switch strings.TrimSpace(field[0]) {
297+
case "bogomips per cpu":
298+
v, err := strconv.ParseFloat(field[1], 64)
299+
if err != nil {
300+
return nil, err
301+
}
302+
commonCPUInfo.BogoMips = v
303+
case "features":
304+
commonCPUInfo.Flags = strings.Fields(field[1])
305+
}
306+
if strings.HasPrefix(line, "processor") {
307+
match := cpuinfoS390XProcessorRegexp.FindStringSubmatch(line)
308+
if len(match) < 2 {
309+
return nil, errors.New("Invalid line found in cpuinfo: " + line)
310+
}
311+
cpu := commonCPUInfo
312+
v, err := strconv.ParseUint(match[1], 0, 32)
313+
if err != nil {
314+
return nil, err
315+
}
316+
cpu.Processor = uint(v)
317+
cpuinfo = append(cpuinfo, cpu)
318+
}
319+
if strings.HasPrefix(line, "cpu number") {
320+
break
321+
}
322+
}
323+
324+
i := 0
325+
for scanner.Scan() {
326+
line := scanner.Text()
327+
if !strings.Contains(line, ":") {
328+
continue
329+
}
330+
field := strings.SplitN(line, ": ", 2)
331+
switch strings.TrimSpace(field[0]) {
332+
case "cpu number":
333+
i++
334+
case "cpu MHz dynamic":
335+
clock := cpuinfoClockRegexp.FindString(strings.TrimSpace(field[1]))
336+
v, err := strconv.ParseFloat(clock, 64)
337+
if err != nil {
338+
return nil, err
339+
}
340+
cpuinfo[i].CPUMHz = v
341+
}
342+
}
343+
344+
return cpuinfo, nil
345+
}
346+
347+
func parseCPUInfoPPC(info []byte) ([]CPUInfo, error) {
348+
scanner := bufio.NewScanner(bytes.NewReader(info))
349+
350+
firstLine := firstNonEmptyLine(scanner)
351+
if !strings.HasPrefix(firstLine, "processor") || !strings.Contains(firstLine, ":") {
352+
return nil, errors.New("invalid cpuinfo file: " + firstLine)
353+
}
354+
field := strings.SplitN(firstLine, ": ", 2)
355+
v, err := strconv.ParseUint(field[1], 0, 32)
356+
if err != nil {
357+
return nil, err
358+
}
359+
firstcpu := CPUInfo{Processor: uint(v)}
360+
cpuinfo := []CPUInfo{firstcpu}
361+
i := 0
362+
363+
for scanner.Scan() {
364+
line := scanner.Text()
365+
if !strings.Contains(line, ":") {
366+
continue
367+
}
368+
field := strings.SplitN(line, ": ", 2)
369+
switch strings.TrimSpace(field[0]) {
370+
case "processor":
371+
cpuinfo = append(cpuinfo, CPUInfo{}) // start of the next processor
372+
i++
373+
v, err := strconv.ParseUint(field[1], 0, 32)
374+
if err != nil {
375+
return nil, err
376+
}
377+
cpuinfo[i].Processor = uint(v)
378+
case "cpu":
379+
cpuinfo[i].VendorID = field[1]
380+
case "clock":
381+
clock := cpuinfoClockRegexp.FindString(strings.TrimSpace(field[1]))
382+
v, err := strconv.ParseFloat(clock, 64)
383+
if err != nil {
384+
return nil, err
385+
}
386+
cpuinfo[i].CPUMHz = v
387+
}
388+
}
389+
return cpuinfo, nil
390+
}
391+
392+
// firstNonEmptyLine advances the scanner to the first non-empty line
393+
// and returns the contents of that line
394+
func firstNonEmptyLine(scanner *bufio.Scanner) string {
395+
for scanner.Scan() {
396+
line := scanner.Text()
397+
if strings.TrimSpace(line) != "" {
398+
return line
399+
}
400+
}
401+
return ""
167402
}

0 commit comments

Comments
 (0)