Skip to content

Commit 1f296a7

Browse files
committed
runc exec: implement CPU affinity
As per - opencontainers/runtime-spec#1253 - opencontainers/runtime-spec#1261 Add some tests (alas it's impossible to test initial CPU affinity without adding debug logging). Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
1 parent 6c749bb commit 1f296a7

File tree

9 files changed

+198
-4
lines changed

9 files changed

+198
-4
lines changed

libcontainer/configs/config.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,9 @@ type Config struct {
225225

226226
// IOPriority is the container's I/O priority.
227227
IOPriority *IOPriority `json:"io_priority,omitempty"`
228+
229+
// ExecCPUAffinity is CPU affinity for a non-init process to be run in the container.
230+
ExecCPUAffinity *CPUAffinity `json:"exec_cpu_affinity,omitempty"`
228231
}
229232

230233
// Scheduler is based on the Linux sched_setattr(2) syscall.
@@ -286,7 +289,10 @@ func ToSchedAttr(scheduler *Scheduler) (*unix.SchedAttr, error) {
286289
}, nil
287290
}
288291

289-
type IOPriority = specs.LinuxIOPriority
292+
type (
293+
IOPriority = specs.LinuxIOPriority
294+
CPUAffinity = specs.CPUAffinity
295+
)
290296

291297
type (
292298
HookName string

libcontainer/configs/config_linux.go

+41
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import (
44
"errors"
55
"fmt"
66
"math"
7+
"strconv"
8+
"strings"
9+
10+
"golang.org/x/sys/unix"
711
)
812

913
var (
@@ -95,3 +99,40 @@ func (c Config) hostIDFromMapping(containerID int64, uMap []IDMap) (int64, bool)
9599
}
96100
return -1, false
97101
}
102+
103+
// ToCPUSet converts a [CPUAffinity] field (initial or final) to [unix.CPUSet].
104+
func ToCPUSet(str string) (*unix.CPUSet, error) {
105+
s := new(unix.CPUSet)
106+
for _, r := range strings.Split(str, ",") {
107+
// Allow extra spaces around.
108+
r = strings.TrimSpace(r)
109+
// Allow empty elements (extra commas).
110+
if r == "" {
111+
continue
112+
}
113+
if r0, r1, found := strings.Cut(r, "-"); found {
114+
start, err := strconv.ParseUint(r0, 10, 32)
115+
if err != nil {
116+
return nil, err
117+
}
118+
end, err := strconv.ParseUint(r1, 10, 32)
119+
if err != nil {
120+
return nil, err
121+
}
122+
if start > end {
123+
return nil, errors.New("invalid range: " + r)
124+
}
125+
for i := int(start); i <= int(end); i++ {
126+
s.Set(i)
127+
}
128+
} else {
129+
val, err := strconv.ParseUint(r, 10, 32)
130+
if err != nil {
131+
return nil, err
132+
}
133+
s.Set(int(val))
134+
}
135+
}
136+
137+
return s, nil
138+
}

libcontainer/container_linux.go

