Skip to content
Lesson 1 of 19

Step 1 of 5 · Reading · ~4 min

Read

Linux Namespaces

PID Namespace

Here's the secret that demystifies Docker in one sentence: a container is not a thing — it's a process that's being lied to. The Linux kernel tells a handful of coordinated lies (namespaces), enforces some spending limits (cgroups), and swaps the floor out from under the process (rootfs). This course builds all three, and we start with the most vivid lie: the process ID namespace.

The lie

Run ps inside any container:

PID  COMMAND
  1  node server.js
 12  ps

That server believes it is PID 1 — the init process, the first process on the machine, the root of the process tree from the kernel course. Meanwhile the host sees the same process as, say, PID 48231. Neither is lying to you: the kernel maintains both numbers simultaneously, and shows each observer the one that belongs to their PID namespace.

A PID namespace is created with one flag at process creation (clone(CLONE_NEWPID)). The first process born into it becomes PID 1 of that namespace; its children get 2, 3, 4… — a fresh, private numbering. The namespace is a one-way mirror:

  • Inside looking out: nothing. The container can't see, signal, or even express the existence of host processes. kill -9 48000 inside the container targets its 48000 (which doesn't exist), never the host's. Isolation isn't a permission check that could have bugs — the outside processes are simply unaddressable.
  • Outside looking in: everything, renumbered. The host sees container processes as ordinary PIDs and can manage them like any other. Parent namespaces contain child namespaces — so one process genuinely holds several PIDs at once, one per namespace level it's visible in.

Why PID 1 is a loaded gun

Becoming PID 1 sounds cosmetic; it isn't. The kernel treats PID 1 specially, and containers inherit both special rules:

  1. Signals: PID 1 ignores any signal it hasn't installed a handler for — even SIGTERM/SIGKILL-adjacent defaults that would kill a normal process. Consequence: docker stop sends SIGTERM to your PID 1; an app that never installs a handler ignores it, waits out the 10-second grace period, and gets hard-killed. Every "my container takes exactly 10 seconds to stop" mystery is this paragraph.
  2. Zombie reaping: when a process dies, its parent must wait() on it or it lingers as a zombie (kernel course, process lesson!). Orphans get re-parented to PID 1 — of their namespace. If your app is PID 1 and it spawns-and-forgets subprocesses, nobody reaps; zombies accumulate. This is why minimal init shims (tini, docker run --init) exist: a 10KB PID 1 whose whole job is wait()ing.

One more everyday consequence: /proc (which ps reads) shows the processes of your PID namespace — which is why mounting a fresh /proc is part of container setup (mount lesson, next) and why ps in a container shows two processes instead of the host's four hundred.

Why the exercise is a simulator

clone(CLONE_NEWPID) requires CAP_SYS_ADMIN. Every program you submit here runs in an unprivileged sandbox with that capability dropped, so a real unshare --pid would fail with EPERM before it printed a byte. That is a real constraint, and it puts you in the same position as a kernel developer reasoning about a subsystem they cannot single-step: you model the bookkeeping. A PID namespace is a struct pid_namespace holding a counter and a table of processes. Reproduce that table and every answer ps gives falls out of it.

Your exercise: PID Namespace Simulator

Four commands, each a syscall with the plumbing stripped off:

The kernel doesYour simulator does
clone(CLONE_NEWPID) allocates a namespace whose counter starts at 1NEWNS — a fresh table with next_pid = 1; emit its id
fork() inside it: alloc_pid() takes the next free number in that namespaceFORK <ns> <name> — hand out that namespace's counter, then bump it
the process dies and nobody wait()s: the entry stays behind as a zombieEXIT <ns> <pid> — flip the row's state to exited; never delete the row
ps reads /proc, which shows only the caller's own namespacePS <ns> — dump that one table, sorted by pid

The behaviour that separates right from almost-right is the counter. Namespace 2's first process is PID 1, not PID 3 — if you keep one global counter you have written a process table, not a namespace. And EXIT must leave the row in place: deleting it would renumber nothing but would erase the zombie, which is exactly the state the section above says PID 1 exists to clean up.

Up nextMount NamespaceLinux Namespaces

Discussion

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

Sign in to post a comment or reply.

Loading…