Step 1 of 5 · Reading · ~3 min
Read
cgroups & Resource Limits
Process Limits
Memory and CPU are the limits everyone sets. The one that saves the host is the one almost nobody sets: a cap on how many processes a container may have.
Why a counter is a security control
The host's process table is finite — a few tens of thousands of entries, shared by everything on the machine. Exhaust it and the failure is total: not "the container is slow" but fork: Resource temporarily unavailable from sshd, from the container runtime, from the monitoring agent. You cannot log in to fix it, because logging in requires a fork.
Three ways containers get there, only one of them malicious:
- A forkbomb.
:(){ :|:& };:is thirteen characters and needs no privileges at all. - A thread leak. Every thread is a task with its own entry in that table; a pool that creates and never joins looks exactly like a slow forkbomb.
- A misconfiguration.
--workers=autoon a 96-core host, in a container you sized for two.
The pids controller answers all three with a counter:
echo 100 > /sys/fs/cgroup/pids/docker/<id>/pids.max # v1
echo 100 > /sys/fs/cgroup/<id>/pids.max # v2, same idea
Once the group holds 100 tasks, the next fork() or clone() fails with EAGAIN before anything is allocated. Nothing is killed, no memory is reclaimed, no signal is sent — the syscall simply refuses, and a well-written program can back off and retry. Docker spells it --pids-limit=100.
Choosing the number
The counter includes threads, so it is bigger than the number of processes you can name. Rough starting points: 10 for a single-process microservice with a health-check helper, 100 for a typical web application with a worker pool, 4096 for a CI runner that shells out to arbitrary build tools. The default, max, is the risky one — it is not a limit at all, and it is what every container without the flag gets.
Two things to know before you set it low: EAGAIN reaches your application, not the daemon, so a library that treats a failed fork as fatal will crash rather than degrade. And the count is live — processes exiting frees slots immediately, so a container that hits the ceiling under burst load recovers on its own.
Why the exercise is a simulator
fork() inside a grader sandbox is neither safe nor permitted, and forking is not the lesson. pids.max is a counter and a comparison; writing it is writing the controller.
Your exercise: PID Limit Enforcement
| The kernel does | Your simulator does |
|---|---|
mkdir a pids cgroup — pids.max defaults to max | CGROUP <name> — count 0, unlimited |
echo N > pids.max | PIDSMAX <name> <n> |
fork(): succeeds and increments, or returns EAGAIN at the ceiling | FORK <name> — OK <count> or EAGAIN |
| a task exits; the slot is free immediately | EXIT <name> — OK <count> |
cat pids.current pids.max | STATUS <name> |
The comparison is count >= max, checked before the increment — with pids.max = 3 the third fork succeeds and the fourth fails, so a container at its limit is at exactly the limit, never one over. EAGAIN must not change the counter. And pids.max = 0 is a legal group that can hold no tasks at all, which is the same edge you met with a memory limit of zero.
Reference: cgroup v2 pids.max.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…