Linux cgroups v2 Tutorial: Control Groups Explained for Beginners-Best Linux Device Drivers Training In Hyderabad

← Previous Lecture Next Lecture →

Linux cgroups v2 Tutorial: Control Groups Explained for Beginners

Part of our free Linux kernel development course — learn how the kernel groups processes and puts limits on the resources they consume

Level
Beginner to Intermediate
Kernel
6.x (modern)
Course
Linux Kernel Programming

Every process on a Linux machine competes for the same CPU cores, the same RAM, and the same disk bandwidth. What happens when one badly behaved program tries to eat everything? This Linux cgroups v2 tutorial answers exactly that question. Control groups (cgroups) are the kernel feature that lets you place processes into named groups and then attach resource limits to each group. Containers, Docker, Kubernetes, and systemd all sit on top of this one mechanism.

This lesson is part of our free Linux kernel development course here on EmbeddedPathashala. In Part 1 you will build a clear mental model of cgroups v2: what it is, how it differs from the older v1 design, which controllers exist on a modern 6.x kernel, and how the whole thing is exposed as a filesystem. In Part 2 we get hands-on and use the CPU controller to throttle real processes.

What You Will Learn

What cgroups are and why the kernel needs them cgroups v1 vs cgroups v2 differences The unified hierarchy design Controllers available on modern kernels The cgroup2 pseudo filesystem Core interface files you must know

Prerequisites

You should be comfortable with the Linux command line, know what a process and a PID are, and have basic familiarity with pseudo filesystems such as /proc and /sys. Any recent Linux distribution works for this Linux cgroups v2 tutorial: Ubuntu 22.04 or newer, Fedora, Debian 11 or newer, or RHEL 9 family. All of these boot with cgroups v2 as the default, so you do not need any special kernel command-line options. Root access (sudo) is needed for the hands-on parts. If you came here from our free embedded systems course or free Linux device drivers course, this lesson fits right after the process management module.

What Are Control Groups in Linux?

A control group is a named collection of processes with resource controls attached to it. Think of it as a labelled box. You drop one or more processes into the box, and then you tell the kernel: everything inside this box may use at most 30 percent of a CPU, or at most 512 MB of memory, or at most 100 forked children. The kernel enforces the limit continuously, no matter how the processes inside behave.

The feature first appeared in kernel 2.6.24 back in 2008, originally developed at Google under the name “process containers”. Over the years it grew organically, controller by controller, and the original design (now called cgroups v1) accumulated real problems. That led to a ground-up redesign, cgroups v2, which was merged in kernel 4.5 and has been the default on mainstream distributions for several years now. Everything in this free Linux kernel development course lesson targets cgroups v2 on a modern kernel.

Why should an embedded or kernel developer care? Three practical reasons:

  • Resource guarantees: on an embedded board you can guarantee that a critical daemon always gets CPU time even when a logging task misbehaves.
  • Containment: a runaway process, or even a deliberate fork bomb, can be boxed in so it cannot take the system down.
  • Containers: every container runtime you will ever touch is built on cgroups plus namespaces. Understanding cgroups means understanding half of how containers actually work.

cgroups v1 vs cgroups v2: What Changed and Why

The single biggest change is the move from multiple hierarchies to one unified hierarchy. In v1, each controller could be mounted as its own independent tree. A process could sit in one position in the CPU tree and a completely different position in the memory tree. That sounded flexible, but in practice it made coordinated resource management between controllers nearly impossible, and every controller ended up behaving slightly differently.

cgroups v2 fixes this with one tree for everything. A process belongs to exactly one cgroup, and all controllers see the same tree. The v2 design also moved from thread-level to process-level granularity by default (a limited “thread mode” exists for special cases), because splitting the threads of one process across different memory cgroups never made sense: threads share one address space.

cgroups v1 (multiple trees) vs cgroups v2 (one unified tree)
cgroups v1
cpu hierarchy
grpA → grpB
memory hierarchy
grpX → grpY
blkio hierarchy
separate tree again
One process = many positions. Hard to coordinate.
cgroups v2
root cgroup (/sys/fs/cgroup)
system.slice user.slice
my_group (cpu + memory + io on one tree)
One process = one position. All controllers cooperate.

Here is a side-by-side comparison you can keep as a reference:

Aspect cgroups v1 cgroups v2
Hierarchy Multiple trees, one or more controllers per tree Single unified tree for all controllers
Process membership A process can be in many cgroups (one per hierarchy) Exactly one cgroup per process
Granularity Per thread Per process (limited thread mode available)
Mount point (typical) Many mounts under /sys/fs/cgroup/<controller> One cgroup2 mount at /sys/fs/cgroup
Controller enabling Chosen at mount time Runtime, via cgroup.subtree_control
Status today Legacy, kept for compatibility Default on all mainstream distributions

Which version is my system running?

One command tells you. If the filesystem type of the cgroup mount point is cgroup2fs, you are on a pure v2 system:

$ stat -fc %T /sys/fs/cgroup/
cgroup2fs

You can double check with the mount table:

$ mount | grep cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)

