runc exec: implement CPU affinity

As per
- https://github.com/opencontainers/runtime-spec/pull/1253
- https://github.com/opencontainers/runtime-spec/pull/1261

CPU affinity can be set in two ways:
1. When creating/starting a container, in config.json's
   Process.ExecCPUAffinity, which is when applied to all execs.
2. When running an exec, in process.json's CPUAffinity, which
   applied to a given exec and overrides the value from (1).

Add some basic tests.

Note that older kernels (RHEL8, Ubuntu 20.04) change CPU affinity of a
process to that of a container's cgroup, as soon as it is moved to that
cgroup, while newer kernels (Ubuntu 24.04, Fedora 41) don't do that.

Because of the above,
 - it's impossible to really test initial CPU affinity without adding
   debug logging to libcontainer/nsenter;
 - for older kernels, there can be a brief moment when exec's affinity
   is different than either initial or final affinity being set;
 - exec's final CPU affinity, if not specified, can be different
   depending on the kernel, therefore we don't test it.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
This commit is contained in:
Kir Kolyshkin
2024-10-21 15:50:38 -07:00
parent 701516b57a
commit 10ca66bff5
13 changed files with 389 additions and 5 deletions
+46 -3
View File
@@ -163,13 +163,52 @@ type setnsProcess struct {
initProcessPid int
}
// Starts setns process with specified initial CPU affinity.
func (p *setnsProcess) startWithCPUAffinity() error {
aff := p.config.CPUAffinity
if aff == nil || aff.Initial == nil {
return p.cmd.Start()
}
errCh := make(chan error)
defer close(errCh)
// Use a goroutine to dedicate an OS thread.
go func() {
runtime.LockOSThread()
// Command inherits the CPU affinity.
if err := unix.SchedSetaffinity(unix.Gettid(), aff.Initial); err != nil {
errCh <- fmt.Errorf("error setting initial CPU affinity: %w", err)
return
}
errCh <- p.cmd.Start()
// Deliberately omit runtime.UnlockOSThread here.
// https://pkg.go.dev/runtime#LockOSThread says:
// "If the calling goroutine exits without unlocking the
// thread, the thread will be terminated".
}()
return <-errCh
}
func (p *setnsProcess) setFinalCPUAffinity() error {
aff := p.config.CPUAffinity
if aff == nil || aff.Final == nil {
return nil
}
if err := unix.SchedSetaffinity(p.pid(), aff.Final); err != nil {
return fmt.Errorf("error setting final CPU affinity: %w", err)
}
return nil
}
func (p *setnsProcess) start() (retErr error) {
defer p.comm.closeParent()
// get the "before" value of oom kill count
// Get the "before" value of oom kill count.
oom, _ := p.manager.OOMKillCount()
err := p.cmd.Start()
// close the child-side of the pipes (controlled by child)
err := p.startWithCPUAffinity()
// Close the child-side of the pipes (controlled by child).
p.comm.closeChild()
if err != nil {
return fmt.Errorf("error starting setns process: %w", err)
@@ -219,6 +258,10 @@ func (p *setnsProcess) start() (retErr error) {
}
}
}
// Set final CPU affinity right after the process is moved into container's cgroup.
if err := p.setFinalCPUAffinity(); err != nil {
return err
}
if p.intelRdtPath != "" {
// if Intel RDT "resource control" filesystem path exists
_, err := os.Stat(p.intelRdtPath)