Skip to content

Commit

Permalink
Merge pull request #263 from dcantah/cg2_kill
Browse files Browse the repository at this point in the history
cgroup2: Add Kill method to manager
  • Loading branch information
estesp authored Jan 3, 2023
2 parents e91481e + a8b169a commit fc3b020
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 0 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,19 @@ if err != nil {
}
```

### Kill all processes in a cgroup

```go
m, err := cgroup2.LoadSystemd("/", "my-cgroup-abc.slice")
if err != nil {
return err
}
err = m.Kill()
if err != nil {
return err
}
```

### Attention

All static path should not include `/sys/fs/cgroup/` prefix, it should start with your own cgroups name
Expand Down
81 changes: 81 additions & 0 deletions cgroup2/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
const (
subtreeControl = "cgroup.subtree_control"
controllersFile = "cgroup.controllers"
killFile = "cgroup.kill"
defaultCgroup2Path = "/sys/fs/cgroup"
defaultSlice = "system.slice"
)
Expand Down Expand Up @@ -357,6 +358,86 @@ func (c *Manager) AddThread(tid uint64) error {
return writeValues(c.path, []Value{v})
}

// Kill will try to forcibly exit all of the processes in the cgroup. This is
// equivalent to sending a SIGKILL to every process. On kernels 5.14 and greater
// this will use the cgroup.kill file, on anything that doesn't have the cgroup.kill
// file, a manual process of freezing -> sending a SIGKILL to every process -> thawing
// will be used.
func (c *Manager) Kill() error {
v := Value{
filename: killFile,
value: "1",
}
err := writeValues(c.path, []Value{v})
if err == nil {
return nil
}
logrus.Warnf("falling back to slower kill implementation: %s", err)
// Fallback to slow method.
return c.fallbackKill()
}

// fallbackKill is a slower fallback to the more modern (kernels 5.14+)
// approach of writing to the cgroup.kill file. This is heavily pulled
// from runc's same approach (in signalAllProcesses), with the only differences
// being this is just tailored to the API exposed in this library, and we don't
// need to care about signals other than SIGKILL.
//
// https://github.com/opencontainers/runc/blob/8da0a0b5675764feaaaaad466f6567a9983fcd08/libcontainer/init_linux.go#L523-L529
func (c *Manager) fallbackKill() error {
if err := c.Freeze(); err != nil {
logrus.Warn(err)
}
pids, err := c.Procs(true)
if err != nil {
if err := c.Thaw(); err != nil {
logrus.Warn(err)
}
return err
}
var procs []*os.Process
for _, pid := range pids {
p, err := os.FindProcess(int(pid))
if err != nil {
logrus.Warn(err)
continue
}
procs = append(procs, p)
if err := p.Signal(unix.SIGKILL); err != nil {
logrus.Warn(err)
}
}
if err := c.Thaw(); err != nil {
logrus.Warn(err)
}

subreaper, err := getSubreaper()
if err != nil {
// The error here means that PR_GET_CHILD_SUBREAPER is not
// supported because this code might run on a kernel older
// than 3.4. We don't want to throw an error in that case,
// and we simplify things, considering there is no subreaper
// set.
subreaper = 0
}

for _, p := range procs {
// In case a subreaper has been setup, this code must not
// wait for the process. Otherwise, we cannot be sure the
// current process will be reaped by the subreaper, while
// the subreaper might be waiting for this process in order
// to retrieve its exit code.
if subreaper == 0 {
if _, err := p.Wait(); err != nil {
if !errors.Is(err, unix.ECHILD) {
logrus.Warnf("wait on pid %d failed: %s", p.Pid, err)
}
}
}
}
return nil
}

func (c *Manager) Delete() error {
// kernel prevents cgroups with running process from being removed, check the tree is empty
processes, err := c.Procs(true)
Expand Down
48 changes: 48 additions & 0 deletions cgroup2/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,54 @@ func TestSystemdFullPath(t *testing.T) {
}
}

func TestKill(t *testing.T) {
checkCgroupMode(t)
manager, err := NewManager(defaultCgroup2Path, "/test1", ToResources(&specs.LinuxResources{}))
if err != nil {
t.Fatal(err)
}
var procs []*exec.Cmd
for i := 0; i < 5; i++ {
cmd := exec.Command("sleep", "infinity")
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
if cmd.Process == nil {
t.Fatal("Process is nil")
}
if err := manager.AddProc(uint64(cmd.Process.Pid)); err != nil {
t.Fatal(err)
}
procs = append(procs, cmd)
}
// Verify we have 5 pids before beginning Kill below.
pids, err := manager.Procs(true)
if err != nil {
t.Fatal(err)
}
if len(pids) != 5 {
t.Fatalf("expected 5 pids, got %d", len(pids))
}
// Now run kill, and check that nothing is running after.
if err := manager.Kill(); err != nil {
t.Fatal(err)
}

done := make(chan struct{})
go func() {
for _, proc := range procs {
_ = proc.Wait()
}
done <- struct{}{}
}()

select {
case <-time.After(time.Second * 3):
t.Fatal("timed out waiting for processes to exit")
case <-done:
}
}

func TestMoveTo(t *testing.T) {
checkCgroupMode(t)
manager, err := NewManager(defaultCgroup2Path, "/test1", ToResources(&specs.LinuxResources{}))
Expand Down
10 changes: 10 additions & 0 deletions cgroup2/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ import (
"strconv"
"strings"
"time"
"unsafe"

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

"github.com/godbus/dbus/v5"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)

const (
Expand Down Expand Up @@ -434,3 +436,11 @@ func readHugeTlbStats(path string) []*stats.HugeTlbStat {
}
return usage
}

func getSubreaper() (int, error) {
var i uintptr
if err := unix.Prctl(unix.PR_GET_CHILD_SUBREAPER, uintptr(unsafe.Pointer(&i)), 0, 0, 0); err != nil {
return -1, err
}
return int(i), nil
}

0 comments on commit fc3b020

Please sign in to comment.