1
0

cpuavoid_linux_test.go 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. //go:build linux && !android && !e2e_testing
  2. package util
  3. import (
  4. "fmt"
  5. "os"
  6. "os/exec"
  7. "runtime"
  8. "strconv"
  9. "sync"
  10. "testing"
  11. "unsafe"
  12. "github.com/stretchr/testify/require"
  13. "golang.org/x/sys/unix"
  14. )
  15. // TestAvoidCPUs changes the affinity of the whole process, so it runs in a child, leaving the test runner untouched.
  16. func TestAvoidCPUs(t *testing.T) {
  17. if os.Getenv("NEBULA_AVOID_CPU_TEST") != "1" {
  18. exe, err := os.Executable()
  19. require.NoError(t, err)
  20. cmd := exec.Command(exe, "-test.run=^TestAvoidCPUs$", "-test.v")
  21. cmd.Env = append(os.Environ(), "NEBULA_AVOID_CPU_TEST=1")
  22. out, err := cmd.CombinedOutput()
  23. require.NoError(t, err, "%s", out)
  24. return
  25. }
  26. allowed, err := AllowedCPUs()
  27. require.NoError(t, err)
  28. if len(allowed) < 2 {
  29. t.Skip("requires at least two allowed CPUs")
  30. }
  31. cpu := allowed[0]
  32. release := make(chan struct{})
  33. var wg sync.WaitGroup
  34. // lockThreads occupies 8 OS threads until release, so the runtime has to start more
  35. lockThreads := func() {
  36. var ready sync.WaitGroup
  37. for range 8 {
  38. ready.Add(1)
  39. wg.Go(func() {
  40. runtime.LockOSThread()
  41. ready.Done()
  42. <-release
  43. })
  44. }
  45. ready.Wait()
  46. }
  47. lockThreads()
  48. require.NoError(t, AvoidCPUs([]int{cpu}, false))
  49. after, err := AllowedCPUs()
  50. require.NoError(t, err)
  51. require.Equal(t, allowed, after, "configuration check must not mutate affinity")
  52. require.Error(t, AvoidCPUs(allowed, true), "must reject an empty mask before applying")
  53. require.NoError(t, AvoidCPUs([]int{cpu, cpu}, true))
  54. requireAvoided(t, cpu, allowed)
  55. // Existing threads stay locked while new ones inherit the restriction
  56. lockThreads()
  57. requireAvoided(t, cpu, allowed)
  58. close(release)
  59. wg.Wait()
  60. }
  61. // requireAvoided checks no thread may run on cpu, and every thread still may on some CPU of allowed only.
  62. func requireAvoided(t *testing.T, cpu int, allowed []int) {
  63. t.Helper()
  64. entries, err := os.ReadDir("/proc/self/task")
  65. require.NoError(t, err)
  66. for _, entry := range entries {
  67. tid, err := strconv.Atoi(entry.Name())
  68. require.NoError(t, err)
  69. var mask unix.CPUSet
  70. err = unix.SchedGetaffinity(tid, &mask)
  71. if err == unix.ESRCH {
  72. continue
  73. }
  74. require.NoError(t, err)
  75. require.False(t, mask.IsSet(cpu), "thread %d", tid)
  76. require.Positive(t, mask.Count())
  77. for id := range len(mask) * 64 {
  78. if mask.IsSet(id) {
  79. require.Contains(t, allowed, id, "must not widen the inherited mask")
  80. }
  81. }
  82. }
  83. }
  84. // TestAvoidCPUsRange checks the bounds of unix.CPUSet, whose words are 32 bits wide on 32-bit platforms.
  85. func TestAvoidCPUsRange(t *testing.T) {
  86. capacity := int(unsafe.Sizeof(unix.CPUSet{})) * 8
  87. require.NoError(t, AvoidCPUs([]int{capacity - 1}, false))
  88. require.EqualError(t, AvoidCPUs([]int{capacity}, false), fmt.Sprintf("CPU %d is out of range", capacity))
  89. require.EqualError(t, AvoidCPUs([]int{-1}, false), "CPU -1 is out of range")
  90. }