Notice the mount point is directly /sys/fs/cgroup. Older tutorials and older books show it at /sys/fs/cgroup/unified alongside a forest of v1 mounts, and they tell you to boot with the cgroup_no_v1=all kernel parameter to get a clean v2 setup. That advice is outdated. On any current distribution, systemd mounts a pure cgroup2 filesystem at /sys/fs/cgroup out of the box and no boot parameter is required. If you are stuck on a very old install that still shows the hybrid layout, either upgrade or add systemd.unified_cgroup_hierarchy=1 to the kernel command line.

Controllers in cgroups v2 on a Modern Kernel

A controller (also called a resource controller or subsystem) is the piece of kernel code that actually enforces limits for one resource type. On a current 6.x kernel, the root cgroup typically advertises these controllers:

$ cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc

The exact list depends on your kernel configuration, but this is what a stock distribution kernel shows today. Note this is a longer list than what older books print (they usually show only cpu io memory pids), because more controllers gained v2 support over the years: cpuset in 5.0, freezer as a core feature in 5.2, hugetlb in 5.6, and the misc controller later still.

Controller What it limits or distributes Typical use case
cpuCPU time: proportional weight and hard bandwidth capsThrottle a batch job to 30% of one core
cpusetWhich CPU cores and NUMA memory nodes a group may usePin a real-time task set to isolated cores
memoryRAM, swap, and kernel memory usageCap a container at 512 MB
ioBlock device bandwidth and IOPSStop a backup job from saturating the disk
pidsNumber of processes (tasks) that can exist in the groupDefend against fork bombs
hugetlbHugeTLB page usageDatabases using huge pages
rdmaRDMA/InfiniBand hardware resourcesHPC clusters
miscScalar resources that do not fit elsewhere (for example SEV encryption ASIDs)Confidential computing VMs

A quick word on the pids controller, because it is the easiest one to appreciate. A fork bomb is a trivial denial-of-service attack: a loop that calls fork() forever until the process table is exhausted and the machine locks up. Put your untrusted workload into a cgroup with pids.max set to, say, 100, and the bomb fizzles out at 100 processes while the rest of the system stays perfectly healthy. One line of shell replaces an entire class of outage.

Two more things worth knowing. The freezer became a core v2 feature rather than a separate controller: every cgroup gets a cgroup.freeze file that suspends and resumes the whole group. And the v1 devices controller has no v2 file interface at all; device access control in v2 is done with BPF programs attached to the cgroup, which is how container runtimes implement it today.

The cgroup2 Filesystem: Everything Is a Directory

Like /proc and /sys, cgroups are exposed through a purpose-built pseudo filesystem. There are no dedicated system calls to create a cgroup or set a limit. You use ordinary filesystem operations:

  • mkdir creates a new cgroup as a child of wherever you created the directory.
  • echo a value into a file configures a limit.
  • cat a file reads current settings or live statistics.
  • rmdir removes an empty cgroup.

When you make a directory under /sys/fs/cgroup, the kernel instantly populates it with interface files. Files starting with cgroup. are core files that exist in every cgroup. Files with a controller prefix (cpu., memory., pids. and so on) appear only when that controller is enabled for the group. The core files you will touch most often:

Interface file Purpose
cgroup.controllersRead-only list of controllers available to this cgroup
cgroup.subtree_controlWhich controllers are enabled for the children of this cgroup; write “+cpu” or “-memory” style strings here
cgroup.procsPIDs of member processes; write a PID here to move a process in
cgroup.eventsPopulated/frozen state, useful for monitoring
cgroup.freezeWrite 1 to suspend every process in the group, 0 to resume
cgroup.killWrite 1 to kill the entire group atomically (kernel 5.14+)
How a limit flows from you to the kernel
1. mkdir
/sys/fs/cgroup/my_group
→ 2. echo “+cpu”
into parent subtree_control
→ 3. echo limit
into my_group/cpu.max
→ 4. echo PID
into my_group/cgroup.procs
The CPU scheduler now enforces the limit on every process in my_group, automatically, forever

One important v2 rule to remember: resources flow top-down. A cgroup can hand a controller to its children only if the controller was handed to it by its parent. In other words, a controller name can appear in a cgroup’s subtree_control only if it also appears in the parent’s subtree_control. There is also the “no internal process” rule: once a cgroup enables controllers for its children, processes can live only in the leaf cgroups, not in the intermediate ones (the root cgroup is exempt). These two rules keep resource accounting unambiguous, and they explain most of the “permission denied” surprises beginners hit. We deal with both rules practically in Part 2.

Where does systemd fit in?

On a modern distribution, systemd is the primary owner of the cgroup tree. It builds a standard layout at boot: system.slice for services, user.slice for login sessions, machine.slice for VMs and containers. Every systemd service already lives in its own cgroup, which is how directives like CPUQuota= and MemoryMax= in unit files work under the hood. You can see which cgroup any process belongs to by reading /proc/<PID>/cgroup; on a pure v2 system it contains a single line beginning with 0:: followed by the path inside the hierarchy.

$ cat /proc/$$/cgroup
0::/user.slice/user-1000.slice/session-2.scope

