Skip to content

Commit 690c91f

Browse files
kolyshkinclaude
andcommitted
unix: add CPUSetDynamic for systems with more than 1024 CPUs
The existing CPUSet type is a fixed-size array limited to 1024 CPUs, which makes it problematic to use for large systems (such as Google's X4 instances with 1440 and 1920 vCPUs), see e.g. opencontainers/runc#5023. Introduce CPUSetDynamic type and NewCPUSet constructor to support large systems. The bit-managing routines (set/clear/isset/fill/count) are separated and reused. Add variants of SchedGetaffinity, SchedSetaffinity and SetMemPolicy that accept the new type. Amend the documentation for CPUSet. Amend the existing TestSchedSetaffinity to: - test set.Fill; - use t.Cleanup to restore the affinity. Add tests for new functionality (mostly a copy of existing tests). This is an alternative to CL 727540 / CL 727541. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Change-Id: I51bba0305b8dfa7a88a4e7fb8758d73f798574f1 Reviewed-on: https://go-review.googlesource.com/c/sys/+/735380 Reviewed-by: Tobias Klauser <tobias.klauser@gmail.com> Reviewed-by: David Chase <drchase@google.com> Reviewed-by: Michael Pratt <mpratt@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent f33a730 commit 690c91f

4 files changed

Lines changed: 216 additions & 23 deletions

File tree

unix/affinity_linux.go

Lines changed: 112 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,19 @@ import (
1313

1414
const cpuSetSize = _CPU_SETSIZE / _NCPUBITS
1515

16-
// CPUSet represents a CPU affinity mask.
16+
// CPUSet represents a bit mask of CPUs, to be used with [SchedGetaffinity], [SchedSetaffinity],
17+
// and [SetMemPolicy].
18+
//
19+
// Note this type can only represent CPU IDs 0 through 1023.
20+
// Use [CPUSetDynamic]/[NewCPUSet] instead to avoid this limit.
1721
type CPUSet [cpuSetSize]cpuMask
1822

19-
func schedAffinity(trap uintptr, pid int, set *CPUSet) error {
20-
_, _, e := RawSyscall(trap, uintptr(pid), uintptr(unsafe.Sizeof(*set)), uintptr(unsafe.Pointer(set)))
23+
// CPUSetDynamic represents a bit mask of CPUs, to be used with [SchedGetaffinityDynamic],
24+
// [SchedSetaffinityDynamic], and [SetMemPolicyDynamic]. Use [NewCPUSet] to allocate.
25+
type CPUSetDynamic []cpuMask
26+
27+
func schedAffinity(trap uintptr, pid int, size uintptr, ptr unsafe.Pointer) error {
28+
_, _, e := RawSyscall(trap, uintptr(pid), uintptr(size), uintptr(ptr))
2129
if e != 0 {
2230
return errnoErr(e)
2331
}
@@ -27,13 +35,13 @@ func schedAffinity(trap uintptr, pid int, set *CPUSet) error {
2735
// SchedGetaffinity gets the CPU affinity mask of the thread specified by pid.
2836
// If pid is 0 the calling thread is used.
2937
func SchedGetaffinity(pid int, set *CPUSet) error {
30-
return schedAffinity(SYS_SCHED_GETAFFINITY, pid, set)
38+
return schedAffinity(SYS_SCHED_GETAFFINITY, pid, unsafe.Sizeof(*set), unsafe.Pointer(set))
3139
}
3240

3341
// SchedSetaffinity sets the CPU affinity mask of the thread specified by pid.
3442
// If pid is 0 the calling thread is used.
3543
func SchedSetaffinity(pid int, set *CPUSet) error {
36-
return schedAffinity(SYS_SCHED_SETAFFINITY, pid, set)
44+
return schedAffinity(SYS_SCHED_SETAFFINITY, pid, unsafe.Sizeof(*set), unsafe.Pointer(set))
3745
}
3846

3947
// Zero clears the set s, so that it contains no CPUs.
@@ -45,9 +53,7 @@ func (s *CPUSet) Zero() {
4553
// will silently ignore any invalid CPU bits in [CPUSet] so this is an
4654
// efficient way of resetting the CPU affinity of a process.
4755
func (s *CPUSet) Fill() {
48-
for i := range s {
49-
s[i] = ^cpuMask(0)
50-
}
56+
cpuMaskFill(s[:])
5157
}
5258

5359
func cpuBitsIndex(cpu int) int {
@@ -58,36 +64,126 @@ func cpuBitsMask(cpu int) cpuMask {
5864
return cpuMask(1 << (uint(cpu) % _NCPUBITS))
5965
}
6066

61-
// Set adds cpu to the set s.
62-
func (s *CPUSet) Set(cpu int) {
67+
func cpuMaskFill(s []cpuMask) {
68+
for i := range s {
69+
s[i] = ^cpuMask(0)
70+
}
71+
}
72+
73+
func cpuMaskSet(s []cpuMask, cpu int) {
6374
i := cpuBitsIndex(cpu)
6475
if i < len(s) {
6576
s[i] |= cpuBitsMask(cpu)
6677
}
6778
}
6879

69-
// Clear removes cpu from the set s.
70-
func (s *CPUSet) Clear(cpu int) {
80+
func cpuMaskClear(s []cpuMask, cpu int) {
7181
i := cpuBitsIndex(cpu)
7282
if i < len(s) {
7383
s[i] &^= cpuBitsMask(cpu)
7484
}
7585
}
7686

77-
// IsSet reports whether cpu is in the set s.
78-
func (s *CPUSet) IsSet(cpu int) bool {
87+
func cpuMaskIsSet(s []cpuMask, cpu int) bool {
7988
i := cpuBitsIndex(cpu)
8089
if i < len(s) {
8190
return s[i]&cpuBitsMask(cpu) != 0
8291
}
8392
return false
8493
}
8594

86-
// Count returns the number of CPUs in the set s.
87-
func (s *CPUSet) Count() int {
95+
func cpuMaskCount(s []cpuMask) int {
8896
c := 0
8997
for _, b := range s {
9098
c += bits.OnesCount64(uint64(b))
9199
}
92100
return c
93101
}
102+
103+
// Set adds cpu to the set s. If cpu is out of bounds for s, no action is taken.
104+
func (s *CPUSet) Set(cpu int) {
105+
cpuMaskSet(s[:], cpu)
106+
}
107+
108+
// Clear removes cpu from the set s. If cpu is out of bounds for s, no action is taken.
109+
func (s *CPUSet) Clear(cpu int) {
110+
cpuMaskClear(s[:], cpu)
111+
}
112+
113+
// IsSet reports whether cpu is in the set s.
114+
func (s *CPUSet) IsSet(cpu int) bool {
115+
return cpuMaskIsSet(s[:], cpu)
116+
}
117+
118+
// Count returns the number of CPUs in the set s.
119+
func (s *CPUSet) Count() int {
120+
return cpuMaskCount(s[:])
121+
}
122+
123+
// NewCPUSet creates a CPU affinity mask capable of representing CPU IDs
124+
// up to maxCPU (exclusive).
125+
func NewCPUSet(maxCPU int) CPUSetDynamic {
126+
numMasks := (maxCPU + _NCPUBITS - 1) / _NCPUBITS
127+
if numMasks == 0 {
128+
numMasks = 1
129+
}
130+
return make(CPUSetDynamic, numMasks)
131+
}
132+
133+
// Zero clears the set s, so that it contains no CPUs.
134+
func (s CPUSetDynamic) Zero() {
135+
clear(s)
136+
}
137+
138+
// Fill adds all possible CPU bits to the set s. On Linux, [SchedSetaffinityDynamic]
139+
// will silently ignore any invalid CPU bits in [CPUSetDynamic] so this is an
140+
// efficient way of resetting the CPU affinity of a process.
141+
func (s CPUSetDynamic) Fill() {
142+
cpuMaskFill(s)
143+
}
144+
145+
// Set adds cpu to the set s. If cpu is out of bounds for s, no action is taken.
146+
func (s CPUSetDynamic) Set(cpu int) {
147+
cpuMaskSet(s, cpu)
148+
}
149+
150+
// Clear removes cpu from the set s. If cpu is out of bounds for s, no action is taken.
151+
func (s CPUSetDynamic) Clear(cpu int) {
152+
cpuMaskClear(s, cpu)
153+
}
154+
155+
// IsSet reports whether cpu is in the set s.
156+
func (s CPUSetDynamic) IsSet(cpu int) bool {
157+
return cpuMaskIsSet(s, cpu)
158+
}
159+
160+
// Count returns the number of CPUs in the set s.
161+
func (s CPUSetDynamic) Count() int {
162+
return cpuMaskCount(s)
163+
}
164+
165+
func (s CPUSetDynamic) size() uintptr {
166+
return uintptr(len(s)) * unsafe.Sizeof(cpuMask(0))
167+
}
168+
169+
func (s CPUSetDynamic) pointer() unsafe.Pointer {
170+
if len(s) == 0 {
171+
return nil
172+
}
173+
return unsafe.Pointer(&s[0])
174+
}
175+
176+
// SchedGetaffinityDynamic gets the CPU affinity mask of the thread specified by pid.
177+
// If pid is 0 the calling thread is used.
178+
//
179+
// If the set is smaller than the size of the affinity mask used by the kernel,
180+
// [EINVAL] is returned.
181+
func SchedGetaffinityDynamic(pid int, set CPUSetDynamic) error {
182+
return schedAffinity(SYS_SCHED_GETAFFINITY, pid, set.size(), set.pointer())
183+
}
184+
185+
// SchedSetaffinityDynamic sets the CPU affinity mask of the thread specified by pid.
186+
// If pid is 0 the calling thread is used.
187+
func SchedSetaffinityDynamic(pid int, set CPUSetDynamic) error {
188+
return schedAffinity(SYS_SCHED_SETAFFINITY, pid, set.size(), set.pointer())
189+
}

unix/syscall_linux.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2644,8 +2644,12 @@ func SchedGetAttr(pid int, flags uint) (*SchedAttr, error) {
26442644
//sys Cachestat(fd uint, crange *CachestatRange, cstat *Cachestat_t, flags uint) (err error)
26452645
//sys Mseal(b []byte, flags uint) (err error)
26462646

2647-
//sys setMemPolicy(mode int, mask *CPUSet, size int) (err error) = SYS_SET_MEMPOLICY
2647+
//sys setMemPolicy(mode int, mask unsafe.Pointer, size uintptr) (err error) = SYS_SET_MEMPOLICY
26482648

26492649
func SetMemPolicy(mode int, mask *CPUSet) error {
2650-
return setMemPolicy(mode, mask, _CPU_SETSIZE)
2650+
return setMemPolicy(mode, unsafe.Pointer(mask), _CPU_SETSIZE)
2651+
}
2652+
2653+
func SetMemPolicyDynamic(mode int, mask CPUSetDynamic) error {
2654+
return setMemPolicy(mode, mask.pointer(), mask.size())
26512655
}

unix/syscall_linux_test.go

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"path/filepath"
2020
"runtime"
2121
"runtime/debug"
22+
"slices"
2223
"strconv"
2324
"strings"
2425
"syscall"
@@ -512,7 +513,12 @@ func TestPselectWithSigmask(t *testing.T) {
512513
}
513514

514515
func TestSchedSetaffinity(t *testing.T) {
516+
const maxcpus = 1024 // _CPU_SETSIZE
515517
var newMask unix.CPUSet
518+
newMask.Fill()
519+
if count := newMask.Count(); count != maxcpus {
520+
t.Errorf("Fill: got %d CPUs, want %d", count, maxcpus)
521+
}
516522
newMask.Zero()
517523
if newMask.Count() != 0 {
518524
t.Errorf("CpuZero: didn't zero CPU set: %v", newMask)
@@ -566,6 +572,14 @@ func TestSchedSetaffinity(t *testing.T) {
566572
}
567573
}
568574

575+
t.Cleanup(func() {
576+
// Restore old mask so it doesn't affect successive tests.
577+
err = unix.SchedSetaffinity(0, &oldMask)
578+
if err != nil {
579+
t.Fatalf("SchedSetaffinity: %v", err)
580+
}
581+
})
582+
569583
err = unix.SchedSetaffinity(0, &newMask)
570584
if err != nil {
571585
t.Fatalf("SchedSetaffinity: %v", err)
@@ -580,11 +594,90 @@ func TestSchedSetaffinity(t *testing.T) {
580594
if gotMask != newMask {
581595
t.Errorf("SchedSetaffinity: returned affinity mask does not match set affinity mask")
582596
}
597+
}
598+
599+
func TestSchedSetaffinityDynamic(t *testing.T) {
600+
const maxcpus = 4096
601+
602+
newMask := unix.NewCPUSet(maxcpus)
603+
newMask.Fill()
604+
if count := newMask.Count(); count != maxcpus {
605+
t.Errorf("Fill: got %d CPUs, want %d", count, maxcpus)
606+
}
607+
newMask.Zero()
608+
if newMask.Count() != 0 {
609+
t.Errorf("Zero: didn't zero CPU set: %v", newMask)
610+
}
611+
cpu := 1
612+
newMask.Set(cpu)
613+
if newMask.Count() != 1 || !newMask.IsSet(cpu) {
614+
t.Errorf("Set: didn't set CPU %d in set: %v", cpu, newMask)
615+
}
616+
cpu = 5
617+
newMask.Set(cpu)
618+
if newMask.Count() != 2 || !newMask.IsSet(cpu) {
619+
t.Errorf("Set: didn't set CPU %d in set: %v", cpu, newMask)
620+
}
621+
newMask.Clear(cpu)
622+
if newMask.Count() != 1 || newMask.IsSet(cpu) {
623+
t.Errorf("Clear: didn't clear CPU %d in set: %v", cpu, newMask)
624+
}
625+
626+
runtime.LockOSThread()
627+
defer runtime.UnlockOSThread()
583628

584-
// Restore old mask so it doesn't affect successive tests
585-
err = unix.SchedSetaffinity(0, &oldMask)
629+
oldMask := unix.NewCPUSet(maxcpus)
630+
err := unix.SchedGetaffinityDynamic(0, oldMask)
586631
if err != nil {
587-
t.Fatalf("SchedSetaffinity: %v", err)
632+
t.Fatalf("SchedGetaffinityDynamic: %v", err)
633+
}
634+
635+
if runtime.NumCPU() < 2 {
636+
t.Skip("skipping setaffinity tests on single CPU system")
637+
}
638+
if runtime.GOOS == "android" {
639+
t.Skip("skipping setaffinity tests on android")
640+
}
641+
642+
// On a system like ppc64x where some cores can be disabled using ppc64_cpu,
643+
// setaffinity should only be called with enabled cores. The valid cores
644+
// are found from the oldMask, but if none are found then the setaffinity
645+
// tests are skipped. Issue #27875.
646+
cpu = 1
647+
if !oldMask.IsSet(cpu) {
648+
newMask.Zero()
649+
for i := range len(oldMask) {
650+
if oldMask.IsSet(i) {
651+
newMask.Set(i)
652+
break
653+
}
654+
}
655+
if newMask.Count() == 0 {
656+
t.Skip("skipping setaffinity tests if CPU not available")
657+
}
658+
}
659+
660+
t.Cleanup(func() {
661+
// Restore old mask so it doesn't affect successive tests.
662+
err = unix.SchedSetaffinityDynamic(0, oldMask)
663+
if err != nil {
664+
t.Fatalf("SchedSetaffinityDynamic: %v", err)
665+
}
666+
})
667+
668+
err = unix.SchedSetaffinityDynamic(0, newMask)
669+
if err != nil {
670+
t.Fatalf("SchedSetaffinityDynamic: %v", err)
671+
}
672+
673+
gotMask := unix.NewCPUSet(maxcpus)
674+
err = unix.SchedGetaffinityDynamic(0, gotMask)
675+
if err != nil {
676+
t.Fatalf("SchedGetaffinityDynamic: %v", err)
677+
}
678+
679+
if !slices.Equal(gotMask, newMask) {
680+
t.Errorf("SchedSetaffinityDynamic: returned affinity mask does not match set affinity mask (%+v != %+v", gotMask, newMask)
588681
}
589682
}
590683

unix/zsyscall_linux.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)