| // 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 cgroup | |
| import ( | |
| "internal/runtime/syscall/linux" | |
| ) | |
| // Include explicit NUL to be sure we include it in the slice. | |
| const ( | |
| v2MaxFile = "/cpu.max\x00" | |
| v1QuotaFile = "/cpu.cfs_quota_us\x00" | |
| v1PeriodFile = "/cpu.cfs_period_us\x00" | |
| ) | |
| // CPU owns the FDs required to read the CPU limit from a cgroup. | |
| type CPU struct { | |
| version Version | |
| // For cgroup v1, this is cpu.cfs_quota_us. | |
| // For cgroup v2, this is cpu.max. | |
| quotaFD int | |
| // For cgroup v1, this is cpu.cfs_period_us. | |
| // For cgroup v2, this is unused. | |
| periodFD int | |
| } | |
| func (c CPU) Close() { | |
| switch c.version { | |
| case V1: | |
| linux.Close(c.quotaFD) | |
| linux.Close(c.periodFD) | |
| case V2: | |
| linux.Close(c.quotaFD) | |
| default: | |
| throw("impossible cgroup version") | |
| } | |
| } | |
| func checkBufferSize(s []byte, size int) { | |
| if len(s) != size { | |
| println("runtime: cgroup buffer length", len(s), "want", size) | |
| throw("runtime: cgroup invalid buffer length") | |
| } | |
| } | |
| // OpenCPU returns a CPU for the CPU cgroup containing the current process, or | |
| // ErrNoCgroup if the process is not in a CPU cgroup. | |
| // | |
| // scratch must have length ScratchSize. | |
| func OpenCPU(scratch []byte) (CPU, error) { | |
| checkBufferSize(scratch, ScratchSize) | |
| base := scratch[:PathSize] | |
| scratch2 := scratch[PathSize:] | |
| n, version, err := FindCPU(base, scratch2) | |
| if err != nil { | |
| return CPU{}, err | |
| } | |
| switch version { | |
| case 1: | |
| n2 := copy(base[n:], v1QuotaFile) | |
| path := base[:n+n2] | |
| quotaFD, errno := linux.Open(&path[0], linux.O_RDONLY|linux.O_CLOEXEC, 0) | |
| if errno != 0 { | |
| // This may fail if this process was migrated out of | |
| // the cgroup found by FindCPU and that cgroup has been | |
| // deleted. | |
| return CPU{}, errSyscallFailed | |
| } | |
| n2 = copy(base[n:], v1PeriodFile) | |
| path = base[:n+n2] | |
| periodFD, errno := linux.Open(&path[0], linux.O_RDONLY|linux.O_CLOEXEC, 0) | |
| if errno != 0 { | |
| // This may fail if this process was migrated out of | |
| // the cgroup found by FindCPU and that cgroup has been | |
| // deleted. | |
| return CPU{}, errSyscallFailed | |
| } | |
| c := CPU{ | |
| version: 1, | |
| quotaFD: quotaFD, | |
| periodFD: periodFD, | |
| } | |
| return c, nil | |
| case 2: | |
| n2 := copy(base[n:], v2MaxFile) | |
| path := base[:n+n2] | |
| maxFD, errno := linux.Open(&path[0], linux.O_RDONLY|linux.O_CLOEXEC, 0) | |
| if errno != 0 { | |
| // This may fail if this process was migrated out of | |
| // the cgroup found by FindCPU and that cgroup has been | |
| // deleted. | |
| return CPU{}, errSyscallFailed | |
| } | |
| c := CPU{ | |
| version: 2, | |
| quotaFD: maxFD, | |
| periodFD: -1, | |
| } | |
| return c, nil | |
| default: | |
| throw("impossible cgroup version") | |
| panic("unreachable") | |
| } | |
| } | |
| // Returns average CPU throughput limit from the cgroup, or ok false if there | |
| // is no limit. | |
| func ReadCPULimit(c CPU) (float64, bool, error) { | |
| switch c.version { | |
| case 1: | |
| quota, err := readV1Number(c.quotaFD) | |
| if err != nil { | |
| return 0, false, errMalformedFile | |
| } | |
| if quota < 0 { | |
| // No limit. | |
| return 0, false, nil | |
| } | |
| period, err := readV1Number(c.periodFD) | |
| if err != nil { | |
| return 0, false, errMalformedFile | |
| } | |
| return float64(quota) / float64(period), true, nil | |
| case 2: | |
| // quotaFD is the cpu.max FD. | |
| return readV2Limit(c.quotaFD) | |
| default: | |
| throw("impossible cgroup version") | |
| panic("unreachable") | |
| } | |
| } | |
| // Returns the value from the quota/period file. | |
| func readV1Number(fd int) (int64, error) { | |
| // The format of the file is "<value>\n" where the value is in | |
| // int64 microseconds and, if quota, may be -1 to indicate no limit. | |
| // | |
| // MaxInt64 requires 19 bytes to display in base 10, thus the | |
| // conservative max size of this file is 19 + 1 (newline) = 20 bytes. | |
| // We'll provide a bit more for good measure. | |
| // | |
| // Always read from the beginning of the file to get a fresh value. | |
| var b [64]byte | |
| n, errno := linux.Pread(fd, b[:], 0) | |
| if errno != 0 { | |
| return 0, errSyscallFailed | |
| } | |
| if n == len(b) { | |
| return 0, errMalformedFile | |
| } | |
| buf := b[:n] | |
| return parseV1Number(buf) | |
| } | |
| // Returns CPU throughput limit, or ok false if there is no limit. | |
| func readV2Limit(fd int) (float64, bool, error) { | |
| // The format of the file is "<quota> <period>\n" where quota and | |
| // period are microseconds and quota may be "max" to indicate no limit. | |
| // | |
| // Note that the kernel is inconsistent about whether the values are | |
| // uint64 or int64: values are parsed as uint64 but printed as int64. | |
| // See kernel/sched/core.c:cpu_max_{show,write}. | |
| // | |
| // In practice, the kernel limits the period to 1s (1000000us) (see | |
| // max_cfs_quota_period), and the quota to (1<<44)us (see | |
| // max_cfs_runtime), so these values can't get large enough for the | |
| // distinction to matter. | |
| // | |
| // MaxInt64 requires 19 bytes to display in base 10, thus the | |
| // conservative max size of this file is 19 + 19 + 1 (space) + 1 | |
| // (newline) = 40 bytes. We'll provide a bit more for good measure. | |
| // | |
| // Always read from the beginning of the file to get a fresh value. | |
| var b [64]byte | |
| n, errno := linux.Pread(fd, b[:], 0) | |
| if errno != 0 { | |
| return 0, false, errSyscallFailed | |
| } | |
| if n == len(b) { | |
| return 0, false, errMalformedFile | |
| } | |
| buf := b[:n] | |
| return parseV2Limit(buf) | |
| } | |
| // FindCPU finds the path to the CPU cgroup that this process is a member of | |
| // and places it in out. scratch is a scratch buffer for internal use. | |
| // | |
| // out must have length PathSize. scratch must have length ParseSize. | |
| // | |
| // Returns the number of bytes written to out and the cgroup version (1 or 2). | |
| // | |
| // Returns ErrNoCgroup if the process is not in a CPU cgroup. | |
| func FindCPU(out []byte, scratch []byte) (int, Version, error) { | |
| checkBufferSize(out, PathSize) | |
| checkBufferSize(scratch, ParseSize) | |
| // The cgroup path is <cgroup mount point> + <relative path>. | |
| // relative path is the cgroup relative to the mount root. | |
| n, version, err := FindCPUCgroup(out, scratch) | |
| if err != nil { | |
| return 0, 0, err | |
| } | |
| n, err = FindCPUMountPoint(out, out[:n], version, scratch) | |
| return n, version, err | |
| } | |
| // FindCPUCgroup finds the path to the CPU cgroup that this process is a member of | |
| // and places it in out. scratch is a scratch buffer for internal use. | |
| // | |
| // out must have length PathSize. scratch must have length ParseSize. | |
| // | |
| // Returns the number of bytes written to out and the cgroup version (1 or 2). | |
| // | |
| // Returns ErrNoCgroup if the process is not in a CPU cgroup. | |
| func FindCPUCgroup(out []byte, scratch []byte) (int, Version, error) { | |
| path := []byte("/proc/self/cgroup\x00") | |
| fd, errno := linux.Open(&path[0], linux.O_RDONLY|linux.O_CLOEXEC, 0) | |
| if errno == linux.ENOENT { | |
| return 0, 0, ErrNoCgroup | |
| } else if errno != 0 { | |
| return 0, 0, errSyscallFailed | |
| } | |
| // The relative path always starts with /, so we can directly append it | |
| // to the mount point. | |
| n, version, err := parseCPUCgroup(fd, linux.Read, out[:], scratch) | |
| if err != nil { | |
| linux.Close(fd) | |
| return 0, 0, err | |
| } | |
| linux.Close(fd) | |
| return n, version, nil | |
| } | |
| // FindCPUMountPoint finds the mount point containing the specified cgroup and | |
| // version with cpu controller, and compose the full path to the cgroup in out. | |
| // scratch is a scratch buffer for internal use. | |
| // | |
| // out must have length PathSize, may overlap with cgroup. | |
| // scratch must have length ParseSize. | |
| // | |
| // Returns the number of bytes written to out. | |
| // | |
| // Returns ErrNoCgroup if no matching mount point is found. | |
| func FindCPUMountPoint(out, cgroup []byte, version Version, scratch []byte) (int, error) { | |
| checkBufferSize(out, PathSize) | |
| checkBufferSize(scratch, ParseSize) | |
| path := []byte("/proc/self/mountinfo\x00") | |
| fd, errno := linux.Open(&path[0], linux.O_RDONLY|linux.O_CLOEXEC, 0) | |
| if errno == linux.ENOENT { | |
| return 0, ErrNoCgroup | |
| } else if errno != 0 { | |
| return 0, errSyscallFailed | |
| } | |
| n, err := parseCPUMount(fd, linux.Read, out, cgroup, version, scratch) | |
| if err != nil { | |
| linux.Close(fd) | |
| return 0, err | |
| } | |
| linux.Close(fd) | |
| return n, nil | |
| } | |