Linux — Level by Level›14 · cgroups & namespaces

Lesson 14 of 19 · Level 3 — Advanced: kernel & security

cgroups & namespaces

A container is just a Linux process with namespaces (what it can see) and cgroups (what it can use). Build that from scratch with unshare and systemd-run, then use the same tools to debug real containers.

Advanced
Key wordsnamespacescgroups v2unsharensenterlsnssystemd-runMemoryMaxcontainer internals
A "container" just a Linux process with limits and its own view namespaces: what it can SEE pid own process tree net own interfaces, IPs mnt own filesystem uts own hostname ipc own shared memory user own UID mapping cgroups: what it can USE cpu shares / quota memory limit → OOM kill io disk bandwidth pids max processes
A container is a process with namespaces (what it sees) and cgroups (what it uses).

A container is a process with blinkers and a budget

There's no "container" object in the Linux kernel. A container is an ordinary process that the runtime starts with:

  • Namespaces: what it can see (its own process list, network, filesystem, hostname).
  • cgroups: what it can use (CPU, memory, I/O, number of processes).
  • A root filesystem from the image (overlayfs), plus security limits (capabilities, seccomp, SELinux/AppArmor).

Picture a horse in a big field. Namespaces are blinkers: the horse only sees its own little patch, and thinks it's alone. cgroups are a feeding schedule: however hungry it is, it only gets its portion of hay. The field (the kernel) is shared. It just doesn't look that way to the horse.

Namespaces

Namespace Isolates
pid Process IDs: the container sees its own PID 1
net Interfaces, IPs, routes, firewall rules, ports
mnt Mount points: its own root filesystem
uts Hostname
ipc Shared memory, message queues
user User and group IDs (root inside ≠ root outside)
cgroup Its view of the cgroup tree

Try a PID namespace:

$ sudo unshare --pid --fork --mount-proc bash
# ps aux
USER   PID %CPU %MEM    VSZ   RSS TTY   STAT START   TIME COMMAND
root     1  0.0  0.0   8944  5412 pts/0 S    11:40   0:00 bash
root     8  0.0  0.0  10884  4352 pts/0 R+   11:40   0:00 ps aux
# exit

Your shell is PID 1, and it can't see any other process on the host.

Try a network namespace:

$ sudo unshare --net bash
# ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# exit

A completely empty network stack. A CNI plugin's job is to plug a virtual cable (a veth pair) into namespaces like this one and give it an IP.

cgroups v2: budgets

cgroups organise processes into a tree and apply limits at each node. Modern distributions use cgroup v2, a single hierarchy under /sys/fs/cgroup:

$ stat -fc %T /sys/fs/cgroup
cgroup2fs
$ systemd-cgls --no-pager | head -12
Control group /:
-.slice
├─user.slice
│ └─user-1000.slice
├─init.scope
│ └─1 /sbin/init
└─system.slice
  ├─nginx.service
  │ ├─1487 nginx: master process /usr/sbin/nginx
  │ └─1488 nginx: worker process
  ...

systemd creates a cgroup for every service. You can start something with limits directly:

$ sudo systemd-run --unit=demo -p MemoryMax=64M -p CPUQuota=20% sleep 600
Running as unit: demo.service
$ cat /sys/fs/cgroup/system.slice/demo.service/memory.max
67108864
$ cat /sys/fs/cgroup/system.slice/demo.service/cpu.max
20000 100000

cpu.max 20000 100000 = 20 ms of CPU per 100 ms period = 20% of one CPU. This is exactly how the kubelet turns a pod's limits.cpu: 200m into kernel settings.

cgroup v2 file Meaning Kubernetes equivalent
memory.max Hard memory limit (OOM above it) limits.memory
cpu.max CPU quota per period limits.cpu
cpu.weight Relative share when CPUs are contended derived from requests.cpu
pids.max Maximum number of processes pod PID limits
memory.current, cpu.stat Current usage, throttling counters what metrics read

Debugging real containers with these tools

On a Kubernetes node, find a container's host PID and look inside its namespaces with the host's tools:

$ sudo crictl ps --name nginx -q | head -1
5d2c1a9e7f3b
$ PID=$(sudo crictl inspect --output go-template --template '{{.info.pid}}' 5d2c1a9e7f3b)
$ sudo nsenter -t "$PID" -n ip addr          # the pod's network view
$ sudo nsenter -t "$PID" -n ss -ltn          # what it's listening on
$ cat /proc/$PID/cgroup                       # its cgroup path under kubepods.slice

This works even when the container image has no shell or tools.

Try it: build a 'container' by hand

  1. Start a PID namespace with unshare and confirm you're PID 1; from another terminal, find the same shell's host PID with ps -ef | grep unshare.
  2. Start a network namespace and check that ping 1.1.1.1 fails ("Network is unreachable").
  3. Use systemd-run with MemoryMax=64M to run stress-ng --vm 1 --vm-bytes 200M --timeout 30s. Check journalctl -u <unit> and dmesg for the OOM kill. (Stop the earlier demo unit first: sudo systemctl stop demo.)
  4. Run systemd-cgtop and find your unit's CPU and memory use.

Going deeper: namespaces and cgroups in production

  • User namespaces map root in a container to an unprivileged user on the host. Kubernetes supports this per pod (hostUsers: false), a big win for isolation.
  • Containers share the kernel. A kernel exploit breaks every namespace boundary. For untrusted code use sandboxed runtimes (gVisor, Kata Containers) or VMs.
  • The kubelet's cgroup layout (kubepods.slice → QoS class → pod → container) is how QoS classes map to eviction and CPU weight (see Kubernetes Administration, lesson 23).
  • PSI (pressure stall information: cpu.pressure, memory.pressure, io.pressure in each cgroup) shows how much time tasks spent waiting for a resource, a better saturation signal than usage.

Recap

  • Container = process + namespaces (what it sees) + cgroups (what it uses) + image filesystem + security settings.
  • unshare creates namespaces; nsenter joins them; lsns lists them.
  • cgroup v2 lives in /sys/fs/cgroup; systemd-run -p MemoryMax=… -p CPUQuota=… applies limits, just like the kubelet does.
  • Debug containers from the node with crictl inspect + nsenter.

This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.