irqcpus_linux.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. //go:build linux && !android && !e2e_testing
  2. package util
  3. import (
  4. "os"
  5. "path/filepath"
  6. "strconv"
  7. "strings"
  8. )
  9. // NICIRQCPUs returns the set of CPUs that service interrupts for the ACTIVE
  10. // RX/TX queues of physical network interfaces that are up. Read-only: it
  11. // matches /proc/interrupts action names against each NIC's PCI address and
  12. // interface name, drops vectors whose queue index is beyond the device's
  13. // active queue count (drivers like mlx5 keep handlers registered for
  14. // deactivated queues, so /proc/interrupts alone over-reports), and unions
  15. // /proc/irq/<n>/effective_affinity_list for the survivors.
  16. //
  17. // Callers use this to keep busy pinned threads OFF those CPUs: a thread
  18. // pinned onto a core that also runs NAPI for a NIC RX queue competes with
  19. // softirq processing for the core and measurably collapses throughput for
  20. // flows hashed to that queue.
  21. func NICIRQCPUs() (map[int]bool, error) {
  22. return nicIRQCPUs("/sys/class/net", "/proc/irq", "/proc/interrupts")
  23. }
  24. // irqAction is one row of /proc/interrupts: the IRQ number and the action
  25. // (handler) name in its final column, e.g. "mlx5_comp3@pci:0000:82:00.0".
  26. type irqAction struct {
  27. irq string
  28. action string
  29. }
  30. func nicIRQCPUs(netDir, irqDir, interruptsPath string) (map[int]bool, error) {
  31. actions, err := parseInterrupts(interruptsPath)
  32. if err != nil {
  33. return nil, err
  34. }
  35. devs, err := os.ReadDir(netDir)
  36. if err != nil {
  37. return nil, err
  38. }
  39. cpus := make(map[int]bool)
  40. for _, dev := range devs {
  41. devPath := filepath.Join(netDir, dev.Name())
  42. pciDev, err := filepath.EvalSymlinks(filepath.Join(devPath, "device"))
  43. if err != nil {
  44. continue // virtual device (lo, tun, bridge, vlan, ...)
  45. }
  46. pciAddr := filepath.Base(pciDev)
  47. state, err := os.ReadFile(filepath.Join(devPath, "operstate"))
  48. if err != nil || strings.TrimSpace(string(state)) != "up" {
  49. continue // a down NIC's queue IRQs don't fire
  50. }
  51. nq := countQueues(filepath.Join(devPath, "queues"))
  52. for _, ia := range actions {
  53. if !strings.Contains(ia.action, pciAddr) && !containsWord(ia.action, dev.Name()) {
  54. continue
  55. }
  56. // Vector naming puts the queue index at the end of the handler
  57. // name (mlx5_comp3@pci:..., ice-eth0-TxRx-3, virtio0-input.3).
  58. // An index at or beyond the active queue count is a handler for
  59. // a deactivated queue: registered, but it will not fire.
  60. name, _, _ := strings.Cut(ia.action, "@")
  61. if idx, ok := trailingInt(name); ok && idx >= nq {
  62. continue
  63. }
  64. for _, cpu := range irqAffinity(irqDir, ia.irq) {
  65. cpus[cpu] = true
  66. }
  67. }
  68. }
  69. return cpus, nil
  70. }
  71. // parseInterrupts extracts (irq, action) pairs from /proc/interrupts,
  72. // skipping the header and the non-numeric summary rows (NMI, LOC, ...).
  73. func parseInterrupts(path string) ([]irqAction, error) {
  74. data, err := os.ReadFile(path)
  75. if err != nil {
  76. return nil, err
  77. }
  78. var out []irqAction
  79. for line := range strings.SplitSeq(string(data), "\n") {
  80. fields := strings.Fields(line)
  81. if len(fields) < 2 {
  82. continue
  83. }
  84. irq, ok := strings.CutSuffix(fields[0], ":")
  85. if !ok {
  86. continue
  87. }
  88. if _, err := strconv.Atoi(irq); err != nil {
  89. continue
  90. }
  91. out = append(out, irqAction{irq: irq, action: fields[len(fields)-1]})
  92. }
  93. return out, nil
  94. }
  95. // irqAffinity returns the CPUs IRQ n actually targets.
  96. // effective_affinity_list is the vector's real target; smp_affinity_list
  97. // (the fallback for kernels without effective affinity reporting) is the
  98. // admin-allowed mask and may be wider.
  99. func irqAffinity(irqDir, irq string) []int {
  100. irqPath := filepath.Join(irqDir, irq)
  101. list, err := os.ReadFile(filepath.Join(irqPath, "effective_affinity_list"))
  102. if err != nil || len(strings.TrimSpace(string(list))) == 0 {
  103. list, err = os.ReadFile(filepath.Join(irqPath, "smp_affinity_list"))
  104. if err != nil {
  105. return nil
  106. }
  107. }
  108. return parseCPUList(strings.TrimSpace(string(list)))
  109. }
  110. // countQueues counts the rx-* entries of a netdev's queues directory — the
  111. // device's ACTIVE RX queues (sysfs removes the directories when a queue is
  112. // deactivated, e.g. by ethtool -L).
  113. func countQueues(queuesDir string) int {
  114. entries, err := os.ReadDir(queuesDir)
  115. if err != nil {
  116. return 0
  117. }
  118. n := 0
  119. for _, e := range entries {
  120. if strings.HasPrefix(e.Name(), "rx-") {
  121. n++
  122. }
  123. }
  124. return n
  125. }
  126. // containsWord reports whether s contains word bounded by non-alphanumeric
  127. // characters (or string edges), so ifname "eth0" doesn't match "eth01".
  128. func containsWord(s, word string) bool {
  129. for start := 0; ; {
  130. i := strings.Index(s[start:], word)
  131. if i < 0 {
  132. return false
  133. }
  134. i += start
  135. before := i == 0 || !isAlnum(s[i-1])
  136. afterIdx := i + len(word)
  137. after := afterIdx == len(s) || !isAlnum(s[afterIdx])
  138. if before && after {
  139. return true
  140. }
  141. start = i + 1
  142. }
  143. }
  144. func isAlnum(b byte) bool {
  145. return b >= '0' && b <= '9' || b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z'
  146. }
  147. // trailingInt parses the decimal digits at the end of s.
  148. func trailingInt(s string) (int, bool) {
  149. i := len(s)
  150. for i > 0 && s[i-1] >= '0' && s[i-1] <= '9' {
  151. i--
  152. }
  153. if i == len(s) {
  154. return 0, false
  155. }
  156. n, err := strconv.Atoi(s[i:])
  157. return n, err == nil
  158. }
  159. // parseCPUList parses the kernel's cpulist format: comma-separated CPU ids
  160. // or inclusive ranges, e.g. "0-3,8,10-12". Malformed elements are skipped —
  161. // this parses trusted kernel output, not user input.
  162. func parseCPUList(s string) []int {
  163. if s == "" {
  164. return nil
  165. }
  166. var cpus []int
  167. for part := range strings.SplitSeq(s, ",") {
  168. lo, hi, ok := strings.Cut(part, "-")
  169. start, err := strconv.Atoi(strings.TrimSpace(lo))
  170. if err != nil {
  171. continue
  172. }
  173. end := start
  174. if ok {
  175. if end, err = strconv.Atoi(strings.TrimSpace(hi)); err != nil {
  176. continue
  177. }
  178. }
  179. for cpu := start; cpu <= end; cpu++ {
  180. cpus = append(cpus, cpu)
  181. }
  182. }
  183. return cpus
  184. }