How does cgroups v2 manage container resources, and why did Linux move from v1 to v2?
Quick Answer
cgroups v2 uses one unified tree where each process belongs to exactly one group, with consistent resource controllers for memory, CPU, and I/O. memory.max sets hard limits, cpu.weight shares CPU proportionally, and io.max throttles disk bandwidth. The move from v1 fixed problems with inconsistent controller hierarchies, race conditions, and added pressure monitoring (PSI).
Detailed Answer
Think of building utility management. In the old system (cgroups v1), electricity, water, and gas each had separate meters and billing departments. A tenant could be on different plans for each utility with no coordination between them. In the new system (cgroups v2), all utilities go through a single management system with one tenant record, coordinated limits, and the ability to detect when a tenant is struggling with any resource.
The move from v1 to v2 happened because v1 had fundamental design problems. In v1, each resource controller (cpu, memory, blkio, cpuset, etc.) had its own separate hierarchy. A process could be in /cgroup/cpu/payments and /cgroup/memory/checkout at the same time, creating inconsistent groupings. Controllers had different naming conventions: memory used memory.limit_in_bytes while CPU used cpu.shares, with different units and behaviors. Some controllers could not work together because they lived on separate hierarchies.
cgroups v2 enforces a single unified tree at /sys/fs/cgroup. Every process belongs to exactly one group, and all enabled controllers apply at that point. Resource files follow a consistent naming pattern: controller.parameter (like memory.max, cpu.weight, io.max). The memory controller uses memory.max for hard limits (the kernel kills processes if exceeded), memory.high as a slowdown point (the kernel throttles allocations before hitting max), and memory.low for best-effort protection from memory reclaim. The CPU controller uses cpu.weight (1-10000, default 100) for proportional CPU sharing and cpu.max for hard bandwidth limits in the format 'quota period' (for example, '200000 100000' means 200ms of CPU every 100ms period, which equals 2 cores). The IO controller uses io.max with per-device read/write bandwidth and IOPS limits.
At production scale, cgroups v2 introduced Pressure Stall Information (PSI), which reports how much time processes in a group spent waiting for CPU, memory, or I/O. This is a game-changer for monitoring because traditional metrics like CPU utilization and memory usage tell you what a container is using but not whether it is struggling. PSI tells you that a container spent 30% of the last 10 seconds stalled on I/O, which directly indicates resource saturation. Kubernetes uses cgroups v2 for memory quality-of-service (throttling via memory.high before OOM), swap management, and smarter eviction decisions.
The tricky part is migration. cgroups v1 and v2 cannot both control the same resource type at the same time. During the transition, some systems run in a hybrid mode where some controllers are on v1 and others on v2, which brings back the coordination problems v2 was built to solve. Container runtimes, systemd, and orchestrators all need to agree on which version to use. On older distributions, switching to v2 requires a kernel boot parameter (systemd.unified_cgroup_hierarchy=1) and checking that all your tools support v2. A misconfigured hybrid setup can result in containers that appear to have no memory limit because the controller is attached to the wrong hierarchy.