Source File
cgroup.go
Belonging Package
internal/runtime/cgroup
// 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 cgroupimport ()var (ErrNoCgroup error = stringError("not in a cgroup")errMalformedFile error = stringError("malformed file"))const _PATH_MAX = 4096const (// Required amount of scratch space for CPULimit.//// TODO(prattmic): This is shockingly large (~70KiB) due to the (very// unlikely) combination of extremely long paths consisting mostly// escaped characters. The scratch buffer ends up in .bss in package// runtime, so it doesn't contribute to binary size and generally won't// be faulted in, but it would still be nice to shrink this. A more// complex parser that did not need to keep entire lines in memory// could get away with much less. Alternatively, we could do a one-off// mmap allocation for this buffer, which is only mapped larger if we// actually need the extra space.ScratchSize = PathSize + ParseSize// Required space to store a path of the cgroup in the filesystem.PathSize = _PATH_MAX// /proc/self/mountinfo path escape sequences are 4 characters long, so// a path consisting entirely of escaped characters could be 4 times// larger.escapedPathMax = 4 * _PATH_MAX// Required space to parse /proc/self/mountinfo and /proc/self/cgroup.// See findCPUMount and findCPURelativePath.ParseSize = 4 * escapedPathMax)// Version indicates the cgroup version.type Version intconst (VersionUnknown Version = iotaV1V2)func parseV1Number( []byte) (int64, error) {// Ignore trailing newline.:= bytealg.IndexByte(, '\n')if < 0 {return 0, errMalformedFile}= [:], := strconv.ParseInt(string(), 10, 64)if != nil {return 0, errMalformedFile}return , nil}func parseV2Limit( []byte) (float64, bool, error) {:= bytealg.IndexByte(, ' ')if < 0 {return 0, false, errMalformedFile}:= [:]if bytealg.Compare(, []byte("max")) == 0 {// No limit.return 0, false, nil}:= [+1:]// Ignore trailing newline, if any.= bytealg.IndexByte(, '\n')if < 0 {return 0, false, errMalformedFile}= [:], := strconv.ParseInt(string(), 10, 64)if != nil {return 0, false, errMalformedFile}, := strconv.ParseInt(string(), 10, 64)if != nil {return 0, false, errMalformedFile}return float64() / float64(), true, nil}// Finds the path of the current process's CPU cgroup and writes it to out.//// fd is a file descriptor for /proc/self/cgroup.// Returns the number of bytes written and the cgroup version (1 or 2).func parseCPUCgroup( int, func( int, []byte) (int, uintptr), []byte, []byte) (int, Version, error) {// The format of each line is//// hierarchy-ID:controller-list:cgroup-path//// controller-list is comma-separated.//// cgroup v2 has hierarchy-ID 0. If a v1 hierarchy contains "cpu", that// is the CPU controller. Otherwise the v2 hierarchy (if any) is the// CPU controller. It is not possible to mount the same controller// simultaneously under both the v1 and the v2 hierarchies.//// See man 7 cgroups for more details.//// hierarchy-ID and controller-list have relatively small maximum// sizes, and the path can be up to _PATH_MAX, so we need a bit more// than 1 _PATH_MAX of scratch space.:= newLineReader(, , )// Bytes written to out.:= 0for {:= .next()if == errIncompleteLine {// Don't allow incomplete lines. While in theory the// incomplete line may be for a controller we don't// care about, in practice all lines should be of// similar length, so we should just have a buffer big// enough for any.return 0, 0,} else if == errEOF {break} else if != nil {return 0, 0,}:= .line()// The format of each line is//// hierarchy-ID:controller-list:cgroup-path//// controller-list is comma-separated.// See man 7 cgroups for more details.:= bytealg.IndexByte(, ':')if < 0 {return 0, 0, errMalformedFile}:= [:]= [+1:]= bytealg.IndexByte(, ':')if < 0 {return 0, 0, errMalformedFile}:= [:]= [+1:]:=if len() == 0 || [0] != '/' {// We rely on this when composing the full path.return 0, 0, errMalformedFile}if len() > len() {// Should not be possible. If we really get a very long cgroup path,// read /proc/self/cgroup will fail with ENAMETOOLONG.return 0, 0, errPathTooLong}if string() == "0" {// v2 hierarchy.= copy(, )// Keep searching, we might find a v1 hierarchy with a// CPU controller, which takes precedence.} else {// v1 hierarchyif containsCPU() {// Found a v1 CPU controller. This must be the// only one, so we're done.return copy(, ), V1, nil}}}if == 0 {// Found nothing.return 0, 0, ErrNoCgroup}// Must be v2, v1 returns above.return , V2, nil}// Returns true if comma-separated list b contains "cpu".func containsCPU( []byte) bool {for len() > 0 {:= bytealg.IndexByte(, ',')if < 0 {// Neither cmd/compile nor gccgo allocates for these string conversions.return string() == "cpu"}:= [:]:= [+1:]if string() == "cpu" {return true}=}return false}// Returns the path to the specified cgroup and version with cpu controller//// fd is a file descriptor for /proc/self/mountinfo.// Returns the number of bytes written.func parseCPUMount( int, func( int, []byte) (int, uintptr), , []byte, Version, []byte) (int, error) {// The format of each line is://// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue// (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)//// (1) mount ID: unique identifier of the mount (may be reused after umount)// (2) parent ID: ID of parent (or of self for the top of the mount tree)// (3) major:minor: value of st_dev for files on filesystem// (4) root: root of the mount within the filesystem// (5) mount point: mount point relative to the process's root// (6) mount options: per mount options// (7) optional fields: zero or more fields of the form "tag[:value]"// (8) separator: marks the end of the optional fields// (9) filesystem type: name of filesystem of the form "type[.subtype]"// (10) mount source: filesystem specific information or "none"// (11) super options: per super block options//// See man 5 proc_pid_mountinfo for more details.//// Note that emitted paths will not contain space, tab, newline, or// carriage return. Those are escaped. See Linux show_mountinfo ->// show_path. We must unescape before returning.//// A mount point matches if the filesystem type (9) is cgroup2,// or cgroup with "cpu" in the super options (11),// and the cgroup is in the root (4). If there are multiple matches,// the first one is selected.//// We return full cgroup path, which is the mount point (5) +// cgroup parameter without the root (4) prefix.//// (4), (5), and (10) are up to _PATH_MAX. The remaining fields have a// small fixed maximum size, so 4*_PATH_MAX is plenty of scratch space.// Note that non-cgroup mounts may have arbitrarily long (11), but we// can skip those when parsing.:= newLineReader(, , )for {:= .next()if == errIncompleteLine {// An incomplete line is fine as long as it doesn't// impede parsing the fields we need. It shouldn't be// possible for any mount to use more than 3*PATH_MAX// before (9) because there are two paths and all other// earlier fields have bounded options. Only (11) has// unbounded options.} else if == errEOF {break} else if != nil {return 0,}:= .line()// Skip first three fields.for range 3 {:= bytealg.IndexByte(, ' ')if < 0 {return 0, errMalformedFile}= [+1:]}// (4) root: root of the mount within the filesystem:= bytealg.IndexByte(, ' ')if < 0 {return 0, errMalformedFile}:= [:]if len() == 0 || [0] != '/' {// We rely on this in hasPathPrefix.return 0, errMalformedFile}= [+1:]// (5) mount point: mount point relative to the process's root= bytealg.IndexByte(, ' ')if < 0 {return 0, errMalformedFile}:= [:]= [+1:]// Skip ahead past optional fields, delimited by " - ".for {= bytealg.IndexByte(, ' ')if < 0 {return 0, errMalformedFile}if +3 >= len() {return 0, errMalformedFile}:= [ : +3]if string() == " - " {= [+3:]break}= [+1:]}// (9) filesystem type: name of filesystem of the form "type[.subtype]"= bytealg.IndexByte(, ' ')if < 0 {return 0, errMalformedFile}:= [:]= [+1:]switch {case V1:if string() != "cgroup" {continue}// (10) mount source: filesystem specific information or "none"= bytealg.IndexByte(, ' ')if < 0 {return 0, errMalformedFile}// Don't care about mount source.= [+1:]// (11) super options: per super block optionsif !containsCPU() {continue}case V2:if string() != "cgroup2" {continue}default:throw("impossible cgroup version")panic("unreachable")}// Check cgroup is in the root.// If the cgroup is /sandbox/container, the matching mount point root could be// /sandbox/container, /sandbox, or /, := unescapePath(, )if != nil {return 0,}= [:]if !hasPathPrefix(, ) {continue // not matched, this is not the mount point we're looking for}// Cutoff the root from cgroup, ensure rel starts with '/' or is empty.:= [:]if == 1 && len() > 1 {// root is "/", but cgroup is not. Keep full cgroup path.=}if hasPathPrefix(, []byte("/..")) {// the cgroup is out of current cgroup namespace, and this mount point// cannot reach that cgroup.//// e.g. If the process is in cgroup /init, but in a cgroup namespace// rooted at /sandbox/container, /proc/self/cgroup will show /../../init.// we can reach it if the mount point root is// /../.. or /../../init, but not if it is /.. or /// While mount point with root /../../.. should able to reach the cgroup,// we don't know the path to the cgroup within that mount point.continue}// All conditions met, compose the full path.// Copy rel to the correct place first, it may overlap with out.:= unescapedLen()if +len() > len() {return 0, errPathTooLong}copy([:], ), := unescapePath([:], )if != nil {return 0,}if != {throw("wrong unescaped len")}return + len(), nil}// Found nothing.return 0, ErrNoCgroup}func hasPathPrefix(, []byte) bool {:= len()if == 1 {return true // root contains everything}if len() < || !bytealg.Equal(, [:]) {return false}return len() == || [] == '/' // must match at path boundary}var (errInvalidEscape error = stringError("invalid path escape sequence")errPathTooLong error = stringError("path too long"))func unescapedLen( []byte) int {return len() - bytealg.Count(, byte('\\'))*3}// unescapePath copies in to out, unescaping escape sequences generated by// Linux's show_path.//// That is, '\', ' ', '\t', and '\n' are converted to octal escape sequences,// like '\040' for space.//// Caller must ensure that out at least has unescapedLen(in) bytes.// in and out may alias; in-place unescaping is supported.//// Returns the number of bytes written to out.//// Also see escapePath in cgroup_linux_test.go.func unescapePath( []byte, []byte) (int, error) {var , intfor < len() {if >= len() {// given that caller already ensured out is long enough, this// is only possible if there are malformed escape sequences// we have not parsed yet.return , errInvalidEscape}:= []if != '\\' {[] =++++continue}// Start of escape sequence.// Escape sequence is always 4 characters: one slash and three// digits.if +3 >= len() {return , errInvalidEscape}var intfor := range 3 {:= [+1+]if < '0' || > '7' {return , errInvalidEscape}*= 8+= int( - '0')}if > 0xFF {return , errInvalidEscape}[] = byte()+++= 4}return , nil}
![]() |
The pages are generated with Golds v0.8.3-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. |