Skip to content

Commit

Permalink
[windows] find processor topology info
Browse files Browse the repository at this point in the history
Adds a `ghw.TopologyInfo` implementation for Windows that uses the
Win32 GetLogicalProcessorInformation API call. This first batch simply
implements NUMA node construction for each identified NUMA node in the
returned logical processor information struct array. Next patches will
implement the cache discovery mechanisms.

Issue #166
  • Loading branch information
jaypipes committed May 31, 2020
1 parent 591c46d commit 8fbcfa8
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
vendor/
coverage*.*
*~
2 changes: 1 addition & 1 deletion topology_stub.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// +build !linux
// +build !linux,!windows
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
Expand Down
169 changes: 169 additions & 0 deletions topology_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//

package ghw

import (
"encoding/binary"
"fmt"
"syscall"
"unsafe"
)

const (
rcFailure = 0
sizeofLogicalProcessorInfo = 32
errInsufficientBuffer syscall.Errno = 122

relationProcessorCore = 0
relationNUMANode = 1
relationCache = 2
relationProcessorPackage = 3
relationGroup = 4
)

func (ctx *context) topologyFillInfo(info *TopologyInfo) error {
nodes, err := topologyNodes()
if err != nil {
return err
}
info.Nodes = nodes
if len(nodes) == 1 {
info.Architecture = ARCHITECTURE_SMP
} else {
info.Architecture = ARCHITECTURE_NUMA
}
return nil
}

func topologyNodes() ([]*TopologyNode, error) {
nodes := make([]*TopologyNode, 0)
lpis, err := getWin32LogicalProcessorInfos()
if err != nil {
return nil, err
}
for _, lpi := range lpis {
switch lpi.relationship {
case relationNUMANode:
nodes = append(nodes, &TopologyNode{
ID: lpi.numaNodeID(),
})
case relationProcessorCore:
// cores++
// processors += countBits(info.ProcessorMask)
case relationProcessorPackage:
// ignore
case relationCache:
// TODO handle cache layers
default:
return nil, fmt.Errorf("Unknown LOGICAL_PROCESSOR_RELATIONSHIP value: %d", lpi.relationship)

}
}
return nodes, nil
}

// This is the CACHE_DESCRIPTOR struct in the Win32 API
type cacheDescriptor struct {
level uint8
associativity uint8
lineSize uint16
size uint32
cacheType uint32
}

// This is the SYSTEM_LOGICAL_PROCESSOR_INFORMATION struct in the Win32 API
type logicalProcessorInfo struct {
processorMask uint64
relationship uint64
// The following dummyunion member is a representation of this part of
// the SYSTEM_LOGICAL_PROCESSOR_INFORMATION struct:
//
// union {
// struct {
// BYTE Flags;
// } ProcessorCore;
// struct {
// DWORD NodeNumber;
// } NumaNode;
// CACHE_DESCRIPTOR Cache;
// ULONGLONG Reserved[2];
// } DUMMYUNIONNAME;
dummyunion [16]byte
}

// numaNodeID returns the NUMA node's identifier from the logical processor
// information struct by grabbing the integer representation of the struct's
// NumaNode unioned data element
func (lpi *logicalProcessorInfo) numaNodeID() int {
if lpi.relationship != relationNUMANode {
return -1
}
return int(binary.LittleEndian.Uint16(lpi.dummyunion[0:]))
}

// ref: https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getlogicalprocessorinformation
func getWin32LogicalProcessorInfos() (
[]*logicalProcessorInfo,
error,
) {
lpis := make([]*logicalProcessorInfo, 0)
win32api := syscall.NewLazyDLL("kernel32.dll")
glpi := win32api.NewProc("GetLogicalProcessorInformation")

// The way the GetLogicalProcessorInformation (GLPI) Win32 API call
// works is wonky, but consistent with the Win32 API calling structure.
// Basically, you need to first call the GLPI API with a NUL pointerr
// and a pointer to an integer. That first call to the API should
// return ERROR_INSUFFICIENT_BUFFER, which is the indication that the
// supplied buffer pointer is NUL and needs to have memory allocated to
// it of an amount equal to the value of the integer pointer argument.
// Once the buffer is allocated this amount of space, the GLPI API call
// is again called. This time, the return value should be 0 and the
// buffer will have been set to an array of
// SYSTEM_LOGICAL_PROCESSOR_INFORMATION structs.
toAllocate := uint32(0)
// first, figure out how much we need
rc, _, win32err := glpi.Call(uintptr(0), uintptr(unsafe.Pointer(&toAllocate)))
if rc == rcFailure {
if win32err != errInsufficientBuffer {
return nil, fmt.Errorf("GetLogicalProcessorInformation Win32 API initial call failed to return ERROR_INSUFFICIENT_BUFFER")
}
} else {
// This shouldn't happen because buffer hasn't yet been allocated...
fmt.Errorf("GetLogicalProcessorInformation Win32 API initial call returned success instead of failure with ERROR_INSUFFICIENT_BUFFER")
}

// OK, now we actually allocate a raw buffer to fill with some number
// of SYSTEM_LOGICAL_PROCESSOR_INFORMATION structs
b := make([]byte, toAllocate)
rc, _, win32err = glpi.Call(uintptr(unsafe.Pointer(&b[0])), uintptr(unsafe.Pointer(&toAllocate)))
if rc == rcFailure {
return nil, fmt.Errorf("GetLogicalProcessorInformation Win32 API call failed to set supplied buffer. Win32 system error: %s", win32err)
}

for x := uint32(0); x < toAllocate; x += sizeofLogicalProcessorInfo {
lpiraw := b[x:x+sizeofLogicalProcessorInfo]
lpi := &logicalProcessorInfo{
processorMask: binary.LittleEndian.Uint64(lpiraw[0:]),
relationship: binary.LittleEndian.Uint64(lpiraw[8:]),
}
copy(lpi.dummyunion[0:16], lpiraw[16:32])
lpis = append(lpis, lpi)
}
return lpis, nil

}

func countBits(num uint64) (count int) {
count = 0
for num > 0 {
if (num & 0x1) == 1 {
count++
}
num >>= 1
}
return
}

0 comments on commit 8fbcfa8

Please sign in to comment.