Merge pull request #5343 from kolyshkin/cpu-aff-dyn

Remove 1024 CPUs limit
This commit is contained in:
Kir Kolyshkin
2026-07-29 11:18:53 -07:00
committed by GitHub
6 changed files with 112 additions and 73 deletions

View File

@@ -6,12 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### libcontainer API ###
- `configs.ToCPUSet` now returns a `unix.CPUSetDynamic` instead of a
`*unix.CPUSet`, and the `Initial`/`Final` fields of `configs.CPUAffinity` and
the `Nodes` field of `configs.LinuxMemoryPolicy` have changed type
accordingly. This lifts the previous 1024 CPUs/nodes limit. (#5343)
### Fixed ###
- The poststart hooks are now executed after starting the user-specified
process, fixing a runtime-spec conformance issue. (#4347, #5186)
### Changed ###
- Updated builds to libseccomp v2.6.1. (#5376)
- The `cpuAffinity` and NUMA `memoryPolicy` settings are no longer limited
to 1024 CPUs/nodes, as runc now uses a dynamically-sized CPU mask. (#5343)
## [1.5.0] - 2026-06-19

View File

@@ -2,7 +2,6 @@ package linux
import (
"os"
"unsafe"
"golang.org/x/sys/unix"
)
@@ -66,18 +65,10 @@ func Recvfrom(fd int, p []byte, flags int) (n int, from unix.Sockaddr, err error
return n, from, err
}
// SchedSetaffinity wraps sched_setaffinity syscall without unix.CPUSet size limitation.
func SchedSetaffinity(pid int, buf []byte) error {
// SchedSetaffinity wraps [unix.SchedSetaffinityDynamic].
func SchedSetaffinity(pid int, aff unix.CPUSetDynamic) error {
err := retryOnEINTR(func() error {
_, _, errno := unix.Syscall(
unix.SYS_SCHED_SETAFFINITY,
uintptr(pid),
uintptr(len(buf)),
uintptr((unsafe.Pointer)(&buf[0])))
if errno != 0 {
return errno
}
return nil
return unix.SchedSetaffinityDynamic(pid, aff)
})
return os.NewSyscallError("sched_setaffinity", err)
}
@@ -90,10 +81,10 @@ func Sendmsg(fd int, p, oob []byte, to unix.Sockaddr, flags int) error {
return os.NewSyscallError("sendmsg", err)
}
// SetMempolicy wraps set_mempolicy.
func SetMempolicy(mode int, mask *unix.CPUSet) error {
// SetMempolicy wraps [unix.SetMempolicyDynamic].
func SetMempolicy(mode int, mask unix.CPUSetDynamic) error {
err := retryOnEINTR(func() error {
return unix.SetMemPolicy(mode, mask)
return unix.SetMemPolicyDynamic(mode, mask)
})
return os.NewSyscallError("set_mempolicy", err)
}

View File

@@ -11,7 +11,6 @@ import (
"strconv"
"strings"
"time"
"unsafe"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
@@ -305,26 +304,53 @@ func ToSchedAttr(scheduler *Scheduler) (*unix.SchedAttr, error) {
type IOPriority = specs.LinuxIOPriority
type CPUAffinity struct {
Initial, Final *unix.CPUSet
Initial, Final unix.CPUSetDynamic
}
// ToCPUSet parses a string in list format into a unix.CPUSet, e.g. "0-3,5,7-9".
func ToCPUSet(str string) (*unix.CPUSet, error) {
// MaxCPU is the highest CPU/NUMA node ID that [ToCPUSet] accepts.
// It is an arbitrary sanity limit, used to avoid allocating
// an unreasonably large mask for a bogus input.
const MaxCPU = 64 * 1024
// ToCPUSet parses a string in list format (e.g. "0-3,5,7-9")
// into a [unix.CPUSetDynamic].
func ToCPUSet(str string) (unix.CPUSetDynamic, error) {
if str == "" {
return nil, nil
}
s := new(unix.CPUSet)
// Since (*CPUset).Set silently ignores too high CPU values,
// find out what the maximum is, and return an error.
maxCPU := uint64(unsafe.Sizeof(*s) * 8)
maxID, ranges, err := cpuStrToRanges(str)
if err != nil {
return nil, err
}
if len(ranges) == 0 {
return nil, fmt.Errorf("no members found in set %q", str)
}
s := unix.NewCPUSet(maxID + 1)
for _, cr := range ranges {
for i := cr.start; i <= cr.end; i++ {
s.Set(i)
}
}
return s, nil
}
type cpuRange struct {
start, end int
}
// cpuStrToRanges parses string (e.g. "0-3,5,7-9") into a slice of cpuRange
// values. It also returns the maximum value and an error, if any.
func cpuStrToRanges(str string) (maxID int, ranges []cpuRange, _ error) {
toInt := func(v string) (int, error) {
ret, err := strconv.ParseUint(v, 10, 32)
if err != nil {
return 0, err
}
if ret >= maxCPU {
return 0, fmt.Errorf("values larger than %d are not supported", maxCPU-1)
if ret > MaxCPU {
return 0, fmt.Errorf("values larger than %d are not supported", MaxCPU)
}
return int(ret), nil
}
@@ -336,34 +362,27 @@ func ToCPUSet(str string) (*unix.CPUSet, error) {
if r == "" {
continue
}
if r0, r1, found := strings.Cut(r, "-"); found {
start, err := toInt(r0)
if err != nil {
return nil, err
start, end, found := strings.Cut(r, "-")
cr := cpuRange{}
var err error
if cr.start, err = toInt(start); err != nil {
return 0, nil, err
}
if found {
if cr.end, err = toInt(end); err != nil {
return 0, nil, err
}
end, err := toInt(r1)
if err != nil {
return nil, err
}
if start > end {
return nil, errors.New("invalid range: " + r)
}
for i := start; i <= end; i++ {
s.Set(i)
if cr.start > cr.end {
return 0, nil, errors.New("invalid range: " + r)
}
} else {
val, err := toInt(r)
if err != nil {
return nil, err
}
s.Set(val)
cr.end = cr.start
}
}
if s.Count() == 0 {
return nil, fmt.Errorf("no members found in set %q", str)
maxID = max(maxID, cr.end)
ranges = append(ranges, cr)
}
return s, nil
return maxID, ranges, nil
}
// ConvertCPUAffinity converts [specs.CPUAffinity] to [CPUAffinity].

View File

@@ -10,5 +10,5 @@ type LinuxMemoryPolicy struct {
// Flags contains mode flags.
Flags int `json:"flags,omitempty"`
// Nodes contains NUMA nodes to which the mode applies.
Nodes *unix.CPUSet `json:"nodes,omitempty"`
Nodes unix.CPUSetDynamic `json:"nodes,omitempty"`
}

View File

@@ -1,47 +1,67 @@
package configs
import (
"slices"
"testing"
"golang.org/x/sys/unix"
)
func TestToCPUSet(t *testing.T) {
set := func(cpus ...int) *unix.CPUSet {
r := &unix.CPUSet{}
set := func(cpus ...int) unix.CPUSetDynamic {
maxCPU := 0
for _, cpu := range cpus {
maxCPU = max(maxCPU, cpu)
}
r := unix.NewCPUSet(maxCPU + 1)
for _, cpu := range cpus {
r.Set(cpu)
}
return r
}
// trim removes trailing all-zero masks so that sets representing the
// same CPUs compare equal regardless of how they were allocated.
trim := func(s unix.CPUSetDynamic) unix.CPUSetDynamic {
for len(s) > 0 && s[len(s)-1] == 0 {
s = s[:len(s)-1]
}
return s
}
testCases := []struct {
in string
out *unix.CPUSet
out unix.CPUSetDynamic
isErr bool
}{
{in: ""}, // Empty means unset.
// Valid cases.
{in: "0", out: &unix.CPUSet{1}},
{in: "1", out: &unix.CPUSet{2}},
{in: "0-1", out: &unix.CPUSet{3}},
{in: "0,1", out: &unix.CPUSet{3}},
{in: ",0,1,", out: &unix.CPUSet{3}},
{in: "0-3", out: &unix.CPUSet{0x0f}},
{in: "0,1,2-3", out: &unix.CPUSet{0x0f}},
{in: "4-7", out: &unix.CPUSet{0xf0}},
{in: "0-7", out: &unix.CPUSet{0xff}},
{in: "0-15", out: &unix.CPUSet{0xffff}},
{in: "16", out: &unix.CPUSet{0x10000}},
{in: "0", out: set(0)},
{in: "1", out: set(1)},
{in: "0-1", out: set(0, 1)},
{in: "0,1", out: set(0, 1)},
{in: ",0,1,", out: set(0, 1)},
{in: "0-3", out: set(0, 1, 2, 3)},
{in: "0,1,2-3", out: set(0, 1, 2, 3)},
{in: "4-7", out: set(4, 5, 6, 7)},
{in: "0-7", out: set(0, 1, 2, 3, 4, 5, 6, 7)},
{in: "0-15", out: set(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)},
{in: "16", out: set(16)},
// Extra whitespace in between ranges are OK.
{in: "1, 2, 1-2", out: &unix.CPUSet{6}},
{in: " , 1 , 3 , 5-7, ", out: &unix.CPUSet{0xea}},
// Somewhat large values. The underlying type in unix.CPUSet
// can either be uint32 or uint64, so we have to use a helper.
{in: "1, 2, 1-2", out: set(1, 2)},
{in: " , 1 , 3 , 5-7, ", out: set(1, 3, 5, 6, 7)},
// Somewhat large values.
{in: "0-3,32-33", out: set(0, 1, 2, 3, 32, 33)},
{in: "127-129, 1", out: set(1, 127, 128, 129)},
{in: "1023", out: set(1023)},
// Values larger than what a (non-dynamic) unix.CPUSet can hold.
{in: "1024", out: set(1024)},
{in: "8191", out: set(8191)},
{in: "4096-4098", out: set(4096, 4097, 4098)},
{in: "0,65536", out: set(0, 65536)},
// Maximum allowed value.
{in: "65536", out: set(65536)},
// Error cases.
{in: "-", isErr: true},
@@ -53,7 +73,9 @@ func TestToCPUSet(t *testing.T) {
{in: "54-53", isErr: true},
// Extra spaces inside a range is not OK.
{in: "1 - 2", isErr: true},
{in: "1024", isErr: true}, // Too big for unix.CPUSet.
// Larger than the maximum supported value.
{in: "65537", isErr: true},
{in: "0-65537", isErr: true},
}
for _, tc := range testCases {
@@ -80,7 +102,7 @@ func TestToCPUSet(t *testing.T) {
if out == nil {
t.Fatalf("want %v, got nil", tc.out)
}
if *out != *tc.out {
if !slices.Equal(trim(out), trim(tc.out)) {
t.Errorf("case %q: want %v, got %v", tc.in, tc.out, out)
}
})

View File

@@ -1,7 +1,6 @@
package libcontainer
import (
"bytes"
"context"
"encoding/json"
"errors"
@@ -203,8 +202,8 @@ func tryResetCPUAffinity(pid int) {
// /sys/devices/system/cpu/possible and kernel_max.
// Instead, we use a huge buffer similarly to go 1.25 runtime in
// getCPUCount().
const maxCPUs = 64 * 1024
buf := bytes.Repeat([]byte{0xff}, maxCPUs/8)
buf := unix.NewCPUSet(configs.MaxCPU)
buf.Fill()
if err := linux.SchedSetaffinity(pid, buf); err != nil {
logrus.WithError(err).Warnf("resetting the CPU affinity of pid %d failed -- the container process may inherit runc's CPU affinity", pid)
return
@@ -224,7 +223,7 @@ func (p *setnsProcess) startWithCPUAffinity() error {
go func() {
runtime.LockOSThread()
// Command inherits the CPU affinity.
if err := unix.SchedSetaffinity(unix.Gettid(), aff.Initial); err != nil {
if err := linux.SchedSetaffinity(unix.Gettid(), aff.Initial); err != nil {
errCh <- fmt.Errorf("error setting initial CPU affinity: %w", err)
return
}
@@ -250,7 +249,7 @@ func (p *setnsProcess) setFinalCPUAffinity() error {
if aff.Final == nil {
return nil
}
if err := unix.SchedSetaffinity(p.pid(), aff.Final); err != nil {
if err := linux.SchedSetaffinity(p.pid(), aff.Final); err != nil {
return fmt.Errorf("error setting final CPU affinity: %w", err)
}
return nil