Skip to content

Commit fc3b020

Browse files
authored
Merge pull request #263 from dcantah/cg2_kill
cgroup2: Add Kill method to manager
2 parents e91481e + a8b169a commit fc3b020

File tree

4 files changed

+152
-0
lines changed

4 files changed

+152
-0
lines changed

README.md

+13
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

+81
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
)
@@ -357,6 +358,86 @@ func (c *Manager) AddThread(tid uint64) error {
357358
return writeValues(c.path, []Value{v})
358359
}
359360

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

cgroup2/manager_test.go

+48
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

+10
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)