Skip to content

Commit f850fd5

Browse files
committed
runc exec: implement --cgroup
TODO: - commit message (this one); - tests for cgroup v1; - yet moar tests; - ... - PROFIT! Signed-off-by: Kir Kolyshkin <[email protected]>
1 parent 5ad7c79 commit f850fd5

File tree

7 files changed

+96
-3
lines changed

7 files changed

+96
-3
lines changed

exec.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ following will output a list of processes running in the container:
8989
Name: "preserve-fds",
9090
Usage: "Pass N additional file descriptors to the container (stdio + $LISTEN_FDS + N in total)",
9191
},
92+
cli.StringFlag{
93+
Name: "cgroup",
94+
Usage: "run the process in an (existing) sub-cgroup",
95+
},
9296
},
9397
Action: func(context *cli.Context) error {
9498
if err := checkArgs(context, 1, minArgs); err != nil {
@@ -148,6 +152,7 @@ func execProcess(context *cli.Context) (int, error) {
148152
init: false,
149153
preserveFDs: context.Int("preserve-fds"),
150154
logLevel: logLevel,
155+
cgroup: context.String("cgroup"),
151156
}
152157
return r.run(p)
153158
}

libcontainer/container_linux.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"net"
1313
"os"
1414
"os/exec"
15+
"path"
1516
"path/filepath"
1617
"reflect"
1718
"strconv"
@@ -555,7 +556,7 @@ func (c *linuxContainer) newSetnsProcess(p *Process, cmd *exec.Cmd, messageSockP
555556
if err != nil {
556557
return nil, err
557558
}
558-
return &setnsProcess{
559+
proc := &setnsProcess{
559560
cmd: cmd,
560561
cgroupPaths: state.CgroupPaths,
561562
rootlessCgroups: c.config.RootlessCgroups,
@@ -567,7 +568,16 @@ func (c *linuxContainer) newSetnsProcess(p *Process, cmd *exec.Cmd, messageSockP
567568
process: p,
568569
bootstrapData: data,
569570
initProcessPid: state.InitProcessPid,
570-
}, nil
571+
}
572+
if p.Cgroup != "" {
573+
for k := range proc.cgroupPaths {
574+
proc.cgroupPaths[k] = path.Join(proc.cgroupPaths[k], p.Cgroup)
575+
}
576+
// Do not try to join init process's cgroup as a fallback
577+
// (see (*setnsProcess).start).
578+
proc.initProcessPid = 0
579+
}
580+
return proc, nil
571581
}
572582

573583
func (c *linuxContainer) newInitConfig(process *Process) *initConfig {

libcontainer/process.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ type Process struct {
8080
ops processOperations
8181

8282
LogLevel string
83+
84+
// Cgroup specifies a (sub-cgroup of a container) to run the process in.
85+
// If empty, the default top-level container's cgroup is used.
86+
Cgroup string
8387
}
8488

8589
// Wait waits for the process to exit.

libcontainer/process_linux.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func (p *setnsProcess) start() (retErr error) {
134134
// On cgroup v2 + nesting + domain controllers, WriteCgroupProc may fail with EBUSY.
135135
// https://github.com/opencontainers/runc/issues/2356#issuecomment-621277643
136136
// Try to join the cgroup of InitProcessPid.
137-
if cgroups.IsCgroup2UnifiedMode() {
137+
if cgroups.IsCgroup2UnifiedMode() && p.initProcessPid != 0 {
138138
initProcCgroupFile := fmt.Sprintf("/proc/%d/cgroup", p.initProcessPid)
139139
initCg, initCgErr := cgroups.ParseCgroupFile(initProcCgroupFile)
140140
if initCgErr == nil {

man/runc-exec.8.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ multiple times.
5959
: Pass _N_ additional file descriptors to the container (**stdio** +
6060
**$LISTEN_FDS** + _N_ in total). Default is **0**.
6161

62+
**--cgroup** _path_
63+
: Execute a process in a sub-cgroup. If the specified cgroup does not exist, an
64+
error is returned. Default is empty path, which means to use container's top
65+
level cgroup. Note that for cgroup v2, by default if **runc exec** can't join
66+
the top level cgroup, it falls back to joining whatever cgroup container's init
67+
is in. This fallback can be disabled by using **--cgroup /**.
68+
6269
# EXAMPLES
6370
If the container can run **ps**(1) command, the following
6471
will output a list of processes running in the container:

tests/integration/exec.bats

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,68 @@ function check_exec_debug() {
167167
[[ "${output}" == *"level=debug"* ]]
168168
check_exec_debug "$output"
169169
}
170+
171+
@test "runc exec --cgroup subcgroup [v2]" {
172+
requires root cgroups_v2
173+
174+
set_cgroups_path
175+
set_cgroup_mount_writable
176+
177+
__runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox
178+
testcontainer test_busybox running
179+
180+
# Check we can't join non-existing subcgroup.
181+
runc exec --cgroup nonexistent test_busybox cat /proc/self/cgroup
182+
[ "$status" -ne 0 ]
183+
[[ "$output" == *" adding pid "*"/nonexistent/cgroup.procs: no such file "* ]]
184+
185+
# Check we can join top-level cgroup (implicit).
186+
runc exec test_busybox grep '^0::/$' /proc/self/cgroup
187+
[ "$status" -eq 0 ]
188+
189+
# Check we can join top-level cgroup (explicit).
190+
runc exec --cgroup / test_busybox grep '^0::/$' /proc/self/cgroup
191+
[ "$status" -eq 0 ]
192+
193+
# Now move "init" to a subcgroup, and check it was moved.
194+
runc exec test_busybox sh -euc "mkdir /sys/fs/cgroup/foobar \
195+
&& echo 1 > /sys/fs/cgroup/foobar/cgroup.procs \
196+
&& grep -w foobar /proc/1/cgroup"
197+
[ "$status" -eq 0 ]
198+
199+
# The following part is taken from
200+
# @test "runc exec (cgroup v2 + init process in non-root cgroup) succeeds"
201+
202+
# The init process is now in "/foo", but an exec process can still
203+
# join "/" because we haven't enabled any domain controller yet.
204+
runc exec test_busybox grep '^0::/$' /proc/self/cgroup
205+
[ "$status" -eq 0 ]
206+
207+
# Turn on a domain controller (memory).
208+
runc exec test_busybox sh -euc 'echo $$ > /sys/fs/cgroup/foobar/cgroup.procs; echo +memory > /sys/fs/cgroup/cgroup.subtree_control'
209+
[ "$status" -eq 0 ]
210+
211+
# An exec process can no longer join "/" after turning on a domain
212+
# controller. Check that cgroup v2 fallback to init cgroup works.
213+
runc exec test_busybox sh -euc "cat /proc/self/cgroup && grep '^0::/foobar$' /proc/self/cgroup"
214+
[ "$status" -eq 0 ]
215+
216+
# Check that --cgroup / disables the init cgroup fallback.
217+
runc exec --cgroup / test_busybox true
218+
[ "$status" -ne 0 ]
219+
[[ "$output" == *" adding pid "*" to cgroups"*"/cgroup.procs: device or resource busy"* ]]
220+
221+
# Check that explicit --cgroup foobar works.
222+
runc exec --cgroup foobar test_busybox grep '^0::/foobar$' /proc/self/cgroup
223+
[ "$status" -eq 0 ]
224+
225+
# Check all processes is in foobar (this check is redundant).
226+
runc exec --cgroup foobar test_busybox sh -euc '! grep -vwH foobar /proc/*/cgroup'
227+
[ "$status" -eq 0 ]
228+
229+
# Add a second subcgroup, check we're in it.
230+
runc exec --cgroup foobar test_busybox mkdir /sys/fs/cgroup/second
231+
[ "$status" -eq 0 ]
232+
runc exec --cgroup second test_busybox grep -w second /proc/self/cgroup
233+
[ "$status" -eq 0 ]
234+
}

utils_linux.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ type runner struct {
258258
notifySocket *notifySocket
259259
criuOpts *libcontainer.CriuOpts
260260
logLevel string
261+
cgroup string
261262
}
262263

263264
func (r *runner) run(config *specs.Process) (int, error) {
@@ -277,6 +278,7 @@ func (r *runner) run(config *specs.Process) (int, error) {
277278
// Populate the fields that come from runner.
278279
process.Init = r.init
279280
process.LogLevel = r.logLevel
281+
process.Cgroup = r.cgroup
280282
if len(r.listenFDs) > 0 {
281283
process.Env = append(process.Env, "LISTEN_FDS="+strconv.Itoa(len(r.listenFDs)), "LISTEN_PID=1")
282284
process.ExtraFiles = append(process.ExtraFiles, r.listenFDs...)

0 commit comments

Comments
 (0)