Source File
cgrouptest_linux.go
Belonging Package
internal/cgrouptest
// Copyright 2025 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.// Package cgrouptest provides best-effort helpers for running tests inside a// cgroup.package cgrouptestimport ()type CgroupV2 struct {orig stringpath string}func ( *CgroupV2) () string {return .path}// Path to cpu.max.func ( *CgroupV2) () string {return filepath.Join(.path, "cpu.max")}// Set cpu.max. Pass -1 for quota to disable the limit.func ( *CgroupV2) (, int64) error {:= "max"if >= 0 {= strconv.FormatInt(, 10)}:= fmt.Sprintf("%s %d", , )return os.WriteFile(.CPUMaxPath(), []byte(), 0)}// InCgroupV2 creates a new v2 cgroup, migrates the current process into it,// and then calls fn. When fn returns, the current process is migrated back to// the original cgroup and the new cgroup is destroyed.//// If a new cgroup cannot be created, the test is skipped.//// This must not be used in parallel tests, as it affects the entire process.func ( *testing.T, func(*CgroupV2)) {, := findCurrent():= findOwnedParent(, , ):= filepath.Join(, )// Make sure the parent allows children to control cpu., := os.ReadFile(filepath.Join(, "cgroup.subtree_control"))if != nil {.Skipf("unable to read cgroup.subtree_control: %v", )}if !slices.Contains(strings.Fields(string()), "cpu") {// N.B. We should have permission to add cpu to// subtree_control, but it seems like a bad idea to change this// on a high-level cgroup that probably has lots of existing// children..Skipf("Parent cgroup %s does not allow children to control cpu, only %q", , string())}, := os.MkdirTemp(, "go-cgrouptest")if != nil {.Skipf("unable to create cgroup directory: %v", )}// Important: defer cleanups so they run even in the event of panic.//// TODO(prattmic): Consider running everything in a subprocess just so// we can clean up if it throws or otherwise doesn't run the defers.defer func() {if := os.Remove(); != nil {// Not much we can do, but at least inform of the// problem..Errorf("Error removing cgroup directory: %v", )}}()migrateTo(, )defer migrateTo(, ):= &CgroupV2{orig: ,path: ,}()}// Returns the mount and relative directory of the current cgroup the process// is in.func findCurrent( *testing.T) (string, string) {// Find the path to our current CPU cgroup. Currently this package is// only used for CPU cgroup testing, so the distinction of different// controllers doesn't matter.var [cgroup.ParseSize]byte:= make([]byte, cgroup.PathSize), := cgroup.FindCPUMountPoint(, [:])if != nil {.Skipf("cgroup: unable to find current cgroup mount: %v", )}:= string([:]), , := cgroup.FindCPURelativePath(, [:])if != nil {.Skipf("cgroup: unable to find current cgroup path: %v", )}if != cgroup.V2 {.Skipf("cgroup: running on cgroup v%d want v2", )}:= string([1:]) // The returned path always starts with /, skip it.= filepath.Join(".", ) // Make sure this isn't empty string at root.return ,}// Returns a parent directory in which we can create our own cgroup subdirectory.func findOwnedParent( *testing.T, , string) string {// There are many ways cgroups may be set up on a system. We don't try// to cover all of them, just common ones.//// To start with, systemd://// Our test process is likely running inside a user session, in which// case we are likely inside a cgroup that looks something like://// /sys/fs/cgroup/user.slice/user-1234.slice/user@1234.service/vte-spawn-1.scope///// Possibly with additional slice layers between user@1234.service and// the leaf scope.//// On new enough kernel and systemd versions (exact versions unknown),// full unprivileged control of the user's cgroups is permitted// directly via the cgroup filesystem. Specifically, the// user@1234.service directory is owned by the user, as are all// subdirectories.// We want to create our own subdirectory that we can migrate into and// then manipulate at will. It is tempting to create a new subdirectory// inside the current cgroup we are already in, however that will likey// not work. cgroup v2 only allows processes to be in leaf cgroups. Our// current cgroup likely contains multiple processes (at least this one// and the cmd/go test runner). If we make a subdirectory and try to// move our process into that cgroup, then the subdirectory and parent// would both contain processes. Linux won't allow us to do that [1].//// Instead, we will simply walk up to the highest directory that our// user owns and create our new subdirectory. Since that directory// already has a bunch of subdirectories, it must not directly contain// and processes.//// (This would fall apart if we already in the highest directory we// own, such as if there was simply a single cgroup for the entire// user. Luckily systemd at least does not do this.)//// [1] Minor technicality: By default a new subdirectory has no cgroup// controller (they must be explicitly enabled in the parent's// cgroup.subtree_control). Linux will allow moving processes into a// subdirectory that has no controllers while there are still processes// in the parent, but it won't allow adding controller until the parent// is empty. As far as I tell, the only purpose of this is to allow// reorganizing processes into a new set of subdirectories and then// adding controllers once done., := os.OpenRoot()if != nil {.Fatalf("error opening cgroup mount root: %v", )}:= os.Getuid()var stringfor != "." {, := .Stat()if != nil {.Fatalf("error stating cgroup path: %v", )}:= .Sys().(*syscall.Stat_t)if int(.Uid) != {// Stop at first directory we don't own.break}== filepath.Join(, "..")}if == "" {.Skipf("No parent cgroup owned by UID %d", )}// We actually want the last directory where we were the owner.return filepath.Join(, )}// Migrate the current process to the cgroup directory dst.func migrateTo( *testing.T, string) {:= []byte(strconv.FormatInt(int64(os.Getpid()), 10))if := os.WriteFile(filepath.Join(, "cgroup.procs"), , 0); != nil {.Skipf("Unable to migrate into %s: %v", , )}}
![]() |
The pages are generated with Golds v0.7.9-preview. (GOOS=linux GOARCH=amd64) Golds is a Go 101 project developed by Tapir Liu. PR and bug reports are welcome and can be submitted to the issue list. Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds. |