Source file src/runtime/cgroup_linux.go
1 // Copyright 2025 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package runtime 6 7 import ( 8 "internal/runtime/cgroup" 9 ) 10 11 // cgroup-aware GOMAXPROCS default 12 // 13 // At startup (defaultGOMAXPROCSInit), we read /proc/self/cgroup and /proc/self/mountinfo 14 // to find our current CPU cgroup and open its limit file(s), which remain open 15 // for the entire process lifetime. We periodically read the current limit by 16 // rereading the limit file(s) from the beginning. 17 // 18 // This makes reading updated limits simple, but has a few downsides: 19 // 20 // 1. We only read the limit from the leaf cgroup that actually contains this 21 // process. But a parent cgroup may have a tighter limit. That tighter limit 22 // would be our effective limit. That said, container runtimes tend to hide 23 // parent cgroups from the container anyway. 24 // 25 // 2. If the process is migrated to another cgroup while it is running it will 26 // not notice, as we only check which cgroup we are in once at startup. 27 var ( 28 // We can't allocate during early initialization when we need to find 29 // the cgroup. Simply use a fixed global as a scratch parsing buffer. 30 cgroupScratch [cgroup.ScratchSize]byte 31 32 cgroupOK bool 33 cgroupCPU cgroup.CPU 34 35 // defaultGOMAXPROCSInit runs before internal/godebug init, so we can't 36 // directly update the GODEBUG counter. Store the result until after 37 // init runs. 38 containermaxprocsNonDefault bool 39 containermaxprocs = &godebugInc{name: "containermaxprocs"} 40 ) 41 42 // Prepare for defaultGOMAXPROCS. 43 // 44 // Must run after parsedebugvars. 45 func defaultGOMAXPROCSInit() { 46 c, err := cgroup.OpenCPU(cgroupScratch[:]) 47 if err != nil { 48 // Likely cgroup.ErrNoCgroup. 49 return 50 } 51 52 if debug.containermaxprocs > 0 { 53 // Normal operation. 54 cgroupCPU = c 55 cgroupOK = true 56 return 57 } 58 59 // cgroup-aware GOMAXPROCS is disabled. We still check the cgroup once 60 // at startup to see if enabling the GODEBUG would result in a 61 // different default GOMAXPROCS. If so, we increment runtime/metrics 62 // /godebug/non-default-behavior/cgroupgomaxprocs:events. 63 procs := getCPUCount() 64 cgroupProcs := adjustCgroupGOMAXPROCS(procs, c) 65 if procs != cgroupProcs { 66 containermaxprocsNonDefault = true 67 } 68 69 // Don't need the cgroup for remaining execution. 70 c.Close() 71 } 72 73 // defaultGOMAXPROCSUpdateGODEBUG updates the internal/godebug counter for 74 // container GOMAXPROCS, once internal/godebug is initialized. 75 func defaultGOMAXPROCSUpdateGODEBUG() { 76 if containermaxprocsNonDefault { 77 containermaxprocs.IncNonDefault() 78 } 79 } 80 81 // Return the default value for GOMAXPROCS when it has not been set explicitly. 82 // 83 // ncpu is the optional precomputed value of getCPUCount. If passed as 0, 84 // defaultGOMAXPROCS will call getCPUCount. 85 func defaultGOMAXPROCS(ncpu int32) int32 { 86 // GOMAXPROCS is the minimum of: 87 // 88 // 1. Total number of logical CPUs available from sched_getaffinity. 89 // 90 // 2. The average CPU cgroup throughput limit (average throughput = 91 // quota/period). A limit less than 2 is rounded up to 2, and any 92 // fractional component is rounded up. 93 // 94 // TODO: add rationale. 95 96 procs := ncpu 97 if procs <= 0 { 98 procs = getCPUCount() 99 } 100 if !cgroupOK { 101 // No cgroup, or disabled by debug.containermaxprocs. 102 return procs 103 } 104 105 return adjustCgroupGOMAXPROCS(procs, cgroupCPU) 106 } 107 108 // Lower procs as necessary for the current cgroup CPU limit. 109 func adjustCgroupGOMAXPROCS(procs int32, cpu cgroup.CPU) int32 { 110 limit, ok, err := cgroup.ReadCPULimit(cpu) 111 if err == nil && ok { 112 limit = ceil(limit) 113 limit = max(limit, 2) 114 if int32(limit) < procs { 115 procs = int32(limit) 116 } 117 } 118 return procs 119 } 120