Skip to content

Commit 02dc5e9

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 f07b578 commit 02dc5e9

File tree

9 files changed

+218
-4
lines changed

9 files changed

+218
-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 [CPUAffinity] (initial or final) value 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
@@ -72,6 +72,7 @@ type initConfig struct {
7272
RootlessCgroups bool `json:"rootless_cgroups,omitempty"`
7373
SpecState *specs.State `json:"spec_state,omitempty"`
7474
Cgroup2Path string `json:"cgroup2_path,omitempty"`
75+
CPUAffinity *specs.CPUAffinity `json:"cpu_affinity,omitempty"`
7576
}
7677

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

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

+55-3
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,62 @@ 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+
cpus, err := configs.ToCPUSet(aff.Initial)
173+
if err != nil {
174+
return fmt.Errorf("invalid CPUAffinity.initial: %w", err)
175+
}
176+
177+
errCh := make(chan error)
178+
defer close(errCh)
179+
180+
// Use a goroutine to dedicate an OS thread.
181+
go func() {
182+
runtime.LockOSThread()
183+
// Command inherits the CPU affinity.
184+
if err := unix.SchedSetaffinity(unix.Gettid(), cpus); err != nil {
185+
runtime.UnlockOSThread()
186+
errCh <- fmt.Errorf("setting initial CPU affinity: %w", err)
187+
return
188+
}
189+
190+
errCh <- p.cmd.Start()
191+
// Deliberately omit runtime.UnlockOSThread here.
192+
// https://pkg.go.dev/runtime#LockOSThread says:
193+
// "If the calling goroutine exits without unlocking the
194+
// thread, the thread will be terminated".
195+
}()
196+
197+
return <-errCh
198+
}
199+
200+
func (p *setnsProcess) setFinalCPUAffinity() error {
201+
aff := p.config.CPUAffinity
202+
if aff == nil || aff.Final == "" {
203+
return nil
204+
}
205+
cpus, err := configs.ToCPUSet(aff.Final)
206+
if err != nil {
207+
return fmt.Errorf("invalid CPUAffinity.final: %w", err)
208+
}
209+
if err := unix.SchedSetaffinity(p.pid(), cpus); err != nil {
210+
return fmt.Errorf("setting final CPU affinity: %w", err)
211+
}
212+
return nil
213+
}
214+
166215
func (p *setnsProcess) start() (retErr error) {
167216
defer p.comm.closeParent()
168217

169-
// get the "before" value of oom kill count
218+
// Get the "before" value of oom kill count.
170219
oom, _ := p.manager.OOMKillCount()
171-
err := p.cmd.Start()
172-
// close the child-side of the pipes (controlled by child)
220+
err := p.startWithCPUAffinity()
221+
// Close the child-side of the pipes (controlled by child).
173222
p.comm.closeChild()
174223
if err != nil {
175224
return fmt.Errorf("error starting setns process: %w", err)
@@ -228,6 +277,9 @@ func (p *setnsProcess) start() (retErr error) {
228277
}
229278
}
230279
}
280+
if err := p.setFinalCPUAffinity(); err != nil {
281+
return err
282+
}
231283

232284
if err := utils.WriteJSON(p.comm.initSockParent, p.config); err != nil {
233285
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

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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, initial set via process.json]" {
47+
first="$(first_cpu)"
48+
second=$((first+1)) # Hacky; might not work in all environments.
49+
50+
runc run -d --console-socket "$CONSOLE_SOCKET" ct1
51+
[ "$status" -eq 0 ]
52+
53+
for cpus in "$first" "$first-$second" "$first,$second" "$second"; do
54+
proc='
55+
{
56+
"terminal": false,
57+
"execCPUAffinity": {
58+
"initial": "'$cpus'"
59+
},
60+
"args": [ "/bin/true" ],
61+
"cwd": "/"
62+
}'
63+
exp=${cpus//,/-} # 1. "," --> "-".
64+
exp=${exp//-/ } # 2. "-" --> " ".
65+
echo "CPUS: $cpus, exp: $exp"
66+
runc --debug exec --process <(echo "$proc") ct1
67+
[[ "$output" == *"Initial CPUs: [$exp]"* ]]
68+
done
69+
}
70+
71+
@test "runc exec [CPU affinity, initial and final are set]" {
72+
first="$(first_cpu)"
73+
second=$((first+1)) # Hacky; might not work in all environments.
74+
75+
update_config " .process.execCPUAffinity.initial = \"$first\"
76+
| .process.execCPUAffinity.final = \"$second\""
77+
78+
runc run -d --console-socket "$CONSOLE_SOCKET" ct1
79+
[ "$status" -eq 0 ]
80+
81+
runc --debug exec ct1 grep "Cpus_allowed_list:" /proc/self/status
82+
[ "$status" -eq 0 ]
83+
[[ "$output" == *"Initial CPUs: [$first]"* ]]
84+
[[ "$output}" == *"Cpus_allowed_list: $second"* ]] # Mind the literal tab.
85+
}

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)