1
0

topo_linux.go 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. //go:build linux
  2. package cpupick
  3. import (
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "strconv"
  8. "strings"
  9. )
  10. // readTopology probes the NUMA node and physical-core layout of cpus from
  11. // sysfs. Anything sysfs won't say degrades toward flatTopology: an unknown
  12. // node becomes node 0, an unknown core becomes a core of its own — either
  13. // way the corresponding arrange rule becomes a no-op instead of a wrong
  14. // answer.
  15. func readTopology(cpus []int) topology {
  16. return readTopologyFrom("/sys/devices/system/node", "/sys/devices/system/cpu", cpus)
  17. }
  18. func readTopologyFrom(nodeDir, cpuDir string, cpus []int) topology {
  19. coreOf, zeroCore := coreGroups(cpuDir, cpus)
  20. return topology{
  21. nodeOf: numaNodes(nodeDir, cpus),
  22. coreOf: coreOf,
  23. zeroCore: zeroCore,
  24. }
  25. }
  26. // numaNodes maps each cpu to its NUMA node via
  27. // /sys/devices/system/node/nodeN/cpulist. CPUs no node claims (or no node
  28. // dirs at all: VMs, non-NUMA kernels) land on node 0.
  29. func numaNodes(nodeDir string, cpus []int) map[int]int {
  30. out := make(map[int]int, len(cpus))
  31. for _, c := range cpus {
  32. out[c] = 0
  33. }
  34. entries, err := os.ReadDir(nodeDir)
  35. if err != nil {
  36. return out
  37. }
  38. want := make(map[int]bool, len(cpus))
  39. for _, c := range cpus {
  40. want[c] = true
  41. }
  42. for _, e := range entries {
  43. id, ok := strings.CutPrefix(e.Name(), "node")
  44. if !ok {
  45. continue
  46. }
  47. n, err := strconv.Atoi(id)
  48. if err != nil {
  49. continue // has_cpu, possible, ... share the prefix
  50. }
  51. b, err := os.ReadFile(filepath.Join(nodeDir, e.Name(), "cpulist"))
  52. if err != nil {
  53. continue
  54. }
  55. list, err := parseCPUList(strings.TrimSpace(string(b)))
  56. if err != nil {
  57. continue
  58. }
  59. for _, c := range list {
  60. if want[c] {
  61. out[c] = n
  62. }
  63. }
  64. }
  65. return out
  66. }
  67. // coreGroups maps each cpu to a dense physical-core id derived from its
  68. // (physical_package_id, core_id) pair — core_id alone repeats across
  69. // sockets. CPUs whose topology files are unreadable get a core of their own.
  70. // The second return is the group id of the core CPU 0 lives on, or -1 when
  71. // that can't be determined; CPU 0's own files are consulted even when 0 is
  72. // not a candidate, so its SMT siblings are recognized under cpusets that
  73. // exclude CPU 0 itself.
  74. func coreGroups(cpuDir string, cpus []int) (map[int]int, int) {
  75. type pkgCore struct{ pkg, core int }
  76. pairOf := func(cpu int) (pkgCore, bool) {
  77. topoDir := filepath.Join(cpuDir, fmt.Sprintf("cpu%d", cpu), "topology")
  78. pkg, err1 := readIntFile(filepath.Join(topoDir, "physical_package_id"))
  79. core, err2 := readIntFile(filepath.Join(topoDir, "core_id"))
  80. if err1 != nil || err2 != nil {
  81. return pkgCore{}, false
  82. }
  83. return pkgCore{pkg, core}, true
  84. }
  85. ids := map[pkgCore]int{}
  86. out := make(map[int]int, len(cpus))
  87. next := 0
  88. for _, cpu := range cpus {
  89. k, ok := pairOf(cpu)
  90. if !ok {
  91. out[cpu] = next
  92. next++
  93. continue
  94. }
  95. id, ok := ids[k]
  96. if !ok {
  97. id = next
  98. next++
  99. ids[k] = id
  100. }
  101. out[cpu] = id
  102. }
  103. zeroCore := -1
  104. if k, ok := pairOf(0); ok {
  105. if id, ok := ids[k]; ok {
  106. zeroCore = id
  107. }
  108. }
  109. return out, zeroCore
  110. }