cgroups organize tasks into a hierarchy for resource accounting, allocation, and limiting. Namespaces change the view of resources, while cgroups control usage behavior; entering a new cgroup namespace does not automatically grant more CPU or memory.
This article focuses on v2. v1 allowed placing multiple controllers at different levels, and it is not absolutely impossible to limit a group of processes simultaneously; however, v2's unified hierarchy and more consistent rules make control relationships easier to compose. Whether v2 is mounted should be checked against the current system, not guessed by the distribution year.
Hierarchy limits apply along the ancestry
Child cgroup configurations cannot bypass parent limits. A parent's CPU quota may be consumed jointly by multiple child groups, and the parent's memory limit constrains the entire subtree; checking only leaf files will miss the true bottleneck.
cgroup.controllers indicates which controllers are available for child groups at the current level, while cgroup.subtree_control determines which are enabled for child levels. mkdir does not mean all controller files and policies are automatically ready. Controllers need to be enabled top-down along the hierarchy.
Regular domain controllers generally follow the no-internal-process constraint: when internal nodes allocate resources to child groups, they cannot simultaneously place regular worker processes to compete with child groups; root and threaded modes have specific rules, so no single generalization covers all cases. cgroup v2
Delegation is more important than directly modifying the system root
Modern systems often manage the cgroup tree via systemd or similar managers. Applications should use delegated subtrees or manager interfaces and cannot assume they can remount /sys/fs/cgroup or arbitrarily modify upper-level controllers.
Writing to cgroup.procs in an allowed target group moves the threads of the entire process; in threaded mode, cgroup.threads has different semantics. Migration permissions also involve requirements such as common ancestors; having write permission on a directory does not allow pulling in arbitrary processes. Process migration does not guarantee that existing memory accounting is fully transferred.
CPU: weight, quota, and placement are configured separately
Interface
Example
Meaning
cpu.weight
200
Weight relative to siblings, default 100
cpu.max
50000 100000
Max 50000 microseconds of CPU time per 100000 microsecond period
cpuset.cpus
0-3
Request available CPU set; also check effective limits and ancestor constraints
weight=200 does not guarantee two CPUs, nor does it guarantee being twice as fast as another group at any time; weight mainly manifests when tasks compete, and idle resources can be used.
The quota in cpu.max is consumed jointly by relevant tasks within the group. Two threads can run on two CPUs simultaneously and exhaust the quota faster, after which they are throttled; it is not "fixed 50% per CPU." Observe increments in nr_throttled, throttled_usec, etc., in cpu.stat to determine if the quota is actually the bottleneck.
cpuset placement limits do not equate to exclusive access to those CPUs; partitioning and exclusive configurations have separate rules. cpuset.mems should also be understood in conjunction with effective nodes and NUMA allocation policies.
Both children can throttle with 30 ms left each
Let parent P have cpu.max=100000 100000 and children A/B each 80000 100000. Assume aligned periods from t=0, no burst, one continuously runnable fair-class task per child, and two available CPUs, omitting slice-distribution details.
In 50 ms wall time, each consumes 50 ms CPU, exhausting P's 100 ms total. Each child retains 30 ms but cannot exceed its ancestor. Raising child limits does not remove this bottleneck before replenishment.
defone_period(active):parent_left=100leaf_left={"A":80,"B":80}used={"A":0,"B":0}for_inrange(100):# One hundred 1 ms slots; each task uses at most one CPU
fornameinactive:ifparent_left>0andleaf_left[name]>0:parent_left-=1leaf_left[name]-=1used[name]+=1returnused, leaf_left, parent_leftassertone_period(("A","B"))==({"A":50,"B":50},{"A":30,"B":30},0)assertone_period(("A",))==({"A":80,"B":0},{"A":0,"B":80},20)
With only A running, it reaches its own 80 ms limit while P retains 20 ms. Inspect competition and per-level cpu.stat deltas. The ideal 50 ms is not a universal Linux timeline across period alignment, slice caches, classes, and load. CPU controller
Weights are hierarchical too. If root children P/Q have equal weight, P contains equally weighted A/B, and Q contains C, a saturated proportional model yields A=1/4,B=1/4,C=1/2, not equal thirds. These shares no longer apply when a group stops competing or other limits intervene.
Run A and B separately and watch both charge the common ancestor. A child's remaining quota can become unusable when its sibling exhausts the parent.
Achild=min(Rchild,Rparent)
R is remaining CPU time in this period. A deeper real hierarchy checks all relevant ancestors; the diagram retains two levels.
Preparing the visual
Why can a child stop despite its remaining budget?
A child’s configured quota is not a reservation and cannot bypass its ancestors. Availability is min(child remaining, ancestor remaining); a sibling’s use of the ancestor can constrain the other child.
{"id":"cgroup-ancestor-budget","title":"Why can a child stop despite its remaining budget?","summary":"A child’s configured quota is not a reservation and cannot bypass its ancestors. Availability is min(child remaining, ancestor remaining); a sibling’s use of the ancestor can constrain the other child.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"parent\" data-i18n=\"parent\"></label><output for=\"parent\" id=\"parent-value\"></output><input id=\"parent\" type=\"range\" min=\"10\" max=\"100\" value=\"60\" step=\"10\"></div><div class=\"control\"><label for=\"a\" data-i18n=\"a\"></label><output for=\"a\" id=\"a-value\"></output><input id=\"a\" type=\"range\" min=\"10\" max=\"100\" value=\"50\" step=\"10\"></div><div class=\"control\"><label for=\"b\" data-i18n=\"b\"></label><output for=\"b\" id=\"b-value\"></output><input id=\"b\" type=\"range\" min=\"10\" max=\"100\" value=\"50\" step=\"10\"></div><p class=\"caption\" data-i18n=\"legend\"></p><div class=\"budget \"><header>root<span id=\"remaining-parent\"></span></header><div><i id=\"used-parent\"></i><b id=\"limit-parent\"></b></div></div><div class=\"budget child\"><header>A<span id=\"remaining-a\"></span></header><div><i id=\"used-a\"></i><b id=\"limit-a\"></b></div></div><div class=\"budget child\"><header>B<span id=\"remaining-b\"></span></header><div><i id=\"used-b\"></i><b id=\"limit-b\"></b></div></div><div class=\"actions\"><button id=\"runA\" data-i18n=\"runA\"></button><button id=\"runB\" data-i18n=\"runB\"></button></div><div class=\"metrics\"><div><span data-i18n=\"availableA\"></span><strong id=\"availableA\"></strong></div><div><span data-i18n=\"availableB\"></span><strong id=\"availableB\"></strong></div></div><div class=\"formula\" id=\"formula\"></div><div class=\"actions\"><button id=\"period\" data-i18n=\"period\"></button></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.budget{padding:10px 0;margin:12px 0}.budget.child{margin-left:18px;border-left:1px solid var(--rule);padding-left:12px}.budget header{font:12px var(--viz-font-ui);display:flex;justify-content:space-between;margin-bottom:10px}.budget>div{height:22px;background:var(--surface);position:relative}.budget i{height:100%;display:block;background:var(--accent);transition:width .2s}.budget b{width:2px;position:absolute;top:-5px;bottom:-5px;background:var(--second);transition:left .2s}.formula{min-height:5em;white-space:pre-line}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let used=[0,0];function draw(){const p=+$('#parent').value,a=+$('#a').value,b=+$('#b').value,total=used[0]+used[1],remaining=Math.max(0,p-total),aa=Math.min(a-used[0],remaining),bb=Math.min(b-used[1],remaining);for(const[id,q,u]of [['parent',p,total],['a',a,used[0]],['b',b,used[1]]]){set(id+'-value',q+' ms');set('remaining-'+id,(q-u)+' / '+q+' ms');$('#used-'+id).style.width=u+'%';$('#limit-'+id).style.left=q+'%';}set('availableA',aa);set('availableB',bb);set('formula','A: min('+(a-used[0])+', '+remaining+') = '+aa+' ms\\nB: min('+(b-used[1])+', '+remaining+') = '+bb+' ms');$('#runA').disabled=aa<10;$('#runB').disabled=bb<10;set('explanation',t('note'));}for(const[id,i]of [['runA',0],['runB',1]])$('#'+id).onclick=()=>{used[i]+=10;draw();};for(const id of ['parent','a','b'])$('#'+id).oninput=()=>{used=[0,0];draw();};$('#period').onclick=()=>{used=[0,0];draw();};draw();","audio":false,"strings":{"a":"A quota / ms","availableA":"A available now / ms","availableB":"B available now / ms","b":"B quota / ms","heading":"Why can a child stop despite its remaining budget?","intro":"Consume CPU time in A or B; each action charges both that child and their common ancestor. Changing configuration starts a new model period.","legend":"Solid bars are CPU time consumed; vertical lines are each level’s quota. Common scale: 0…100 ms.","note":"Defaults are A=50, B=50, ancestor=60. Run A for 50 ms, then B for 10 ms: B retains 40 ms of its own quota, but the common ancestor has none.","parent":"Ancestor quota / ms","period":"Start next period","reset":"Reset","runA":"A consumes 10 ms CPU","runB":"B consumes 10 ms CPU","scope":"Two cgroup-v2 levels, common 100 ms period, no internal tasks or burst. Each action charges 10 ms CPU time, not wall time. Runqueues, quota batch transfers, and precise throttling statistics are omitted. Child quotas may sum above the ancestor but create no extra CPU budget.","scopeLabel":"Model scope and assumptions"}}
Memory: high is pressure control, max is the boundary
Exceeding memory.high applies reclaim and throttling pressure to allocation tasks; it can be temporarily breached and does not trigger the OOM killer itself. memory.max is a hard limit; upon reaching it, the system attempts reclaim and other handling first; if unsuccessful, it may enter memcg OOM. It does not kill processes immediately for exceeding by even one byte.
memory.low is best-effort reclaim protection, while memory.min provides stronger protection semantics, but effective protection is influenced by ancestors and competing allocations, not pre-allocated physical RAM. memory.swap.max has independent control over swap.
memory.oom.group can require OOM handling to treat workloads as a group; you cannot directly draw an arrow from "high reclaim failure" to "killing the whole group due to oom.group." It is necessary to distinguish the level where events occur and the triggering causes.
Reclaim selection depends on page state, activity, writability, swap, and policies, and does not guarantee clearing all caches first and then anonymous pages. memory.current includes the corresponding accounted memory and does not equal a specific process's RSS.
I/O and Task Count
io.max limits bytes or IOPS per device; the major/minor device numbers in the format should come from the actual target device; the effect of io.weight depends on block layer and corresponding scheduler/policy support. Cache writes, writebacks, and underlying device paths affect visible performance; you cannot directly equate arbitrary read/write call rates with block device rates.
pids.max limits the number of kernel tasks, including threads. Reaching the limit prevents corresponding fork/clone creation, typically resulting in EAGAIN; setting a lower limit or moving into existing tasks does not automatically kill excess tasks. Observe pids.current and pids.events.
PSI: Observe Waiting, Not Just Usage
PSI's some represents the percentage of time in a window where at least one task was blocked by the corresponding resource; full represents the percentage of time where all non-idle tasks within the scope were blocked. avg10/avg60/avg300 are average percentages, and total is cumulative microseconds, not another percentage.
High CPU usage might indicate sufficient execution of useful work, while PSI helps observe waiting losses; high memory usage might mainly be valuable cache and should not be directly equated with memory pressure. System-level CPU full has special semantics and cannot be mechanically applied to memory full interpretations. PSI
PSI some and full combine wall-time intervals
Observe 100 ms of cgroup I/O pressure with exactly two non-idle tasks demanding work throughout. A waits during [0,40), B during [20,60), and each progresses otherwise.
Interval ms
A
B
Pressure contribution
0–20
Stalled
Progress
some +20
20–40
Stalled
Stalled
some/full +20 each
40–60
Progress
Stalled
some +20
60–100
Progress
Progress
Neither
Thus some=60 ms and full=20 ms. Adding task waits gives 80 ms, not the union of stalled wall time.
defpressure_ms(blocked,window=100):assertblockedandwindow>0points={0,window}forintervalsinblocked:forstart,endinintervals:assert0<=start<=end<=windowpoints.update((start,end))points=sorted(points)some=full=0forstart,endinzip(points,points[1:]):waiting=[any(a<=start<bfora,binintervals)forintervalsinblocked]ifany(waiting):some+=end-startifall(waiting):full+=end-startreturnsome, fullwaits=[[(0,40)],[(20,60)]]assertpressure_ms(waits)==(60,20)assertpressure_ms(waits+[[]])==(60,0)# C always progresses, making full zero
assertpressure_ms([[(0,10)],[(10,20)]])==(20,0)assertpressure_ms([[],[]])==(0,0)assertpressure_ms([[(0,40),(20,60)]])==(60,60)
This fixed non-idle-task model omits dynamic task/accounting scope. A voluntarily idle thread with no work is not a permanently progressing task that forces full to zero. PSI
A some total increase of 60000 μs over 100000 μs is approximately 60% for that sampling window. It does not recompute avg10; avg10/60/300 are kernel-maintained smoothed averages. Keep interval, counter resets, and group lifetime consistent before subtracting totals.
Locate the belonging group based on the actual cgroup mount root and process path, then read available files such as cpu.stat, memory.events, memory.pressure, and pids.current. Containers may only expose a segment of the hierarchy, and /proc paths are also affected by namespaces; do not unconditionally concatenate strings to the host root directory.
The diagnostic order can be: confirm the belonging group and ancestor constraints, check if controllers are enabled, then compare statistical increments with actual request latency. Reading counters does not explain all causality, but it can distinguish "configured limits" from "actually being limited."
Kernel Reading Entry Points
kernel/cgroup manages core hierarchy and membership, mm/memcontrol.c manages memory accounting and control, and block/blk-cgroup.c manages block layer associations. The css of a controller is the subsystem state, and not all controllers attach the same object to the request_queue.
Entering the source code from user-visible interface contracts, then viewing specific version charge, reclaim, or scheduling paths, is more reliable than disguising dozens of lines of comments as a universal try_charge call graph. The other half of the isolation view can be found in Namespaces.