Skip to content

fork

Memory kills, CPU throttles, pids refuses

Three limits on a cgroup, and three completely different failures. Somebody who has only seen “the container died” has seen the first and assumed the other two behave the same way.

A cgroup is an accounting boundary

Not a sandbox. A cgroup counts what the processes in it are using and refuses to let them go past a limit, and the way it refuses is different for every limit.

memory.max kills. When the total goes over, the OOM killer picks a process in the cgroup and takes it — the process gets no warning and no chance to clean up, because it is killed with a signal it cannot catch.

cpu.max throttles. Nothing dies; everything gets slower. A CPU-limited container looks exactly like a slow disk, and nobody thinks to look at the cgroup.

pids.max refuses. fork returns EAGAIN, and most programs do not check fork's return value at all.

Three failures that look nothing alike, and one of them is silent unless the program checks a return value.

three-limits-three-failures

The quiet one

pids.max is the limit nobody notices, because fork failing is something most programs do not check for. A supervisor that forks a worker per job and never tests the return value will silently stop starting work, with no error anywhere and every process apparently healthy.

Try this
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
  for (int i = 0; i < 6; i = i + 1) {
    pid_t pid = fork();
    if (pid == 0) {
      _exit(0);
    }
    if (pid < 0) {
      printf("fork failed at %d\n", i);
      return 1;
    }
    int status;
    waitpid(pid, &status, 0);
  }
  return 0;
}

Watch: Which iteration it stops at, and what the trace says `clone` returned.

three-limits-three-failures

The limit applies to everything underneath

A limit is on the subtree, not on the one directory. Splitting a workload into child cgroups does not give it more memory, and a child with no limit of its own is still bounded by its parent's.

Writing a larger number into a child changes nothing and reads back as the number you wrote — which is why cat memory.max inside a container is not the answer to "how much memory do I have". The answer is the tightest limit on the path to the root, and nothing shows it to you.

three-limits-three-failures

Before you run it

A cgroup holds a supervisor that allocated almost nothing and a worker that allocated nearly all of the limit. The worker allocates a little more and the cgroup goes over.

Predict

Which process does the OOM killer take?

three-limits-three-failures

Trace

Run one of the programs on the left.

Everything you do here stays in this browser.Part of liter8.sh