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.
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
- Start a PID namespace with
unshareand confirm you're PID 1; from another terminal, find the same shell's host PID withps -ef | grep unshare. - Start a network namespace and check that
ping 1.1.1.1fails ("Network is unreachable"). - Use
systemd-runwithMemoryMax=64Mto runstress-ng --vm 1 --vm-bytes 200M --timeout 30s. Checkjournalctl -u <unit>anddmesgfor the OOM kill. (Stop the earlierdemounit first:sudo systemctl stop demo.) - Run
systemd-cgtopand 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.pressurein 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.
unsharecreates namespaces;nsenterjoins them;lsnslists 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.