// 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 runtime import ( "internal/runtime/cgroup" ) // cgroup-aware GOMAXPROCS default // // At startup (defaultGOMAXPROCSInit), we read /proc/self/cgroup and /proc/self/mountinfo // to find our current CPU cgroup and open its limit file(s), which remain open // for the entire process lifetime. We periodically read the current limit by // rereading the limit file(s) from the beginning. // // This makes reading updated limits simple, but has a few downsides: // // 1. We only read the limit from the leaf cgroup that actually contains this // process. But a parent cgroup may have a tighter limit. That tighter limit // would be our effective limit. That said, container runtimes tend to hide // parent cgroups from the container anyway. // // 2. If the process is migrated to another cgroup while it is running it will // not notice, as we only check which cgroup we are in once at startup. var ( // We can't allocate during early initialization when we need to find // the cgroup. Simply use a fixed global as a scratch parsing buffer. cgroupScratch [cgroup.ScratchSize]byte cgroupOK bool cgroupCPU cgroup.CPU // defaultGOMAXPROCSInit runs before internal/godebug init, so we can't // directly update the GODEBUG counter. Store the result until after // init runs. containermaxprocsNonDefault bool containermaxprocs = &godebugInc{name: "containermaxprocs"} ) // Prepare for defaultGOMAXPROCS. // // Must run after parsedebugvars. func defaultGOMAXPROCSInit() { c, err := cgroup.OpenCPU(cgroupScratch[:]) if err != nil { // Likely cgroup.ErrNoCgroup. return } if debug.containermaxprocs > 0 { // Normal operation. cgroupCPU = c cgroupOK = true return } // cgroup-aware GOMAXPROCS is disabled. We still check the cgroup once // at startup to see if enabling the GODEBUG would result in a // different default GOMAXPROCS. If so, we increment runtime/metrics // /godebug/non-default-behavior/cgroupgomaxprocs:events. procs := getCPUCount() cgroupProcs := adjustCgroupGOMAXPROCS(procs, c) if procs != cgroupProcs { containermaxprocsNonDefault = true } // Don't need the cgroup for remaining execution. c.Close() } // defaultGOMAXPROCSUpdateGODEBUG updates the internal/godebug counter for // container GOMAXPROCS, once internal/godebug is initialized. func defaultGOMAXPROCSUpdateGODEBUG() { if containermaxprocsNonDefault { containermaxprocs.IncNonDefault() } } // Return the default value for GOMAXPROCS when it has not been set explicitly. // // ncpu is the optional precomputed value of getCPUCount. If passed as 0, // defaultGOMAXPROCS will call getCPUCount. func defaultGOMAXPROCS(ncpu int32) int32 { // GOMAXPROCS is the minimum of: // // 1. Total number of logical CPUs available from sched_getaffinity. // // 2. The average CPU cgroup throughput limit (average throughput = // quota/period). A limit less than 2 is rounded up to 2, and any // fractional component is rounded up. // // TODO: add rationale. procs := ncpu if procs <= 0 { procs = getCPUCount() } if !cgroupOK { // No cgroup, or disabled by debug.containermaxprocs. return procs } return adjustCgroupGOMAXPROCS(procs, cgroupCPU) } // Lower procs as necessary for the current cgroup CPU limit. func adjustCgroupGOMAXPROCS(procs int32, cpu cgroup.CPU) int32 { limit, ok, err := cgroup.ReadCPULimit(cpu) if err == nil && ok { limit = ceil(limit) limit = max(limit, 2) if int32(limit) < procs { procs = int32(limit) } } return procs }