Linux cgroup v2 Freezer: Verify Freeze and Thaw Before Maintenance
Use cgroup v2 freezing safely: verify convergence, delegation, ancestor state, and recovery; a successful write is only a request.
The cgroup v2 freezer can stop processes in a non-root cgroup and its descendants without sending each process an ordinary stop signal. The interface is deliberately small: write 1 to cgroup.freeze, then observe cgroup.events until its frozen value becomes 1; write 0 to request thaw. The important operational detail is that a successful write is a request, not proof that the hierarchy reached the requested state. The kernel may take time to complete a freeze, an ancestor can keep descendants frozen, and process migration or fork races can change membership.
This is a process-execution control, not an application checkpoint, filesystem freeze, database backup hook, or crash-consistent snapshot. Use it only when the workload owner has defined what may safely stop and how it will resume.
Confirm cgroup v2 and identify the exact subtree
Start with the actual cgroup v2 mount and the workload’s current placement. Do not guess a path from a unit name or use the root cgroup as a test target.
findmnt -t cgroup2 -o TARGET,OPTIONS
systemctl show --property=ControlGroup --value example.service
On many systemd systems the unified hierarchy is mounted at /sys/fs/cgroup, but containers and delegated environments can expose a different mount or only a subtree. Resolve the unit’s ControlGroup beneath the actual cgroup2 mount, then inspect it before writing anything:
CG=/sys/fs/cgroup/example.slice/example.service # replace with the verified path
test -d "$CG" || { echo "cgroup path not found" >&2; exit 1; }
CG=$(realpath -e -- "$CG") || { echo "could not resolve cgroup path" >&2; exit 1; }
CG2_MOUNT=$(findmnt -n -T "$CG" -t cgroup2 -o TARGET)
test -n "$CG2_MOUNT" || { echo "target is not on a cgroup2 mount" >&2; exit 1; }
CG2_MOUNT=$(realpath -e -- "$CG2_MOUNT") || { echo "could not resolve cgroup2 mount" >&2; exit 1; }
case "$CG/" in
"$CG2_MOUNT/"*) ;;
*) echo "target is outside the cgroup2 mount" >&2; exit 1 ;;
esac
test "$CG" != "$CG2_MOUNT" || { echo "refusing the visible cgroup2 mount root" >&2; exit 1; }
test -f "$CG/cgroup.freeze" || { echo "freezer interface unavailable" >&2; exit 1; }
cat "$CG/cgroup.events"
cat "$CG/cgroup.procs"
The chosen directory must be below the visible cgroup2 mount root, expose the freezer file, and have authority explicitly delegated to the operator. A cgroup namespace can make that mount root a view of a subtree rather than the global hierarchy root, so the example conservatively refuses to freeze the entire hierarchy visible inside that namespace. On a systemd-managed host, prefer the manager’s supported freeze operation for a unit when available; do not fight the manager by moving service processes or writing to a cgroup that it has not delegated. The kernel interface does not override systemd’s ownership or an orchestrator’s lifecycle policy.
Also establish which processes will be stopped. Freezing a parent cgroup includes its descendants, so a broad service slice can pause unrelated units. In an ordinary domain cgroup, cgroup.procs lists processes in that cgroup while descendants can hold additional processes; a threaded domain has different reporting semantics and can expose processes across its subtree. The populated field in cgroup.events reports whether processes exist in the cgroup subtree; it is not the freezer status. Record both the target path and the relevant process tree before a maintenance window.
Request a state transition and observe completion
The following is an operator-run pattern for a delegated, disposable test cgroup. Run the controller from outside the target subtree: if the command that performs the thaw is itself frozen, it cannot run to thaw the workload. A manager that owns the cgroup should perform the operation instead of this direct-write example.
set -u
CG=/sys/fs/cgroup/maintenance.slice/example.service # verified delegated child
CG=$(realpath -e -- "$CG") || { echo "could not resolve cgroup path" >&2; exit 1; }
CG2_MOUNT=$(findmnt -n -T "$CG" -t cgroup2 -o TARGET)
test -n "$CG2_MOUNT" || { echo "target is not on a cgroup2 mount" >&2; exit 1; }
CG2_MOUNT=$(realpath -e -- "$CG2_MOUNT") || { echo "could not resolve cgroup2 mount" >&2; exit 1; }
case "$CG/" in
"$CG2_MOUNT/"*) ;;
*) echo "target is outside the cgroup2 mount" >&2; exit 1 ;;
esac
test "$CG" != "$CG2_MOUNT" || { echo "refusing the visible cgroup2 mount root" >&2; exit 1; }
FREEZE="$CG/cgroup.freeze"
EVENTS="$CG/cgroup.events"
test -f "$FREEZE" && test -r "$FREEZE" && test -w "$FREEZE" && test -r "$EVENTS" \
|| { echo "freezer/event interface unavailable or not delegated" >&2; exit 1; }
write_freeze() {
printf '%s\n' "$1" | tee "$FREEZE" >/dev/null
}
wait_for_state() {
wanted=$1
deadline=$((SECONDS + 15))
while ! awk -v wanted="$wanted" \
'$1 == "frozen" && $2 == wanted { found=1 } END { exit !found }' \
"$EVENTS"; do
(( SECONDS < deadline )) || return 1
sleep 0.1
done
}
# This direct-write example requires exclusive control of this delegated cgroup.
# Refuse a pre-existing local freeze request or an already-frozen effective state.
read -r prior_request < "$FREEZE"
if [[ "$prior_request" != 0 ]]; then
echo "cgroup already has a local freeze request; refusing to take ownership" >&2
exit 1
fi
if awk '$1 == "frozen" && $2 == 1 { found=1 } END { exit !found }' "$EVENTS"; then
echo "cgroup is already frozen, possibly by an ancestor; refusing" >&2
exit 1
fi
owns_freeze=0
# Attempt to thaw only a request this script made; this is best-effort cleanup.
thaw_on_exit() {
if (( owns_freeze )); then write_freeze 0 || true; fi
}
trap thaw_on_exit EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
owns_freeze=1
if ! write_freeze 1; then
echo "could not request freeze; check delegation and permissions" >&2
exit 1
fi
if ! wait_for_state 1; then
echo "cgroup did not reach frozen=1 within the window" >&2
exit 1 # EXIT trap requests thaw
fi
# Run only the bounded maintenance operation approved for this workload.
if ! write_freeze 0; then
echo "could not request thaw; use the independent recovery procedure" >&2
exit 1 # EXIT trap makes one more best-effort thaw request
fi
if ! wait_for_state 0; then
echo "cgroup did not report frozen=0; inspect ancestor state" >&2
exit 1
fi
owns_freeze=0
trap - EXIT HUP INT TERM
This example assumes Bash (for SECONDS and arithmetic syntax), awk, sleep, and write permission on the delegated freezer interface. Adapt the mount and cgroup path to the target host. It deliberately checks the kernel’s reported state rather than treating tee success as completion. In an automated controller, add structured logging, a bounded operation duration, alerting, and an independent watchdog that can request thaw if the controlling process disappears. An EXIT trap cannot run after SIGKILL, kernel failure, or power loss.
For a manual test, use the bounded Bash procedure above rather than reducing it to two unchecked writes. Run it only against a disposable cgroup whose local request is initially 0, with one designated writer and no manager concurrently changing the state; the kernel does not attach an owner identity to the 0/1 request. Recheck the resolved path and mountpoint in the shell that performs the operation.
Do not run a freeze experiment against a production database, a cgroup containing the management agent, or a service that owns a critical lock without its operator’s approval and a tested resume path. If a command is launched from within the target cgroup, the controller itself can stop as part of the request; a terminal that appears hung is not evidence that the freeze is still converging.
Interpret hierarchy, membership, and failure correctly
cgroup.freeze exists on non-root cgroups and accepts 0 or 1. Writing 1 affects the cgroup and descendants, but the operation can take time. The kernel reports completed state in the frozen key of cgroup.events and can generate a notification when that value changes. A frozen descendant may also be held frozen by an ancestor. Writing 0 to the descendant removes its own freeze request; it cannot cancel an ancestor’s request. When frozen remains 1, inspect every ancestor and ask the manager that owns the hierarchy before repeatedly issuing writes.
Freezer state does not pin process membership. The kernel documents that processes can enter or leave a frozen cgroup, including around a fork race; a process moved into a frozen cgroup stops, while one moved out becomes runnable. A fatal signal can kill a frozen process. For maintenance that requires a stable workload set, first stop new work and block the controller’s migration path, then freeze and verify. Do not infer that a frozen=1 reading is a permanent seal on the cgroup’s process list.
Freezing also does not force an application to flush its own buffers, commit a database transaction, drain a request queue, or make external services stop writing. If the goal is a backup or consistent snapshot, use the application’s supported checkpoint or quiesce protocol and the storage platform’s consistency mechanism. A process freezer can be one controlled step in such a larger procedure, but it is not that procedure by itself.
cgroup.kill is not a thaw or a stronger pause: it kills processes in the cgroup tree with SIGKILL. Likewise, SIGSTOP is a per-process signal rather than a hierarchical cgroup state interface. Keep freeze, kill, and ordinary service stop as distinct operations with different recovery consequences.
Failure and recovery checklist
If the freeze write fails, verify the cgroup path, write permission, cgroup2 mount, and whether a manager has delegated the subtree. If the state never becomes frozen 1, inspect cgroup.events, the workload’s process tree, ancestor freezes, recent migrations, and the controller’s own cgroup. Do not escalate to freezing a broader parent merely to force the check to pass.
If the controller exits unexpectedly, use a separate management session outside the target subtree to inspect the target and its ancestors. Only the designated cgroup owner, after coordinating other writers, should clear the local request made by this procedure; the kernel does not record an operator identity. Then verify that the target reports frozen 0 and the application has resumed health checks. If an ancestor still holds the subtree frozen, coordinate with the ancestor’s owner. Record the final state and any processes that exited during the maintenance window.
Treat cgroup.freeze as a reversible control with an explicit owner, expiry, and observable state. The safe sequence is to identify the exact delegated subtree, stop new work, request the transition, confirm frozen=1, run one bounded operation, request thaw, and confirm frozen=0. If the application needs more than stopped execution, use its native consistency protocol instead.
Related:
- Linux cgroup v2 I/O Control: Weights, Limits, Latency, and Verification
- Linux cgroup v2 memory.reclaim: Controlled Proactive Reclaim
Sources: