avoid_linux_test.go 2.8 KB

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