From Chaos to Order
Cgroups have two major versions with fundamentally different designs. You will encounter both in production, so understanding the difference is essential.
Cgroups v1 — The Original Design (2008):
In v1, each resource controller has its own independent hierarchy. The CPU controller has one tree of groups. The memory controller has a completely separate tree. A process can belong to different groups in each hierarchy simultaneously.
CPU hierarchy: Memory hierarchy:
/ /
├── high-priority/ ├── web-tier/
│ └── nginx (PID 42) │ └── nginx (PID 42)
└── low-priority/ └── batch-tier/
└── backup (PID 99) └── backup (PID 99)
This sounds flexible, but it created serious problems:
- Coordinating limits across controllers was complex and error-prone.
- The same process appearing in multiple hierarchies made accounting inconsistent.
- Software managing cgroups (like Docker) had to juggle multiple independent trees.
- Some controllers had subtle incompatibilities with each other.
Cgroups v2 — The Unified Hierarchy (stable since kernel 4.5, default in most distros since ~2020):
In v2, there is exactly one hierarchy. Every process belongs to exactly one cgroup in that single tree. You enable whichever controllers you want on any given cgroup, and all controllers operate on the same unified group membership.
Single unified hierarchy:
/
├── system.slice/
│ ├── nginx.service/ ← memory.max=1G, cpu.max=50% 100ms
│ └── postgres.service/ ← memory.max=4G, cpu.max=200% 100ms
└── docker/
├── container-A/ ← memory.max=512M, io.max=50MB/s
└── container-B/ ← memory.max=256M
Key improvements in v2:
- Pressure stall information (PSI): v2 exposes memory.pressure, cpu.pressure, and io.pressure files that report what percentage of time processes in the group are stalled waiting for that resource. This is far more actionable than simple usage metrics.
- Delegation model: Unprivileged users can manage sub-hierarchies they own, enabling rootless containers.
- Consistent accounting: All controllers see the same group membership, eliminating the v1 inconsistency problem.
How to tell which version your system uses: a populated /sys/fs/cgroup/cgroup.controllers file indicates v2, and the same story shows up in the mounted filesystem type at /sys/fs/cgroup — a single cgroup2 mount means v2, while several separate cgroup mounts (one per controller) means v1.
Practical implication: Docker on modern systems defaults to cgroups v2. If you are debugging container resource limits and the paths you find in documentation don't exist, you are probably looking at v1 docs on a v2 system. The controller files have different names: v1 uses memory.limit_in_bytes, v2 uses memory.max.
A common interview question: "What is the difference between cgroups v1 and v2, and why does it matter for containers?" The key answer: v1 has per-controller independent hierarchies (a process can be in different groups for CPU vs. memory), while v2 has a single unified hierarchy (one group membership, multiple controllers). This matters for containers because v2 enables accurate accounting, pressure stall metrics (PSI) for smarter scheduling, and rootless container support. Kubernetes moved to requiring cgroups v2 for full feature support starting with version 1.25.