Skip to content
Lesson 8 of 19

Step 1 of 5 · Reading · ~3 min

Read

cgroups & Resource Limits

cgroups: Memory Limits

Namespaces answer what can this process see. They say nothing about how much it can take — a process in six namespaces can still allocate every page on the host and take the machine down with it. Control groups are the other half of a container, and they are as unglamorous as they sound: a directory tree of files you write numbers into.

The tree is the API

/sys/fs/cgroup/memory/                      # cgroup v1
  └── docker/
      └── <container-id>/
            ├── tasks                    # the pids in this group, one per line
            ├── memory.limit_in_bytes    # 524288000 = 500 MB
            ├── memory.usage_in_bytes    # read-only: current charge
            └── memory.failcnt           # how often the limit was hit

Setting a limit is echo 524288000 > memory.limit_in_bytes. Joining a process is echo $PID > tasks. There is no daemon and no library — docker run -m 500m writes those two files, and so could you.

cgroup v2 replaced the per-controller trees with one unified hierarchy and clearer names: memory.max for the limit, memory.current for the charge, memory.swap.max for swap. Most distributions have defaulted to v2 since 2021, but v1 paths are still what you find in older blog posts and in a lot of production tooling, so both names are worth recognising.

Charging happens on the fault, not on the malloc

This is the part that surprises people. malloc() of a gigabyte succeeds instantly and charges the cgroup nothing — the kernel has only handed out address space. The charge lands when the process touches a page and the fault handler has to find real memory for it. So a container's usage climbs while it walks an array it allocated long ago, and a limit can be hit by code that contains no allocation call at all.

When the charge would exceed the limit, the kernel first tries to reclaim (drop clean page cache belonging to this cgroup); if that is not enough, it invokes the OOM killer scoped to the cgroup. It picks a victim inside the group — heuristically the biggest consumer — and SIGKILLs it. Other processes in the container and everything on the host are untouched. That is why "my container died with exit code 137 and there is nothing in the application log" is the signature of a memory limit: 137 is 128 + 9, and SIGKILL leaves no handler a chance to log anything.

memory.soft_limit_in_bytes is a different animal: a hint honoured only when the host is under pressure, never a hard cap.

Why the exercise is a simulator

Writing to /sys/fs/cgroup needs root on the host, which no submission on this platform has. What the files actually implement is a running total compared against a threshold, with one rule that carries all the behaviour: a rejected allocation is not charged.

Your exercise: Memory cgroup Simulator

The kernel doesYour simulator does
mkdir /sys/fs/cgroup/memory/<name>CGROUP <name> — usage 0, no limit
echo N > memory.limit_in_bytes (memory.max in v2)LIMIT <name> <bytes>
a page fault charges the group, or trips the OOM killerALLOC <name> <pid> <bytes>OK, or OOM <pid> with usage unchanged
pages freed, charge returnedFREE <name> <pid> <bytes>
cat memory.current memory.maxSTATUS <name>usage=<n> limit=<n>

Two edges decide most of the tests. A group created but never limited is unlimited, and no allocation can fail in it — that is not a special case, it is the absence of a limit file. And LIMIT of exactly 0 is legal and means every allocation of a non-zero size fails, which is the real behaviour of memory.max = 0 and a good reason not to compute a limit from user input without checking it.

Reference: kernel Documentation/admin-guide/cgroup-v2.rst, memory controller.

Up nextcgroups: CPU Limitscgroups & Resource Limits

Discussion

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

Sign in to post a comment or reply.

Loading…