A Linux server has 32 CPUs and 64 GiB of memory. One API instance should be allowed to use at most two CPUs' worth of time and 4 GiB of memory, even when the rest of the machine is idle.
A separate PID namespace can change which processes the API sees. A mount namespace can change which file-system tree it sees. Neither mechanism limits how much CPU time or memory those processes consume.
Linux uses control groups, commonly called cgroups, for that job.
A cgroup collects processes into an accounting and control boundary. Kernel controllers measure the resources charged to that group and apply policies such as relative CPU weight, CPU-time quota, memory boundaries, I/O limits, and task-count limits.
An API process tree belongs to one cgroup, and that cgroup's controller policies govern CPU, memory, I/O, and task accounting or control.
Namespaces control what processes can see. Cgroups control how processes are grouped, accounted, prioritized, and limited.
Cgroups are not specific to containers. Linux uses them for system services, login sessions, user workloads, virtual-machine processes, and any other tasks that need a shared resource boundary.
Two ideas form the cgroup model.
First, a cgroup has member processes. The kernel can account for those processes as one workload even when the workload contains many processes and threads.
Second, controllers attach resource-specific behavior to the group. The CPU controller understands CPU time and scheduling weight. The memory controller understands charged memory. The I/O controller understands block-device I/O.
The group is stable even when individual processes come and go. A supervisor can start a new worker inside the same cgroup, and its resource use contributes to the same totals.
This is more useful than limiting one PID at a time. A backend service may fork workers, create many threads, and invoke helper programs. The cgroup provides a boundary around the complete process tree rather than one initial executable.
Cgroups are organized as a tree.
A production host might use a structure such as:
Limits set on api.service apply to everything beneath it. The two replicas share that budget rather than each receiving it.
Each child is constrained by its ancestors.
If api.service has a 4 GiB memory maximum, its two replica cgroups cannot collectively escape that boundary. A child can receive a smaller maximum, but it cannot make the parent's limit disappear.
CPU weight is distributed hierarchically as well. The scheduler first distributes contested CPU capacity between sibling groups, then distributes the capacity received by a group among that group's children and tasks.
The weights do not flatten into one machine-wide number. replica-2 competes first with replica-1 for the capacity their parent receives. The parent competes separately with its siblings.
Hierarchy lets an operator express both organizational and resource boundaries: all API replicas belong to one service budget, while each replica can receive its own policy inside that budget.
Linux has two major cgroup interface generations.
Cgroup v1 can organize different controllers in separate hierarchies. A process might belong to one group for CPU and another for memory. Its control files use names such as cpu.cfs_quota_us and memory.limit_in_bytes.
Cgroup v2 uses one unified hierarchy. A process has one cgroup path that applies across the enabled controllers, and the interface follows consistent delegation and hierarchy rules.
Modern Linux distributions commonly use cgroup v2, and this chapter uses its interface.
Check the mounted file-system type:
On a unified v2 system, the result is:
The current process's membership commonly appears as one line:
Example:
The empty controller field between the two colons is characteristic of the unified v2 hierarchy.
Some systems use cgroup v1 or a hybrid arrangement. Do not combine v1 filenames with a v2 path; first identify the hierarchy that actually controls the target workload.
Cgroup v2 is normally mounted at /sys/fs/cgroup. It is a kernel-managed pseudo-file system, not an ordinary disk-backed directory tree. Creating a directory such as /sys/fs/cgroup/example creates a cgroup.
Writing configuration files changes kernel policy. Reading status files retrieves live kernel state.
Every cgroup can expose core files such as:
cgroup.procs for process membershipcgroup.controllers for controllers available to childrencgroup.subtree_control for controllers enabled in childrencgroup.events for general state such as whether descendants are populatedcgroup.type for the cgroup's structural modeController-specific files appear when the corresponding controller is available at that level. Examples include cpu.max, memory.current, io.stat, and pids.max.
The files are interfaces, not stored configuration text. A successful write asks the kernel to validate and apply a policy immediately. Invalid values and forbidden hierarchy changes fail with an error.
A controller available to a parent is listed in its cgroup.controllers file:
Example:
To distribute CPU control to child cgroups, the parent enables it in cgroup.subtree_control:
Afterward, eligible children expose CPU controller files.
This split supports delegation. A parent decides which resource controllers its descendants may manage. A child cannot enable a controller its parent did not delegate.
Cgroup v2 also applies a no internal process rule for ordinary domain controllers: a non-root cgroup generally cannot both contain member processes and distribute domain resources to child cgroups. Processes belong in leaf cgroups, while internal nodes organize and control descendants. A service parent can therefore be empty while replica-1 and replica-2 leaf cgroups contain the actual workload processes.
Threaded cgroups provide a specialized exception for thread-level organization, but normal services and containers should use the simpler domain hierarchy.
On a system managed by systemd, the service manager already owns much of this tree. Arbitrarily enabling controllers or moving processes under /sys/fs/cgroup can conflict with it. Production software should use delegated subtrees or service-manager properties rather than modifying system-owned nodes.
The cgroup.procs file lists process IDs belonging directly to a cgroup:
Writing a PID migrates that process into the target cgroup:
The write is subject to permission, hierarchy, and thread-group rules. In the normal domain model, moving a process through cgroup.procs moves its threads together.
A newly created child process inherits its parent's cgroup membership. This keeps a service's future workers inside the service boundary without requiring the runtime to discover and move each child afterward.
Moving an existing parent does not automatically move every child process it created in the past. Process ancestry and cgroup hierarchy are different structures. A manager that needs to migrate a complete workload must identify and move the correct process group or create it inside the target cgroup from the beginning.
Individual threads can be organized through cgroup.threads in a valid threaded subtree. That model is useful for specialized runtimes but is not the normal container boundary.
The CPU controller records CPU time used by the cgroup and its descendants.
Read:
Typical fields include:
usage_usec is total consumed CPU time. user_usec and system_usec divide that time between user and kernel execution.
The period and throttling fields relate to CPU bandwidth limits. They accumulate over the cgroup's lifetime, so a nonzero value is historical evidence rather than proof that throttling is happening at the current moment.
CPU time is additive across parallel execution. Two threads each running for 100 milliseconds on separate CPUs consume about 200 milliseconds of CPU time during 100 milliseconds of wall-clock time.
cpu.weight controls a cgroup's relative share of CPU capacity when sibling cgroups compete.
The default weight is 100, and valid values range from 1 to 10000.
Suppose two continuously runnable sibling groups give the API a cpu.weight of 300 and a batch workload a weight of 100.
Under sustained contention for the same CPUs, the API group is eligible for roughly three times the CPU share of the batch group, subject to hierarchy, affinity, and other scheduling constraints.
Weight is not a reservation and not a ceiling.
If the batch group is idle, the API can use the available CPUs rather than stopping at 75%. If the host has spare capacity, even a low-weight group can run freely. Weight becomes meaningful when eligible siblings compete for constrained CPU time.
cpu.max configures a quota followed by a period, both measured in microseconds. The default commonly reads max 100000.
max means no hard CPU bandwidth quota at this cgroup level.
The setting 50000 100000 allows 50 milliseconds of aggregate CPU execution during each 100-millisecond period. That is half of one CPU's sustained capacity.
The setting 200000 100000 allows two CPUs' worth of sustained execution. It does not pin the group to two particular CPU cores.
Quota is consumed across all runnable threads in the cgroup. With a 50-millisecond quota, two threads running simultaneously can exhaust the budget in about 25 milliseconds of wall-clock time.
Once the budget is exhausted, otherwise runnable tasks wait until the next period replenishes it. Idle CPUs elsewhere on the host do not override the cgroup's policy.
cpu.max is a ceiling, not a guarantee that the workload will receive that much CPU. Host contention, ancestor limits, CPU affinity, and higher-priority work can all reduce what it actually gets.
These bandwidth controls apply to ordinary fair-scheduled workloads. Specialized real-time scheduling policies use different admission and bandwidth rules.
cpu.stat provides evidence about quota enforcement.
The relevant fields are:
nr_periods: number of CPU bandwidth enforcement periods observednr_throttled: number of times the cgroup was throttledthrottled_usec: accumulated time during which its runnable work was throttledTo measure current behavior, sample the counters twice:
Increasing nr_throttled shows that the group exhausted its quota during the interval. The ratio nr_throttled / nr_periods says how often this occurred, but it does not by itself quantify application impact.
Throttling is different from ordinary scheduler waiting. A runnable task can wait because another task currently owns a CPU even when no quota exists. Throttling means cgroup policy has temporarily made the task ineligible to run.
The cpuset controller restricts where work may execute.
cpuset.cpus requests a set of CPUs, while cpuset.cpus.effective reports the CPUs actually available after ancestor constraints are applied. Setting cpuset.cpus to 2-5 allows the group's tasks to run only on CPUs 2 through 5.
This is different from quota. A cpuset determines which CPUs may run the workload, quota determines how much aggregate CPU time it may consume, and weight determines how contested CPU time is shared with siblings.
A group can be allowed on four CPUs but limited to one CPU's worth of time. Conversely, a group with no quota might still run only on two CPUs because of its effective cpuset.
cpuset.mems and its effective counterpart constrain eligible NUMA memory nodes. Correct cpuset configuration must respect the sets allowed by ancestors.
The memory controller accounts memory charged to a cgroup and its descendants.
Current usage is exposed through:
The total can include anonymous memory, file-backed memory and page cache, kernel memory used on the group's behalf, page tables, socket buffers, and shared-memory charges.
This is why cgroup memory usage is not equivalent to the sum of process RSS values.
Summed RSS can double-count pages shared by several processes, while cgroup accounting can include charged kernel and cache memory not obvious in an application heap metric.
memory.stat provides a category breakdown:
The exact fields depend on the kernel. Common categories include anon, file, kernel, slab, sock, pagetables, and shmem.
Cgroup v2 provides several memory policies rather than one generic “memory limit.”
memory.max is the hard usage boundary. If the cgroup cannot reclaim enough memory to satisfy a charge without exceeding this boundary, local out-of-memory handling may select a process within the cgroup.
memory.high is a throttling and reclaim boundary. Crossing it places work under heavy reclaim pressure but does not directly serve as the hard kill limit.
memory.low provides best-effort protection from reclaim below a configured amount when memory is contested. memory.min provides stronger protection and must be configured carefully because excessive protected memory can leave the system unable to reclaim enough.
memory.swap.max limits swap usage charged to the cgroup independently from the main memory boundary.
Relevant state is exposed through:
The file contains counters for events such as crossing memory.high, reaching memory.max, entering an OOM condition, and OOM kills.
These settings are hierarchical. A child can be below its own memory.max while the combined usage of siblings reaches an ancestor's boundary.
The I/O controller accounts for and controls block-device I/O.
io.stat reports activity by device:
An entry begins with the device's major and minor number and can include read bytes, write bytes, and I/O counts.
io.weight expresses relative priority between sibling cgroups when the device and I/O scheduler support proportional control.
io.max configures absolute limits. For example, 8:0 rbps=10485760 wbps=5242880 limits reads from device 8:0 to 10 MiB/s and writes to 5 MiB/s.
The device identity matters because limits apply at a block-device layer, not to an arbitrary pathname. Use:
to relate mounted storage to device numbers on a straightforward local system.
Buffered I/O can temporarily accumulate in memory before write-back reaches the device. Application-visible write timing therefore does not always align immediately with the block I/O charged by the controller.
The PID controller limits the number of tasks in a cgroup and its descendants.
Its primary files are pids.current, pids.max, and pids.events. Setting pids.max to 200 allows at most 200 tasks within that cgroup subtree.
Threads count toward this boundary because Linux threads are kernel-scheduled tasks. A program with one process and 150 threads has consumed far more than one unit of the PID budget.
When the limit prevents another task from being created, operations such as fork() or thread creation fail, commonly with EAGAIN. The kernel does not need to kill existing tasks merely because the boundary has been reached.
The PID controller prevents a fork bomb or runaway thread creator from exhausting the host's complete task capacity. It does not change which PIDs the workload can see; that is the job of a PID namespace.
Available controllers depend on kernel configuration and hardware.
The hugetlb controller accounts for and limits explicitly allocated huge pages. The rdma controller constrains selected RDMA resources. The misc controller supports limits for scalar resources registered by kernel subsystems.
Cgroup v2 also exposes useful core controls that are not ordinary resource controllers.
Writing 1 to cgroup.freeze freezes the cgroup's tasks, and writing 0 allows them to run again. cgroup.events reports whether the cgroup is populated and whether it is frozen.
Per-cgroup pressure stall information can appear in cpu.pressure, memory.pressure, and io.pressure.
These files measure time tasks lost because the corresponding resource was unavailable. They complement usage and limit counters: high CPU usage says work executed, while CPU pressure says runnable work spent time waiting.
There is no general cgroup v2 controller that simply sets “network bandwidth” through a network.max file. Linux networking policies commonly use traffic control, firewalling, BPF, or platform-specific mechanisms alongside cgroup identity.
Cgroup controls fall into different semantic categories.
A maximum such as cpu.max, memory.max, or pids.max creates a ceiling. It does not promise the workload will receive the maximum.
A weight such as cpu.weight or io.weight determines relative sharing under contention. It does not stop a group from using idle capacity.
A placement rule such as cpuset.cpus determines which resources are eligible. It does not define how much time the workload receives on them.
A protection such as memory.low influences reclaim priority under pressure. It is not the same as preallocating or reserving physical memory exclusively.
Calling all four “limits” obscures how the kernel will behave when the host is idle, contended, or out of capacity.
Loading simulation...
On most modern general-purpose Linux systems, systemd builds and manages the cgroup hierarchy.
It creates units under structures such as system.slice, user.slice, and machine.slice.
Services and scopes become cgroups. Unit settings map to cgroup controls:
CPUWeight= maps to relative CPU weightCPUQuota= maps to CPU bandwidth quotaMemoryHigh= and MemoryMax= map to memory boundariesTasksMax= maps to the PID controllerIOWeight= maps to relative I/O weightFor example:
This asks the user's service manager to create a transient scope for the command. It works only when the user manager and delegated hierarchy permit the requested controls.
Container runtimes likewise create cgroups directly or ask systemd to create appropriate scopes. A runtime option such as a CPU or memory limit ultimately becomes kernel cgroup configuration.
Using the service manager preserves ownership and delegation rules. Hand-editing a unit's cgroup files can be overwritten or conflict with later service-manager actions.
Inspect a process's unified cgroup path through /proc/<pid>/cgroup. For the current shell:
Suppose the result is:
On a host whose cgroup v2 hierarchy is mounted normally, the corresponding directory is /sys/fs/cgroup/system.slice/api.service.
Inspect its membership and policies:
Inside a container, a cgroup namespace may make the visible path relative to a container-specific root. The mounted /sys/fs/cgroup view can also be restricted or read-only. Always interpret the path from the environment in which the inspection runs.
Run this experiment only in a disposable cgroup v2 Linux environment where you have administrative access and the CPU controller is already delegated to the root's children.
Confirm the prerequisites:
The first command should print cgroup2fs, and the second should include cpu. If cpu is absent, do not modify a production hierarchy merely for the experiment; use a delegated subtree or a disposable VM configured by an administrator.
Create one leaf cgroup and set a quota of half a CPU:
Start one CPU-bound shell, record its PID, and move it into the cgroup:
Sample its CPU state twice:
usage_usec should increase by roughly half of the elapsed wall time, subject to other scheduling effects. nr_throttled and throttled_usec should also increase because the busy process repeatedly exhausts its period budget.
Stop and reap the exact demonstration process, then remove the empty cgroup:
rmdir succeeds only after the cgroup has no member processes or child cgroups. Removing the directory removes the cgroup object; it is not a command for killing a populated workload.
Consider an API with one coordinator, eight worker processes, and many threads.
The service manager places the entire process tree into one cgroup and configures:
The memory values are byte counts corresponding to 3 GiB and 4 GiB. The CPU quota permits two CPUs' worth of aggregate execution per period. Eight workers do not multiply it into sixteen CPUs. If many worker threads run simultaneously, they consume the shared budget faster.
The CPU weight gives the API favorable sharing relative to lower-weight sibling services when they contend. It does not reserve 75% of the machine and does not prevent the API from using idle capacity below its quota.
The memory controller charges the coordinator, workers, file cache, and relevant kernel allocations to one service boundary. memory.high creates reclaim pressure before memory.max becomes the hard backstop.
The PID limit covers processes and threads together. A worker that accidentally creates unbounded threads cannot consume every task slot on the host.
The cgroup turns a changing process tree into one resource unit. The service can restart workers and create helpers without escaping the policies inherited from its parent cgroup.
Cgroups organize processes into hierarchical accounting and resource-control boundaries. Controllers apply CPU, memory, I/O, task-count, placement, and other policies to the complete group and its descendants.
Cgroup v2 uses one unified hierarchy. Processes join through cgroup.procs, children inherit membership, parents enable controllers through cgroup.subtree_control, and non-root internal nodes normally organize leaf cgroups rather than containing workload processes themselves.
Weights determine relative sharing under contention, quotas and maxima impose ceilings, cpusets constrain placement, and memory protections influence reclaim. These policies are not interchangeable.
The cgroup file system exposes both configuration and evidence: cpu.max, cpu.stat, memory.current, memory.events, io.stat, pids.current, and related files describe the workload's actual resource boundary.
The central mental model is:
A namespace changes the workload's view; a cgroup defines the resource domain in which the workload is accounted and controlled.
5 quizzes