When running multiple background compilation tasks, microservice containers, or developer databases on a Linux workstation or server, a single runaway process can consume 100% of physical RAM and trigger the Linux Out-Of-Memory (OOM) killer. While legacy Unix tools like ulimit restrict per-shell boundaries, modern Linux resource governance relies entirely on Control Groups version 2 (cgroups v2). Unified natively under the /sys/fs/cgroup virtual filesystem, cgroups v2 allows administrators to set hard and high memory ceilings, CPU weight throttling, and I/O bandwidth limits across process trees.
Understanding the cgroups v2 Unified Hierarchy
Unlike cgroups v1, which maintained disconnected hierarchies for each resource controller, cgroups v2 implements a strictly unified tree:
- Single Hierarchy: Every process belongs to exactly one cgroup. Resource controllers (CPU, memory, IO, pids) are enabled hierarchically from root to leaf nodes.
- No Internal Processes: In cgroups v2, a parent cgroup containing child cgroups cannot host processes directly, preventing controller delegation ambiguities.
- Pressure Stall Information (PSI): Integrated kernel sensors that measure exact CPU, memory, and I/O stall times, signaling when workloads begin starving for hardware resources.
Checking cgroups v2 Availability
Modern Linux distributions (such as Ubuntu 22.04 LTS and 24.04 LTS, Debian 12, and Fedora) enable cgroups v2 by default. Confirm unified hierarchy support:
stat -fc %T /sys/fs/cgroup
If the command outputs cgroup2fs, your system is running native cgroups v2.
Inspect available controllers enabled on the root cgroup:
cat /sys/fs/cgroup/cgroup.controllers
Typical controllers include cpuset cpu io memory pids.
Creating a Custom cgroup and Setting Memory Quotas
Because cgroups operate as a virtual filesystem, creating a group is as simple as creating a standard directory:
# Create a dedicated group named 'sandbox_worker'
sudo mkdir -p /sys/fs/cgroup/sandbox_worker
# Set a high memory throttling limit (2 GB) and an absolute hard ceiling (2.5 GB)
echo "2G" | sudo tee /sys/fs/cgroup/sandbox_worker/memory.high
echo "2500M" | sudo tee /sys/fs/cgroup/sandbox_worker/memory.max
# Restrict swap space consumption to 512 MB
echo "512M" | sudo tee /sys/fs/cgroup/sandbox_worker/memory.swap.max
When a process inside this group reaches memory.high, the kernel actively slows its allocations and reclaims page cache without killing the process. If it exceeds memory.max, the OOM killer terminates processes strictly within that cgroup, leaving the rest of the operating system completely untouched.
Configuring CPU Quotas and Weights
To prevent a task from monopolizing CPU cores:
Option 1: Proportional Weight Balancing: Assign relative CPU priority (range: 1 to 10000, default: 100):
echo "50" | sudo tee /sys/fs/cgroup/sandbox_worker/cpu.weight
Option 2: Hard CPU Bandwidth Caps: Limit a process to 1.5 CPU cores (150,000 microseconds per 100,000 microsecond period):
echo "150000 100000" | sudo tee /sys/fs/cgroup/sandbox_worker/cpu.max
Assigning Active Processes to a cgroup
To move a running process into your custom group, write its Process ID (PID) into cgroup.procs:
echo 18429 | sudo tee /sys/fs/cgroup/sandbox_worker/cgroup.procs
All child threads and subsequently spawned sub-processes automatically inherit the cgroup constraints.
Managing Transient Limits via systemd-run
Rather than manually manipulating the /sys/fs/cgroup filesystem, systemd provides the systemd-run wrapper to launch transient services with instant cgroups v2 boundaries:
systemd-run --user --scope -p MemoryMax=1G -p CPUQuota=50% python3 intensive_script.py
This command spawns the script inside an isolated cgroup slice, enforcing a 1 GB memory cap and a 50% single-core CPU ceiling, cleaning up the cgroup automatically when execution terminates.