+4
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,7 @@ func (c *Container) newInitConfig(process *Process) *initConfig {
697697
AppArmorProfile: c.config.AppArmorProfile,
698698
ProcessLabel: c.config.ProcessLabel,
699699
Rlimits: c.config.Rlimits,
700+
CPUAffinity: c.config.ExecCPUAffinity,
700701
CreateConsole: process.ConsoleSocket != nil,
701702
ConsoleWidth: process.ConsoleWidth,
702703
ConsoleHeight: process.ConsoleHeight,
@@ -713,6 +714,9 @@ func (c *Container) newInitConfig(process *Process) *initConfig {
713714
if len(process.Rlimits) > 0 {
714715
cfg.Rlimits = process.Rlimits
715716
}
717+
if process.CPUAffinity != nil {
718+
cfg.CPUAffinity = process.CPUAffinity
719+
}
716720
if cgroups.IsCgroup2UnifiedMode() {
717721
cfg.Cgroup2Path = c.cgroupManager.Path("")
718722
}

libcontainer/init_linux.go

+18
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ type initConfig struct {
7171
RootlessCgroups bool `json:"rootless_cgroups,omitempty"`
7272
SpecState *specs.State `json:"spec_state,omitempty"`
7373
Cgroup2Path string `json:"cgroup2_path,omitempty"`
74+
CPUAffinity *specs.CPUAffinity `json:"cpu_affinity,omitempty"`
7475
}
7576

7677
// Init is part of "runc init" implementation.
@@ -198,6 +199,23 @@ func startInitialization() (retErr error) {
198199
}
199200
}()
200201

202+
// See tests/integration/cpu_affinity.bats.
203+
if logrus.GetLevel() >= logrus.DebugLevel {
204+
var cpus unix.CPUSet
205+
err := unix.SchedGetaffinity(0, &cpus)
206+
if err != nil {
207+
logrus.Debugf("sched_getaffinity: error %v", err)
208+
} else {
209+
var list []int
210+
for i := 0; i < 256; i++ {
211+
if cpus.IsSet(i) {
212+
list = append(list, i)
213+
}
214+
}
215+
logrus.Debugf("Initial CPUs: %v", list)
216+
}
217+
}
218+
201219
var config initConfig
202220
if err := json.NewDecoder(initPipe).Decode(&config); err != nil {
203221
return err

libcontainer/process.go

+2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ type Process struct {
102102
Scheduler *configs.Scheduler
103103

104104
IOPriority *configs.IOPriority
105+
106+
CPUAffinity *configs.CPUAffinity
105107
}
106108

107109
// Wait waits for the process to exit.

libcontainer/process_linux.go

+56-3
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,63 @@ type setnsProcess struct {
163163
initProcessPid int
164164
}
165165

166+
// Starts setns process with specified initial CPU affinity.
167+
func (p *setnsProcess) startWithCPUAffinity() error {
168+
aff := p.config.CPUAffinity
169+
if aff == nil || aff.Initial == "" {
170+
return p.cmd.Start()
171+
}
172+
logrus.Debugf("Initial CPU affinity: %s", aff.Initial)
173+
cpus, err := configs.ToCPUSet(aff.Initial)
174+
if err != nil {
175+
return fmt.Errorf("invalid CPUAffinity.initial: %w", err)
176+
}
177+
178+
errCh := make(chan error)
179+
defer close(errCh)
180+
181+
// Use a goroutine to dedicate an OS thread.
182+
go func() {
183+
runtime.LockOSThread()
184+
// Command inherits the CPU affinity.
185+
if err := unix.SchedSetaffinity(unix.Gettid(), cpus); err != nil {
186+
runtime.UnlockOSThread()
187+
errCh <- fmt.Errorf("setting initial CPU affinity: %w", err)
188+
return
189+
}
190+
191+
errCh <- p.cmd.Start()
192+
// Deliberately omit runtime.UnlockOSThread here.
193+
// https://pkg.go.dev/runtime#LockOSThread says:
194+
// "If the calling goroutine exits without unlocking the
195+
// thread, the thread will be terminated".
196+
}()
197+
198+
return <-errCh
199+
}
200+
201+
func (p *setnsProcess) setFinalCPUAffinity() error {
202+
aff := p.config.CPUAffinity
203+
if aff == nil || aff.Final == "" {
204+
return nil
205+
}
206+
cpus, err := configs.ToCPUSet(aff.Final)
207+
if err != nil {
208+
return fmt.Errorf("invalid CPUAffinity.final: %w", err)
209+
}
210+
if err := unix.SchedSetaffinity(p.pid(), cpus); err != nil {
211+
return fmt.Errorf("setting final CPU affinity: %w", err)
212+
}
213+
return nil
214+
}
215+
166216
func (p *setnsProcess) start() (retErr error) {
167217
defer p.comm.closeParent()
168218

169-
// get the "before" value of oom kill count
219+
// Get the "before" value of oom kill count.
170220
oom, _ := p.manager.OOMKillCount()
171-
err := p.cmd.Start()
172-
// close the child-side of the pipes (controlled by child)
221+
err := p.startWithCPUAffinity()
222+
// Close the child-side of the pipes (controlled by child).
173223
p.comm.closeChild()
174224
if err != nil {
175225
return fmt.Errorf("error starting setns process: %w", err)
@@ -228,6 +278,9 @@ func (p *setnsProcess) start() (retErr error) {
228278
}
229279
}
230280
}
281+
if err := p.setFinalCPUAffinity(); err != nil {
282+
return err
283+
}
231284

232285
if err := utils.WriteJSON(p.comm.initSockParent, p.config); err != nil {
233286
return fmt.Errorf("error writing config to pipe: %w", err)

libcontainer/specconv/spec_linux.go

+5
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,11 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) {
556556
ioPriority := *spec.Process.IOPriority
557557
config.IOPriority = &ioPriority
558558
}
559+
if spec.Process.ExecCPUAffinity != nil {
560+
a := *spec.Process.ExecCPUAffinity
561+
config.ExecCPUAffinity = &a
562+
}
563+
559564
}
560565
createHooks(spec, config)
561566
config.Version = specs.Version

tests/integration/cpu_affinity.bats

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bats
2+
# Exec CPU affinity tests. For more details, see:
3+
# - https://github.com/opencontainers/runtime-spec/pull/1253
4+
5+
load helpers
6+
7+
function setup() {
8+
requires smp cgroups_cpuset
9+
setup_busybox
10+
}
11+
12+
function teardown() {
13+
teardown_bundle
14+
}
15+
16+
function all_cpus() {
17+
cat /sys/devices/system/cpu/online
18+
}
19+
20+
function first_cpu() {
21+
all_cpus | sed 's/[-,].*//g'
22+
}
23+
24+
@test "runc exec [CPU affinity, initial set via process.json]" {
25+
first="$(first_cpu)"
26+
second=$((first + 1)) # Hacky; might not work in all environments.
27+
28+
runc run -d --console-socket "$CONSOLE_SOCKET" ct1
29+
[ "$status" -eq 0 ]
30+
31+
for cpus in "$first" "$first-$second" "$first,$second" "$second"; do
32+
proc='
33+
{
34+
"terminal": false,
35+
"execCPUAffinity": {
36+
"initial": "'$cpus'"
37+
},
38+
"args": [ "/bin/true" ],
39+
"cwd": "/"
40+
}'
41+
exp=${cpus//,/-} # 1. "," --> "-".
42+
exp=${exp//-/ } # 2. "-" --> " ".
43+
echo "CPUS: $cpus, exp: $exp"
44+
runc --debug exec --process <(echo "$proc") ct1
45+
[[ "$output" == *"Initial CPU affinity: $cpus"* ]]
46+
[[ "$output" == *"Initial CPUs: [$exp]"* ]]
47+
done
48+
}
49+
50+
@test "runc exec [CPU affinity, initial and final are set]" {
51+
first="$(first_cpu)"
52+
second=$((first + 1)) # Hacky; might not work in all environments.
53+
54+
update_config " .process.execCPUAffinity.initial = \"$first\"
55+
| .process.execCPUAffinity.final = \"$second\""
56+
57+
runc run -d --console-socket "$CONSOLE_SOCKET" ct1
58+
[ "$status" -eq 0 ]
59+
60+
runc --debug exec ct1 grep "Cpus_allowed_list:" /proc/self/status
61+
[ "$status" -eq 0 ]
62+
[[ "$output" == *"Initial CPUs: [$first]"* ]]
63+
[[ "$output" == *"Cpus_allowed_list: $second"* ]] # Mind the literal tab.
64+
}

utils_linux.go

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func newProcess(p specs.Process) (*libcontainer.Process, error) {
5757
AppArmorProfile: p.ApparmorProfile,
5858
Scheduler: p.Scheduler,
5959
IOPriority: p.IOPriority,
60+
CPUAffinity: p.ExecCPUAffinity,
6061
}
6162

6263
if p.ConsoleSize != nil {

0 commit comments

Comments
 (0)