Skip to content
Lesson 10 of 19

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=auto on 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 doesYour simulator does
mkdir a pids cgroup — pids.max defaults to maxCGROUP <name> — count 0, unlimited
echo N > pids.maxPIDSMAX <name> <n>
fork(): succeeds and increments, or returns EAGAIN at the ceilingFORK <name>OK <count> or EAGAIN
a task exits; the slot is free immediatelyEXIT <name>OK <count>
cat pids.current pids.maxSTATUS <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.

Up nextrootfs: Container FilesystemFilesystems & Images

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…