PID Namespaces
A PID namespace (created with the
CLONE_NEWPIDflag, hex0x20000000in Linux 6.12) isolates the process-ID number space: it gives a set of processes their own private, independently numbered tree of PIDs, starting over at 1. The first process placed in a new PID namespace becomes PID 1 — the namespace’s “init” — and inherits init-like responsibilities: it reaps orphaned descendants, signals sent to it from outside are filtered for its protection, and its death tears the entire namespace down. A single process simultaneously has a different PID in every PID namespace from which it is visible, walking outward to the root. Crucially,CLONE_NEWPIDaffects only children created after the call — the caller never changes its own PID namespace — which is whyunshare(CLONE_NEWPID)must be followed by afork()to land a process inside (pid_namespaces(7)). This is one of the three load-bearing namespaces of a container (alongside mount and network); it is what makespsinside a container show a clean tree rooted at PID 1.
This note is pinned to Linux 6.12 LTS (released 2024-11-17), with the in-kernel struct pid_namespace and the PID-1-death code path verified against the v6.12 source. The flag, the /proc/[pid]/ns/pid handle, and the family-wide framing live in Linux Namespaces Overview; the three system calls that create and join namespaces live in clone unshare and setns. This note’s distinct contribution is the special semantics of PID 1 and the per-namespace PID translation that distinguishes the PID namespace from every other type.
Mental Model
A PID namespace is a layer in a tree of namespaces, not an isolated island. When you create a new PID namespace, it is a child of the namespace you created it from; processes in the child are visible to the parent (and grandparent, up to the root), but processes in the parent are invisible to the child. The consequence is that a single task has several PIDs at once — one per layer it lives in. A process that is PID 1 inside its container might be PID 4242 to the host’s root namespace and PID 87 to an intermediate namespace. There is no single “true” PID; the number you get depends on which namespace is asking.
The two defining behaviors both flow from one design choice: the kernel models the new namespace’s first process as an init process, exactly like the system’s real PID 1 (/sbin/init or systemd). Real init has two kernel-conferred jobs — reap orphans and be hard to kill by accident — and the kernel grants those same two properties to every PID-namespace init. That is why a container’s main process behaves like a tiny init: orphaned grandchildren reparent to it, and stray signals from the host bounce off it.
flowchart TB subgraph ROOT["Root PID namespace (host), level 0"] H1["systemd (PID 1)"] H4242["container shell<br/>(PID 4242 here)"] end subgraph CHILD["Child PID namespace (container), level 1"] C1["container shell<br/>(PID 1 here = init)"] C7["app worker (PID 7)"] C8["app worker (PID 8)"] end H1 --> H4242 H4242 -.same task,<br/>two PIDs.-> C1 C1 --> C7 C1 --> C8 ROOT -.can see + signal.-> CHILD CHILD -.CANNOT see.-> ROOT
One task viewed from two PID namespaces. What it shows: the container’s shell is PID 1 inside its own namespace but PID 4242 in the host’s root namespace — the same task_struct, two different numbers. Visibility is one-directional: the host can see and signal into the container, but the container cannot see the host. The insight to take: “PID” is not a global identifier of a process; it is a per-namespace label. System calls that take a PID always interpret it in the caller’s namespace.
The pid_namespace Structure
The v6.12 definition of the kernel object encodes most of the behavior described in this note (pid_namespace.h, v6.12):
struct pid_namespace {
struct idr idr; /* PID -> struct pid map for THIS ns */
struct rcu_head rcu;
unsigned int pid_allocated; /* count of allocated PIDs */
struct task_struct *child_reaper; /* this namespace's PID 1 / init */
struct kmem_cache *pid_cachep;
unsigned int level; /* nesting depth: 0 = root */
struct pid_namespace *parent; /* enclosing namespace */
struct user_namespace *user_ns; /* owning user namespace */
struct ucounts *ucounts;
int reboot; /* exit code if this pidns rebooted */
struct ns_common ns; /* inode number + refcount (the /proc handle) */
int memfd_noexec_scope;
} __randomize_layout;Walking the load-bearing fields: idr is an integer-ID-to-pointer map that allocates and tracks the PIDs belonging to this namespace — each namespace has its own, which is exactly why PID 1 can be reused in every namespace. child_reaper is the pointer to this namespace’s PID 1; the kernel consults it whenever a process is orphaned, to decide who should adopt the orphan. level is the nesting depth — 0 for the root namespace, incremented by 1 for each nested child; it is bounded (see “Nesting” below). parent links to the enclosing namespace, forming the tree the mental-model diagram drew. user_ns records which user namespace owns this PID namespace, which is how capability checks against it are evaluated. And ns (a struct ns_common) carries the inode number that /proc/[pid]/ns/pid reports and the reference count that keeps the namespace alive.
PID 1: The Init Process and Its Special Semantics
The first process created in a new PID namespace — “the process created using clone(2) with the CLONE_NEWPID flag, or the first child created by a process after a call to unshare(2) using the CLONE_NEWPID flag” — “has the PID 1, and is the ‘init’ process for the namespace” (pid_namespaces(7)). Three properties make PID 1 special.
Reaping orphaned descendants
When any process in a namespace dies leaving children whose parent has also exited, those children are orphaned. In a normal Unix system, orphans reparent to the system’s PID 1, whose job is to wait() on them and release their zombie entries. The PID namespace replicates this locally: per the man page, the namespace’s init “becomes the parent of any child processes that are orphaned because a process that resides in this PID namespace terminated.” Mechanically, this is the child_reaper pointer in the struct — when the kernel needs to reparent an orphan, it walks up to find the namespace’s reaper and hands the orphan over.
This is the source of a classic container bug: if a container’s entrypoint is a shell or application that does not wait() on its children, orphaned grandchildren accumulate as zombies under it, because that process — not the host’s systemd — is now the reaper. Container runtimes and tini-style “init shims” exist specifically to be a well-behaved PID 1 that reaps zombies.
Signal filtering — protecting init from accidental death
PID 1 is shielded from signals in a way ordinary processes are not. Two filtering rules apply (pid_namespaces(7)):
For signals from within the same namespace: “Only signals for which the ‘init’ process has established a signal handler can be sent to the ‘init’ process by other members of the PID namespace.” A kill -TERM 1 from inside the container is silently dropped unless the container’s PID 1 has explicitly installed a SIGTERM handler. This protects the namespace from a child accidentally killing its own init and collapsing everything.
For signals from an ancestor namespace (e.g. the host killing a container’s init): the same handler rule applies — “a process in an ancestor namespace can … send signals to the ‘init’ process of a child PID namespace only if the ‘init’ process has established a handler for that signal.” But two signals bypass all filtering: “SIGKILL or SIGSTOP are treated exceptionally: these signals are forcibly delivered when sent from an ancestor PID namespace. Neither of these signals can be caught by the ‘init’ process.” This is the kernel’s escape hatch — the host can always force-kill (SIGKILL) or force-stop (SIGSTOP) a container’s init regardless of what handlers it installed, which is how docker kill and the OOM killer can reliably terminate a container even if its PID 1 ignores SIGTERM. One further detail: in a handler invoked for a signal from an ancestor namespace, “the siginfo_t si_pid field … will be zero,” because the ancestor’s PID has no meaning inside the child.
Death of init destroys the namespace
This is the most consequential property. Per the man page: “If the ‘init’ process of a PID namespace terminates, the kernel terminates all of the processes in the namespace via a SIGKILL signal.” After that, “a subsequent fork(2) into this PID namespace fail[s] with the error ENOMEM; it is not possible to create a new process in a PID namespace whose ‘init’ process has terminated.”
The v6.12 source shows exactly how this happens, in zap_pid_ns_processes() (pid_namespace.c, v6.12). When init exits, the kernel:
- Calls
disable_pid_allocation(pid_ns)— “Don’t allow any more processes into the pid namespace.” This is precisely why laterfork()s returnENOMEM: the namespace’s PID allocator is switched off, so no newstruct pidcan be created in it. - Walks the namespace’s
idr(every PID belonging to this namespace) and, for each remaining task, callsgroup_send_sig_info(SIGKILL, SEND_SIG_PRIV, task, PIDTYPE_MAX)— delivering an unblockableSIGKILLto every process in the namespace. kernel_wait4()s in a loop until every child has been reaped (-ECHILD), so the namespace tears down cleanly.
This is why a container is exactly as alive as its PID 1: the moment the entrypoint exits, the kernel guarantees every other process in the container dies too. There is no “container with a dead init but live workers” state — the kernel forbids it.
PID Translation: A Different PID in Each Namespace
A process “has one process ID in each of the layers of the PID namespace hierarchy in which it is visible, … walking back through each direct ancestor namespace through to the root PID namespace” (pid_namespaces(7)). This is unique to the PID namespace — no other namespace type gives a process multiple simultaneous identities.
The visibility rule is directional: “A process is visible to other processes in its PID namespace, and to the processes in each direct ancestor PID namespace going back to the root PID namespace,” while “the processes in a child PID namespace can’t see processes in the parent and further removed ancestor namespaces.” The host can inspect and signal a container’s processes; the container cannot even see the host’s.
The translation rule governs every PID-taking system call: “System calls that operate on process IDs always operate using the process ID that is visible in the PID namespace of the caller.” So getpid() returns 1 to the container’s init but 4242 to a host process examining the same task. A subtle consequence the man page spells out: getpid() “always returns the PID associated with the namespace in which the process was created” — a process never sees a PID from a namespace deeper than its own. This is also why /proc/self is namespace-relative: reading the /proc/self symlink “yields the process ID of the caller in the PID namespace of the procfs mount.”
The Unshare-Then-Fork Requirement
The single most surprising property of CLONE_NEWPID is that it does not move the calling process. Per pid_namespaces(7): “Calls to setns(2) that specify a PID namespace file descriptor and calls to unshare(2) with the CLONE_NEWPID flag cause children subsequently created by the caller to be placed in a different PID namespace from the caller. These calls do not, however, change the PID namespace of the calling process, because doing so would change the caller’s idea of its own PID (as reported by getpid()), which would break many applications and libraries.”
This is not a quirk; it is structural, and the v6.12 nsproxy struct proves it (nsproxy.h, v6.12). The field is named pid_ns_for_children, and the comment is explicit: “The pid namespace is an exception — it’s accessed using task_active_pid_ns. The pid namespace here is the namespace that children will use.” unshare(CLONE_NEWPID) sets pid_ns_for_children to the new namespace but leaves the caller’s active PID namespace (the one getpid() reads, via task_active_pid_ns) untouched. The fundamental invariant is that “a process’s PID namespace membership is determined when the process is created and cannot be changed thereafter.”
The practical recipe therefore differs between the two creation paths:
clone(CLONE_NEWPID, …)forks a child directly into a new PID namespace; that child is PID 1 immediately. One call, done.unshare(CLONE_NEWPID)does not make the caller PID 1. It armspid_ns_for_children; the caller must thenfork(), and that child becomes PID 1 of the new namespace. This is the “unshare-then-fork” pattern. The command-lineunshare --pid --forkexists precisely becauseunshare --pidalone leaves you in a broken half-state where you are still in the old namespace but your children land in a new empty one.
One more restriction enforces the “membership is fixed at creation” rule: “A process may call unshare(2) with the CLONE_NEWPID flag only once” — you cannot keep peeling off new PID namespaces for your future children with repeated unshare calls.
Nesting Depth
PID namespaces nest, and the depth is bounded. Per pid_namespaces(7): “Since Linux 3.7, the kernel limits the maximum nesting depth for PID namespaces to 32.” The limit is the MAX_PID_NS_LEVEL constant in v6.12 include/linux/pid_namespace.h:
#define MAX_PID_NS_LEVEL 32create_pid_namespace() in v6.12 kernel/pid_namespace.c enforces it directly: the new namespace’s level is computed as parent_pid_ns->level + 1, and if (level > MAX_PID_NS_LEVEL) the call fails. The bound exists because each struct pid carries an array of one upid entry per level it belongs to — deeper nesting makes every struct pid larger, so the kernel caps it. In practice 32 levels is far beyond any real workload; even deeply nested container-in-container setups rarely exceed two or three.
/proc Must Be Remounted
A new PID namespace does not automatically get a matching /proc. The /proc filesystem is namespace-aware — it “shows (in the /proc/[pid] directories) only processes visible in the PID namespace of the process that performed the mount” — but a freshly created namespace inherits the parent’s /proc mount, which still reflects the parent’s PID view. Per the man page: “After creating a new PID namespace, it is useful for the child to change its root directory and mount a new procfs instance at /proc so that tools such as ps(1) work correctly.” Until you mount -t proc proc /proc inside the new namespace (which a mount namespace makes safe to do without disturbing the host), ps and top will report the host’s processes, not the container’s. This is why every container runtime pairs a PID namespace with a mount namespace and a fresh procfs.
Configuration and Demonstration
The behaviors above are exercised with unshare(1). The session below is illustrative — creating a PID namespace requires CAP_SYS_ADMIN (typically sudo or a user namespace), so absolute PIDs will differ on any given run and are shown only to make the relationships concrete, not as captured output:
# WRONG: unshare --pid WITHOUT --fork. The exec'd shell stays in the OLD
# PID namespace; only its CHILDREN are born into the new one. The first
# child runs fine, but once a child exits there is no init, the namespace
# is marked dying (disable_pid_allocation), and a LATER fork fails:
$ sudo unshare --pid --mount-proc bash
# ls # first child: works
...
# ls # a subsequent fork once the ns is dying:
bash: fork: Cannot allocate memory # ENOMEM — the unshare-then-fork trap
# RIGHT: --fork makes unshare fork after creating the namespace, so the
# new shell IS PID 1. --mount-proc remounts /proc so ps sees the new view.
$ sudo unshare --pid --fork --mount-proc bash
# ps -e
PID TTY TIME CMD
1 pts/0 00:00:00 bash # we are PID 1 inside; clean isolated tree
N pts/0 00:00:00 ps # ps gets the next free PID in the new ns
# From the host, the SAME shell has an ordinary large host PID — PID
# translation: PID 1 inside maps to some host PID <H> in the root ns:
$ ps -ef | grep '[b]ash'
root <H> <ppid> ... bash # PID 1 inside == <H> outsideLine by line: --pid requests CLONE_NEWPID; without --fork the unshare process exec’s the shell while still in the old namespace, so its children — not it — are born into the new namespace with no PID 1. The new namespace survives only while a child is running; once a child exits, disable_pid_allocation (the zap_pid_ns_processes path from the source above) marks the namespace dying, and a subsequent fork() returns ENOMEM — the “fork: Cannot allocate memory” trap. Note the error appears on a later fork, not on entry. Adding --fork makes unshare itself fork after the unshare(2) call, so the resulting bash is genuinely PID 1. --mount-proc (which implies a mount namespace) remounts a fresh procfs so ps -e reports the namespace’s own PIDs — PID 1 is the shell, and ps gets the next free PID N. The host’s ps -ef then shows the same task under an ordinary host PID <H> — concrete PID translation: one task, PID 1 inside and <H> outside.
Failure Modes and Common Misunderstandings
“unshare --pid will put my shell in a new PID namespace.” No — it puts your children there and leaves you in the old one. Without --fork you get a half-state that breaks on the first child exit (ENOMEM). Always pair --pid with --fork.
“kill -9 1 inside a container will be ignored like other signals to PID 1.” From inside the namespace, yes, SIGKILL to PID 1 is special-cased and ignored (a container can’t kill 1 itself away with SIGKILL from within). But from an ancestor namespace, SIGKILL and SIGSTOP are forcibly delivered and uncatchable — the host can always kill the container.
“My container leaks zombie processes.” Almost always because the entrypoint is not a real init and doesn’t reap. The container’s PID 1 is the reaper for the namespace; if it ignores SIGCHLD and never wait()s, orphaned grandchildren pile up as zombies. Use an init shim (tini, the runtime’s --init) as PID 1.
“PID 1 is just a number.” Inside a PID namespace it carries init semantics: it reaps, it is signal-shielded, and its death SIGKILLs the whole namespace. Treating the container entrypoint as an ordinary process is the root of most PID-namespace surprises.
“The host sees the container’s PIDs and vice versa.” Visibility is one-directional. The host (ancestor) sees and can signal the container’s processes; the container (child) cannot see the host’s at all.
See Also
- Linux Namespaces Overview — the parent: all eight namespace types, the
CLONE_NEW*flags, and the/proc/[pid]/ns/handle - clone unshare and setns — the three system calls; the full
unshare/setnsmechanics and thepid_for_childrenhandle - Mount Namespaces — the load-bearing sibling that makes remounting
/procsafe per container - Network Namespaces — the third load-bearing namespace; private network stack
- User Namespaces — owns the
user_nsthat a PID namespace’s capability checks are evaluated against; enables rootless PID namespaces - fork clone and exec System Calls — the process-creation primitives
CLONE_NEWPIDrides on - Pause Container — in Kubernetes, the process that holds a pod’s PID/network namespaces open
- Linux Containers and Isolation MOC — the parent map (section B owns this note)