mirror of
https://github.com/opencontainers/runc.git
synced 2026-08-09 01:21:08 +00:00
Merge pull request #5343 from kolyshkin/cpu-aff-dyn
Remove 1024 CPUs limit
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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].
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user