Skip to content

Commit a1856a5

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 ed26953 commit a1856a5

File tree

4 files changed

+175
-3
lines changed

4 files changed

+175
-3
lines changed

libcontainer/configs/config.go

+3
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.

libcontainer/process_linux.go

+90-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"path/filepath"
1313
"runtime"
1414
"strconv"
15+
"strings"
1516
"sync"
1617
"time"
1718

@@ -122,13 +123,96 @@ func (p *setnsProcess) signal(sig os.Signal) error {
122123
return unix.Kill(p.pid(), s)
123124
}
124125

126+
func affToUnix(str string) (*unix.CPUSet, error) {
127+
s := new(unix.CPUSet)
128+
for _, r := range strings.Split(str, ",") {
129+
// Allow extra spaces around.
130+
r = strings.TrimSpace(r)
131+
// Allow empty elements (extra commas).
132+
if r == "" {
133+
continue
134+
}
135+
if r0, r1, found := strings.Cut(r, "-"); found {
136+
start, err := strconv.ParseUint(r0, 10, 32)
137+
if err != nil {
138+
return nil, err
139+
}
140+
end, err := strconv.ParseUint(r1, 10, 32)
141+
if err != nil {
142+
return nil, err
143+
}
144+
if start > end {
145+
return nil, errors.New("invalid range: " + r)
146+
}
147+
for i := int(start); i <= int(end); i++ {
148+
s.Set(i)
149+
}
150+
} else {
151+
val, err := strconv.ParseUint(r, 10, 32)
152+
if err != nil {
153+
return nil, err
154+
}
155+
s.Set(int(val))
156+
}
157+
}
158+
159+
return s, nil
160+
}
161+
162+
// Starts setns process with specified initial CPU affinity.
163+
func (p *setnsProcess) startWithCPUAffinity() error {
164+
aff := p.config.Config.ExecCPUAffinity
165+
if aff == nil || aff.Initial == "" {
166+
return p.cmd.Start()
167+
}
168+
cpus, err := affToUnix(aff.Initial)
169+
if err != nil {
170+
return fmt.Errorf("invalid execCPUAffinity.initial: %w", err)
171+
}
172+
173+
errCh := make(chan error)
174+
defer close(errCh)
175+
176+
// Use a goroutine to dedicate an OS thread.
177+
go func() {
178+
// Don't call runtime.UnlockOSThread to terminate the OS thread
179+
// when goroutine exits.
180+
runtime.LockOSThread()
181+
182+
// Command inherits the CPU affinity.
183+
if err := unix.SchedSetaffinity(unix.Gettid(), cpus); err != nil {
184+
errCh <- fmt.Errorf("setting initial CPU affinity: %w", err)
185+
return
186+
}
187+
188+
errCh <- p.cmd.Start()
189+
}()
190+
191+
return <-errCh
192+
}
193+
194+
func (p *setnsProcess) setFinalCPUAffinity() error {
195+
aff := p.config.Config.ExecCPUAffinity
196+
if aff == nil || aff.Final == "" {
197+
return nil
198+
}
199+
cpus, err := affToUnix(aff.Final)
200+
if err != nil {
201+
return fmt.Errorf("invalid execCPUAffinity.final: %w", err)
202+
}
203+
if err := unix.SchedSetaffinity(p.pid(), cpus); err != nil {
204+
return fmt.Errorf("setting final CPU affinity: %w", err)
205+
}
206+
return nil
207+
}
208+
125209
func (p *setnsProcess) start() (retErr error) {
126210
defer p.comm.closeParent()
127211

128-
// get the "before" value of oom kill count
212+
// Get the "before" value of oom kill count.
129213
oom, _ := p.manager.OOMKillCount()
130-
err := p.cmd.Start() // https://github.com/opencontainers/runc/pull/3923/commits/afc23e33971b657c4a09c54b16c6139651171aad
131-
// close the child-side of the pipes (controlled by child)
214+
err := p.startWithCPUAffinity()
215+
// Close the child-side of the pipes (controlled by child).
132216
p.comm.closeChild()
133217
if err != nil {
134218
return fmt.Errorf("error starting setns process: %w", err)
@@ -196,6 +280,9 @@ func (p *setnsProcess) start() (retErr error) {
196280
}
197281
}
198282
}
283+
if err := p.setFinalCPUAffinity(); err != nil {
284+
return err
285+
}
199286

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

libcontainer/specconv/spec_linux.go

+5
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,11 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) {
539539
ioPriority := *spec.Process.IOPriority
540540
config.IOPriority = &ioPriority
541541
}
542+
if spec.Process.ExecCPUAffinity != nil {
543+
a := *spec.Process.ExecCPUAffinity
544+
config.ExecCPUAffinity = &a
545+
}
546+
542547
}
543548
createHooks(spec, config)
544549
config.Version = specs.Version

tests/integration/cpu_affinity.bats

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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 inherited from runc]" {
25+
requires root # For taskset.
26+
27+
first="$(first_cpu)"
28+
29+
# Container's process CPU affinity is inherited from that of runc.
30+
taskset -p -c "$first" $$
31+
32+
runc run -d --console-socket "$CONSOLE_SOCKET" ct1
33+
[ "$status" -eq 0 ]
34+
35+
# Check init.
36+
runc exec ct1 grep "Cpus_allowed_list:" /proc/1/status
37+
[ "$status" -eq 0 ]
38+
[[ "${lines[0]}" == "Cpus_allowed_list: $first" ]]
39+
40+
# Check exec.
41+
runc exec ct1 grep "Cpus_allowed_list:" /proc/self/status
42+
[ "$status" -eq 0 ]
43+
[[ "${lines[0]}" == "Cpus_allowed_list: $first" ]]
44+
}
45+
46+
@test "runc exec [CPU affinity, only initial is set]" {
47+
requires root # For taskset.
48+
49+
first="$(first_cpu)"
50+
51+
update_config ".process.execCPUAffinity.initial = \"$first\""
52+
53+
runc run -d --console-socket "$CONSOLE_SOCKET" ct1
54+
[ "$status" -eq 0 ]
55+
56+
runc exec ct1 grep "Cpus_allowed_list:" /proc/self/status
57+
[ "$status" -eq 0 ]
58+
[[ "${lines[0]}" == "Cpus_allowed_list: $first" ]]
59+
}
60+
61+
@test "runc exec [CPU affinity, initial and final are set]" {
62+
requires root # For taskset.
63+
64+
first="$(first_cpu)"
65+
second=$((first+1)) # Hacky; might not work in all environments.
66+
67+
update_config " .process.execCPUAffinity.initial = \"$first\"
68+
| .process.execCPUAffinity.final = \"$second\""
69+
70+
taskset -p -c "$first" $$
71+
runc run -d --console-socket "$CONSOLE_SOCKET" ct1
72+
[ "$status" -eq 0 ]
73+
74+
runc exec ct1 grep "Cpus_allowed_list:" /proc/self/status
75+
[ "$status" -eq 0 ]
76+
[[ "${lines[0]}" == "Cpus_allowed_list: $second" ]]
77+
}

0 commit comments

Comments
 (0)