Skip to content

Commit a8b169a

Browse files
committed
cgroup2: Add Kill method to manager
This adds in support for killing all of the processes in a cgroup. In 5.14+ this is very simple, a cgroup.kill file exists that all you need to do is write "1" to https://lwn.net/Articles/855924/. On kernels prior, or if the file doesn't exist to be more pedantic to account for potential backports, I've taken the approach runc currently uses which is a manual process of freezing the cgroup -> sending a signal to all of the processes -> thawing the cgroup. This also adds in a simple test for this that should work on 5.15+ and prior kernels. Signed-off-by: Danny Canter <[email protected]>
1 parent e8802a1 commit a8b169a

File tree

4 files changed

+154
-1
lines changed

4 files changed

+154
-1
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,19 @@ if err != nil {
188188
}
189189
```
190190

191+
### Kill all processes in a cgroup
192+
193+
```go
194+
m, err := cgroup2.LoadSystemd("/", "my-cgroup-abc.slice")
195+
if err != nil {
196+
return err
197+
}
198+
err = m.Kill()
199+
if err != nil {
200+
return err
201+
}
202+
```
203+
191204
### Attention
192205

193206
All static path should not include `/sys/fs/cgroup/` prefix, it should start with your own cgroups name

cgroup2/manager.go

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
const (
4343
subtreeControl = "cgroup.subtree_control"
4444
controllersFile = "cgroup.controllers"
45+
killFile = "cgroup.kill"
4546
defaultCgroup2Path = "/sys/fs/cgroup"
4647
defaultSlice = "system.slice"
4748
)
@@ -366,6 +367,86 @@ func (c *Manager) AddThread(tid uint64) error {
366367
return writeValues(c.path, []Value{v})
367368
}
368369

370+
// Kill will try to forcibly exit all of the processes in the cgroup. This is
371+
// equivalent to sending a SIGKILL to every process. On kernels 5.14 and greater
372+
// this will use the cgroup.kill file, on anything that doesn't have the cgroup.kill
373+
// file, a manual process of freezing -> sending a SIGKILL to every process -> thawing
374+
// will be used.
375+
func (c *Manager) Kill() error {
376+
v := Value{
377+
filename: killFile,
378+
value: "1",
379+
}
380+
err := writeValues(c.path, []Value{v})
381+
if err == nil {
382+
return nil
383+
}
384+
logrus.Warnf("falling back to slower kill implementation: %s", err)
385+
// Fallback to slow method.
386+
return c.fallbackKill()
387+
}
388+
389+
// fallbackKill is a slower fallback to the more modern (kernels 5.14+)
390+
// approach of writing to the cgroup.kill file. This is heavily pulled
391+
// from runc's same approach (in signalAllProcesses), with the only differences
392+
// being this is just tailored to the API exposed in this library, and we don't
393+
// need to care about signals other than SIGKILL.
394+
//
395+
// https://github.com/opencontainers/runc/blob/8da0a0b5675764feaaaaad466f6567a9983fcd08/libcontainer/init_linux.go#L523-L529
396+
func (c *Manager) fallbackKill() error {
397+
if err := c.Freeze(); err != nil {
398+
logrus.Warn(err)
399+
}
400+
pids, err := c.Procs(true)
401+
if err != nil {
402+
if err := c.Thaw(); err != nil {
403+
logrus.Warn(err)
404+
}
405+
return err
406+
}
407+
var procs []*os.Process
408+
for _, pid := range pids {
409+
p, err := os.FindProcess(int(pid))
410+
if err != nil {
411+
logrus.Warn(err)
412+
continue
413+
}
414+
procs = append(procs, p)
415+
if err := p.Signal(unix.SIGKILL); err != nil {
416+
logrus.Warn(err)
417+
}
418+
}
419+
if err := c.Thaw(); err != nil {
420+
logrus.Warn(err)
421+
}
422+
423+
subreaper, err := getSubreaper()
424+
if err != nil {
425+
// The error here means that PR_GET_CHILD_SUBREAPER is not
426+
// supported because this code might run on a kernel older
427+
// than 3.4. We don't want to throw an error in that case,
428+
// and we simplify things, considering there is no subreaper
429+
// set.
430+
subreaper = 0
431+
}
432+
433+
for _, p := range procs {
434+
// In case a subreaper has been setup, this code must not
435+
// wait for the process. Otherwise, we cannot be sure the
436+
// current process will be reaped by the subreaper, while
437+
// the subreaper might be waiting for this process in order
438+
// to retrieve its exit code.
439+
if subreaper == 0 {
440+
if _, err := p.Wait(); err != nil {
441+
if !errors.Is(err, unix.ECHILD) {
442+
logrus.Warnf("wait on pid %d failed: %s", p.Pid, err)
443+
}
444+
}
445+
}
446+
}
447+
return nil
448+
}
449+
369450
func (c *Manager) Delete() error {
370451
// kernel prevents cgroups with running process from being removed, check the tree is empty
371452
processes, err := c.Procs(true)
@@ -763,7 +844,8 @@ func setDevices(path string, devices []specs.LinuxDeviceCgroup) error {
763844
// the reason this is necessary is because the "-" character has a special meaning in
764845
// systemd slice. For example, when creating a slice called "my-group-112233.slice",
765846
// systemd will create a hierarchy like this:
766-
// /sys/fs/cgroup/my.slice/my-group.slice/my-group-112233.slice
847+
//
848+
// /sys/fs/cgroup/my.slice/my-group.slice/my-group-112233.slice
767849
func getSystemdFullPath(slice, group string) string {
768850
return filepath.Join(defaultCgroup2Path, dashesToPath(slice), dashesToPath(group))
769851
}

cgroup2/manager_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,54 @@ func TestSystemdFullPath(t *testing.T) {
142142
}
143143
}
144144

145+
func TestKill(t *testing.T) {
146+
checkCgroupMode(t)
147+
manager, err := NewManager(defaultCgroup2Path, "/test1", ToResources(&specs.LinuxResources{}))
148+
if err != nil {
149+
t.Fatal(err)
150+
}
151+
var procs []*exec.Cmd
152+
for i := 0; i < 5; i++ {
153+
cmd := exec.Command("sleep", "infinity")
154+
if err := cmd.Start(); err != nil {
155+
t.Fatal(err)
156+
}
157+
if cmd.Process == nil {
158+
t.Fatal("Process is nil")
159+
}
160+
if err := manager.AddProc(uint64(cmd.Process.Pid)); err != nil {
161+
t.Fatal(err)
162+
}
163+
procs = append(procs, cmd)
164+
}
165+
// Verify we have 5 pids before beginning Kill below.
166+
pids, err := manager.Procs(true)
167+
if err != nil {
168+
t.Fatal(err)
169+
}
170+
if len(pids) != 5 {
171+
t.Fatalf("expected 5 pids, got %d", len(pids))
172+
}
173+
// Now run kill, and check that nothing is running after.
174+
if err := manager.Kill(); err != nil {
175+
t.Fatal(err)
176+
}
177+
178+
done := make(chan struct{})
179+
go func() {
180+
for _, proc := range procs {
181+
_ = proc.Wait()
182+
}
183+
done <- struct{}{}
184+
}()
185+
186+
select {
187+
case <-time.After(time.Second * 3):
188+
t.Fatal("timed out waiting for processes to exit")
189+
case <-done:
190+
}
191+
}
192+
145193
func TestMoveTo(t *testing.T) {
146194
checkCgroupMode(t)
147195
manager, err := NewManager(defaultCgroup2Path, "/test1", ToResources(&specs.LinuxResources{}))

cgroup2/utils.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ import (
2626
"strconv"
2727
"strings"
2828
"time"
29+
"unsafe"
2930

3031
"github.com/containerd/cgroups/v3/cgroup2/stats"
3132

3233
"github.com/godbus/dbus/v5"
3334
"github.com/opencontainers/runtime-spec/specs-go"
3435
"github.com/sirupsen/logrus"
36+
"golang.org/x/sys/unix"
3537
)
3638

3739
const (
@@ -434,3 +436,11 @@ func readHugeTlbStats(path string) []*stats.HugeTlbStat {
434436
}
435437
return usage
436438
}
439+
440+
func getSubreaper() (int, error) {
441+
var i uintptr
442+
if err := unix.Prctl(unix.PR_GET_CHILD_SUBREAPER, uintptr(unsafe.Pointer(&i)), 0, 0, 0); err != nil {
443+
return -1, err
444+
}
445+
return int(i), nil
446+
}

0 commit comments

Comments
 (0)