Because systemd manages the tree, the clean way to create your own groups in production is delegation (asking systemd for a subtree you own) or simply using systemd-run. For learning, creating groups by hand under /sys/fs/cgroup works fine, and that is exactly what we do in the next lesson.

Common Mistakes and Troubleshooting

  • Following pre-2020 tutorials. If a guide tells you to look in /sys/fs/cgroup/unified or to boot with cgroup_no_v1=all, it targets the old hybrid layout. On current systems, cgroup2 is mounted directly at /sys/fs/cgroup and those steps are unnecessary.
  • Empty cgroup.controllers in a child group. Not a bug. The parent has not enabled that controller in its subtree_control. Enable it one level up first.
  • Confusing v1 and v2 file names. cpu.cfs_quota_us and cpu.shares are v1 files. Their v2 equivalents are cpu.max and cpu.weight. If the file you expect does not exist, you are probably reading documentation for the other version.
  • Trying to mix versions for one controller. A controller can be active in v1 or v2, never both at once. On a pure v2 system this never comes up, but on old hybrid setups it caused endless confusion.
  • Editing groups owned by systemd. Manually changing files inside system.slice can be silently overwritten by systemd. Create your own top-level group for experiments, or use proper delegation.

Key Takeaways

  • cgroups let the Linux kernel apply resource limits to groups of processes; they are the foundation of containers and systemd resource management.
  • cgroups v2 replaced the multi-hierarchy v1 design with one unified tree, one cgroup per process, and consistent controller behaviour.
  • Modern kernels expose these v2 controllers: cpuset, cpu, io, memory, hugetlb, pids, rdma, and misc.
  • The interface is a pseudo filesystem at /sys/fs/cgroup: mkdir creates groups, echo configures limits, cat reads statistics.
  • Controllers are enabled per subtree through cgroup.subtree_control, resources flow strictly top-down, and processes live in leaf groups.

Conclusion

You now have the complete conceptual map for this Linux cgroups v2 tutorial: what control groups are, why version 2 exists, which controllers a modern kernel offers, and how the filesystem interface works. None of this required writing a single line of C, and yet you are already ahead of many working developers who use containers daily without knowing what enforces their limits.

Theory becomes real when a process you launched gets visibly throttled in front of your eyes. That is Part 2 of this lesson in our free Linux kernel development course: we create a cgroup by hand, enable the CPU controller, cap a busy-loop workload with cpu.max, and measure the difference. If you are following the full free embedded systems course track, keep the kernel documentation for cgroup v2 open in another tab; it is the authoritative reference we cross-check against throughout.

Frequently Asked Questions

What is the difference between cgroups v1 and v2?

cgroups v1 allowed multiple independent hierarchies, one or more controllers per tree, with per-thread granularity. cgroups v2 uses a single unified hierarchy where each process belongs to exactly one cgroup, controllers behave consistently, and management happens per process. v2 is the default on all current mainstream distributions.

How do I check if my Linux system uses cgroups v2?

Run stat -fc %T /sys/fs/cgroup/. If the output is cgroup2fs, you are on a pure v2 system. Output of tmpfs indicates a legacy or hybrid v1 layout.

Which controllers are available in cgroups v2?

On a modern 6.x kernel: cpuset, cpu, io, memory, hugetlb, pids, rdma, and misc. Read /sys/fs/cgroup/cgroup.controllers to see the exact list your kernel provides.

Do I still need the cgroup_no_v1=all boot parameter?

No, not on any recent distribution. That parameter was needed in the hybrid era when v1 and v2 were mounted side by side. Ubuntu 22.04+, Fedora 31+, Debian 11+, and RHEL 9 boot with a pure cgroup v2 hierarchy by default.

Are cgroups the same thing as containers?

No. A container combines cgroups (resource limits) with namespaces (isolation of what a process can see) plus a filesystem image. cgroups are one essential ingredient, not the whole recipe.

Can cgroups prevent a fork bomb?

Yes. The pids controller limits how many processes can exist inside a cgroup. Set pids.max to a sane number for untrusted workloads and a fork bomb hits the ceiling without harming the rest of the system.

How does systemd use cgroups?

systemd places every service, user session, and virtual machine in its own cgroup within slices such as system.slice and user.slice. Unit file directives like CPUQuota, MemoryMax, and TasksMax translate directly into cgroup v2 interface file writes.

Is this lesson enough to work with Docker and Kubernetes resource limits?

It gives you the foundation. Docker’s –cpus and –memory flags and Kubernetes requests/limits all compile down to the v2 interface files covered in this free Linux kernel development course. After Part 2 you will be able to inspect and reason about what those tools configure.

References

  • Linux kernel documentation: Control Group v2 (docs.kernel.org/admin-guide/cgroup-v2.html)
  • man 7 cgroups: the cgroups overview manual page (man7.org)
  • systemd documentation: Control Group APIs and Delegation

Continue the Free Linux Kernel Development Course

Next: get hands-on with the cgroups v2 CPU controller and throttle real processes with cpu.max.

Next Lecture → ← Previous Lecture

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *