Linux Namespaces Overview
A namespace is a kernel facility that “wraps a global system resource in an abstraction that makes it appear to the processes within the namespace that they have their own isolated instance of the global resource” (namespaces(7)). Where the unmodified kernel exposes one of each global resource to every process — one process-ID space, one mount table, one network stack, one hostname — a namespace lets a subset of processes be given their own private copy of that resource, while the rest of the system carries on with the original. As of the Linux 6.12 / 6.18 LTS era there are eight namespace types, each virtualizing exactly one global resource. A process belongs to exactly one namespace of each type, and a new namespace is created by passing the corresponding
CLONE_NEW*flag to one of the three lifecycle system calls (clone(2),unshare(2),setns(2)). Namespaces are half of what makes a Linux container: they partition visibility (what a process can see), while control groups partition consumption (what it can use).
This note is the parent enumerator for the namespace family. It pins every claim to Linux 6.12 LTS (released 2024-11-17) or 6.18 LTS (released 2025-11-30), and verifies flag values and the per-process namespace structure against the v6.12 source tree. The deep mechanics of individual namespaces live in their own notes — PID Namespaces, Mount Namespaces, Network Namespaces, User Namespaces, UTS Namespaces, IPC Namespaces, Cgroup Namespaces, Time Namespaces — and the three system calls that manage them all live in clone unshare and setns. This note’s job is the shape of the whole family: how many there are, what each isolates, how a process relates to them, and how /proc exposes them.
Mental Model
The cleanest way to think about namespaces is to ask, for each kernel resource that processes share, “what if every group of processes could have its own copy?” The hostname is global: change it and uname changes for everyone. A UTS namespace breaks that — processes inside see a private hostname. The set of mounts is global: mount affects what every process sees at /mnt. A mount namespace breaks that. The list of process IDs is global: PID 4242 means the same task to everyone. A PID namespace breaks that. There is no single “namespace” object that bundles these together; there are eight independent virtualizers, each addressing one resource, and you opt into any subset.
A process’s relationship to its namespaces is held in a small kernel structure called the nsproxy. The v6.12 definition makes the “one of each type” rule mechanically obvious — it is simply a struct with one pointer per namespace type (nsproxy.h, v6.12):
struct nsproxy {
refcount_t count;
struct uts_namespace *uts_ns;
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns;
struct pid_namespace *pid_ns_for_children;
struct net *net_ns;
struct time_namespace *time_ns;
struct time_namespace *time_ns_for_children;
struct cgroup_namespace *cgroup_ns;
};A process cannot belong to “two mount namespaces” because there is exactly one mnt_ns pointer to fill in. The nsproxy is shared by tasks that share all their namespaces — the comment in the source notes that “the nsproxy is shared by tasks which share all namespaces; as soon as a single namespace is cloned or unshared, the nsproxy is copied.” That copy-on-write of the proxy is why creating one new namespace is cheap: most pointers are simply carried over to the new proxy unchanged.
Two subtleties in that struct deserve attention. First, the user namespace is absent from nsproxy — it is reached through the process’s credentials (task->cred->user_ns), not the proxy, because user-namespace membership governs the privilege with which every other namespace operation is checked. Second, the PID namespace appears as pid_ns_for_children, not pid_ns — the kernel 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.” This single design decision is the root cause of the “unshare-then-fork” behavior covered in PID Namespaces: changing a process’s PID namespace would change the value getpid() returns to it, which would break the process, so the change only affects future children.
flowchart TB subgraph TASK["task_struct (one process)"] CRED["cred -> user_ns<br/>(privilege context)"] NSP["nsproxy<br/>(one pointer per type)"] end NSP --> UTS["uts_ns"] NSP --> IPC["ipc_ns"] NSP --> MNT["mnt_ns"] NSP --> NET["net_ns"] NSP --> PIDC["pid_ns_for_children<br/>(active PID ns via task_active_pid_ns)"] NSP --> TIME["time_ns + time_ns_for_children"] NSP --> CG["cgroup_ns"] PROC["/proc/[pid]/ns/<type><br/>(one symlink per type, the handle)"] -.exposes inode of.-> NSP
The per-process namespace structure in Linux 6.12. What it shows: a process holds exactly one pointer per namespace type in its nsproxy (plus the user namespace via credentials), and /proc/[pid]/ns/ exposes each as a symlink. The insight to take: “a process belongs to one namespace of each type” is not a rule the kernel enforces with checks — it is structural, because there is literally one slot per type. PID and user are the two architectural exceptions: PID via task_active_pid_ns / pid_ns_for_children, user via the credential.
The Eight Namespace Types
The table below is the canonical enumeration. Two version columns are given deliberately, because they answer two different questions and the two are routinely conflated. The “feature since” column is the kernel release in which the namespace mechanism first existed (verified against clone(2)); the “/proc/ns symlink since” column is when the /proc/[pid]/ns/<type> handle for that namespace first appeared (per the version table in namespaces(7)). The two differ because the unified /proc/[pid]/ns/ interface was retrofitted in Linux 3.0–3.8 onto namespace types that already existed; for example mount namespaces shipped in 2.4.19 (2002) but their ns/mnt symlink only arrived in 3.8.
| Namespace | CLONE_NEW* flag | Hex value (v6.12) | Isolates | Feature since (clone.2) | /proc/ns symlink since (namespaces.7) |
|---|---|---|---|---|---|
| Mount | CLONE_NEWNS | 0x00020000 | Mount points (the mount table) | Linux 2.4.19 | Linux 3.8 |
| UTS | CLONE_NEWUTS | 0x04000000 | Hostname and NIS domain name | Linux 2.6.19 | Linux 3.0 |
| IPC | CLONE_NEWIPC | 0x08000000 | System V IPC, POSIX message queues | Linux 2.6.19 | Linux 3.0 |
| Network | CLONE_NEWNET | 0x40000000 | Network devices, stacks, ports, routing | Linux 2.6.24 | Linux 3.0 |
| PID | CLONE_NEWPID | 0x20000000 | Process IDs | Linux 2.6.24 | Linux 3.8 |
| User | CLONE_NEWUSER | 0x10000000 | User and group IDs, capabilities, keyrings | Linux 2.6.23 → 3.8 | Linux 3.8 |
| Cgroup | CLONE_NEWCGROUP | 0x02000000 | Cgroup root directory | Linux 4.6 | Linux 4.6 |
| Time | CLONE_NEWTIME | 0x00000080 | Boot and monotonic clocks | Linux 5.6 | Linux 5.6 |
One attribution caveat on the table: the “feature since” column is sourced from clone(2) for all rows except Time — clone(2) does not document CLONE_NEWTIME (it is the unshare-only namespace), so the time namespace’s 5.6 introduction is taken from the namespaces(7) version table, where for Time the feature and the /proc symlink landed together in Linux 5.6.
The flag values are quoted from the v6.12 include/uapi/linux/sched.h (sched.h, v6.12), where they are defined as plain bit constants:
#define CLONE_NEWTIME 0x00000080 /* New time namespace */
#define CLONE_NEWNS 0x00020000 /* New mount namespace group */
#define CLONE_NEWCGROUP 0x02000000 /* New cgroup namespace */
#define CLONE_NEWUTS 0x04000000 /* New utsname namespace */
#define CLONE_NEWIPC 0x08000000 /* New ipc namespace */
#define CLONE_NEWUSER 0x10000000 /* New user namespace */
#define CLONE_NEWPID 0x20000000 /* New pid namespace */
#define CLONE_NEWNET 0x40000000 /* New network namespace */A historical curiosity worth understanding: CLONE_NEWNS (“NS” = namespace, generically) is the oldest flag and the most cramped name — when mount namespaces were the only namespace type, a flag literally named “new namespace” sufficed. Later types got descriptive suffixes (NEWUTS, NEWNET, …). And CLONE_NEWTIME, the newest (Linux 5.6, 2020), has the lowest hex value (0x80) of any namespace flag. That low placement is because the high 32-bit clone flag space was already exhausted by 2020: CLONE_NEWNET occupies 0x40000000 and CLONE_IO the top bit 0x80000000, so a free bit had to be found in the low byte that historically held the child’s exit signal.
Uncertain
Verify: that the low-byte placement of
CLONE_NEWTIME(0x80) is why the time namespace can only be created viaunshare(2)and not via the legacyclone(2)(as opposed toclone3(2)). Reason: the placement (exhausted high flag space) and the unshare-only restriction (stated by time_namespaces(7): “the only way to create a time namespace is by calling unshare(2) with the CLONE_NEWTIME flag”) are both verified facts, but the causal link between them is an inference, not a primary-source statement. In v6.12kernel/nsproxy.c,copy_namespaces()(the clone path) does includeCLONE_NEWTIMEin its mask, so the restriction is more nuanced than “clone rejects the bit.” The full mechanism belongs in Time Namespaces. To resolve: read the time-namespace patch series cover letter /kernel/time/namespace.csemantics. uncertain
What each one virtualizes, in one paragraph
The mount namespace (CLONE_NEWNS) gives a process its own mount table — its own answer to “what filesystem is mounted at this path.” It is the basis of per-container root filesystems; see Mount Namespaces. The UTS namespace (CLONE_NEWUTS, where UTS abbreviates the historical “UNIX Time-sharing System” utsname struct) isolates the hostname and NIS (Network Information Service) domain name, so a container can have its own hostname. The IPC namespace (CLONE_NEWIPC) isolates System V inter-process-communication objects (shared-memory segments, semaphores, message queues) and POSIX message queues, so a container’s ipcs listing is private. The network namespace (CLONE_NEWNET) is the heaviest: a complete private network stack — its own loopback, its own network interfaces, routing table, firewall rules, and port number space; see Network Namespaces. The PID namespace (CLONE_NEWPID) gives a private process-ID space whose first process becomes PID 1 with init-like semantics; see PID Namespaces. The user namespace (CLONE_NEWUSER) maps user and group IDs and isolates capabilities and keyrings, so a process can be “root inside, unprivileged outside” — per user_namespaces(7), “a process can have a normal unprivileged user ID outside a user namespace while at the same time having a user ID of 0 inside the namespace”; see User Namespaces. The cgroup namespace (CLONE_NEWCGROUP) virtualizes the view of the cgroup hierarchy root, so a container sees its cgroup as / rather than its true host path; see Cgroup Namespaces. The time namespace (CLONE_NEWTIME) offsets CLOCK_MONOTONIC and CLOCK_BOOTTIME — but deliberately not CLOCK_REALTIME (time_namespaces(7)) — so a checkpoint-restored process sees an uptime consistent with its original host; see Time Namespaces.
The Three Load-Bearing Namespaces
Although there are eight types, a container is overwhelmingly defined by three: PID, mount, and network. These are “load-bearing” in the sense that without them you do not have a recognizable container.
The mount namespace is load-bearing because it is what gives a container its own filesystem — without it, a container would see the host’s /, and “container image” would be meaningless. It is the namespace the runtime uses to pivot_root into the image’s root filesystem (see pivot_root and the Container Rootfs). The PID namespace is load-bearing because it is what makes the container’s process tree start from PID 1 — it is why ps inside a container shows a clean, isolated set of processes beginning with the entrypoint, and why killing PID 1 tears the container down. The network namespace is load-bearing because it is what gives a container its own IP address and port space — it is why two containers can both bind port 80 without conflict, and it is the endpoint a veth pair plugs into.
The other five — UTS, IPC, user, cgroup, time — virtualize narrower resources. They matter (user namespaces in particular are the security pivot for rootless containers, covered in User Namespaces), but a process with only a UTS namespace is just a process with a private hostname, not a container. This is exactly why isolation is a spectrum: you can unshare -u for a hostname alone, or unshare -mnpuU for nearly the full set, and every combination in between is a valid kernel configuration.
The /proc/[pid]/ns/ Handle
Each namespace a process belongs to is exposed as a magic symlink under /proc/[pid]/ns/ — ns/mnt, ns/net, ns/pid, ns/user, ns/uts, ns/ipc, ns/cgroup, ns/time, plus two special “for_children” handles described below (namespaces(7)). These symlinks are the handle by which userspace identifies and manipulates namespaces, and they serve three distinct purposes.
First, identity comparison. Each symlink’s target is a magic string like mnt:[4026531840], where the number is the namespace’s inode number — the inum field of the kernel’s struct ns_common, which every namespace embeds (ns_common.h, v6.12):
struct ns_common {
struct dentry *stashed;
const struct proc_ns_operations *ops;
unsigned int inum; /* the number in mnt:[NNNN] */
refcount_t count; /* reference count keeping the ns alive */
};Two processes are in the same namespace if and only if the device and inode of their ns/<type> symlinks match — per the man page, “if two processes are in the same namespace, then the device IDs and inode numbers of their /proc/[pid]/ns/xxx symbolic links will be the same.” This is how tools like lsns group processes by namespace.
Second, joining via setns. Opening one of these symlinks yields a file descriptor that can be passed to setns(2) to enter that namespace — the mechanism behind nsenter and behind how kubectl exec or a sidecar joins a pod’s existing network namespace. The full lifecycle of opening, holding, and joining is covered in clone unshare and setns.
Third, keeping a namespace alive. A namespace exists as long as its ns_common.count reference count is non-zero. Normally that count tracks member processes, so when the last process in a namespace exits, the namespace is destroyed. But bind-mounting a ns/<type> file elsewhere in the filesystem takes a reference — per the man page, this “keeps the corresponding namespace … alive even if all processes currently in the namespace terminate.” This is the trick ip netns add uses to create a persistent, empty network namespace with no process in it (see Network Namespaces).
Two of the symlinks are not the namespace the process is in, but the namespace its future children will be in: ns/pid_for_children and ns/time_for_children. Per namespaces(7), each “is a handle for the [PID/time] namespace of child processes created by this process [and] can change as a consequence of calls to unshare(2) and setns(2).” These exist precisely because, as the nsproxy struct showed, a process cannot move its own PID or time namespace — only stage one for its descendants. After unshare(CLONE_NEWPID), ns/pid still points at the caller’s original namespace while ns/pid_for_children points at the new one; the divergence is the visible signature of the “unshare-then-fork” pattern.
How a Namespace Comes Into Being
The eight flags are consumed by exactly three system calls, the subject of clone unshare and setns:
clone(2)creates a new process directly inside the requested new namespaces.clone(CLONE_NEWNET | CLONE_NEWPID | …)forks a child that is born in fresh namespaces. In v6.12,kernel/nsproxy.c:copy_namespaces()handles this path: if anyCLONE_NEW*flag is set it allocates a newnsproxyfor the child, otherwise the child shares the parent’s proxy.unshare(2)moves the calling process into new namespaces (for most types), without forking.unshare -mdetaches the caller’s mount namespace in place. PID and time are the exceptions:unshare(CLONE_NEWPID)does not move the caller (it setspid_ns_for_children), so aforkis required to actually land a process in the new PID namespace — see PID Namespaces.setns(2)moves the calling process into an existing namespace identified by a file descriptor (from/proc/[pid]/ns/). This is how a process joins a namespace someone else created.
Creating any namespace other than a user namespace requires the CAP_SYS_ADMIN capability in the user namespace that will own the new namespace — verified in v6.12 kernel/nsproxy.c, where both copy_namespaces() and unshare_nsproxy_namespaces() gate on ns_capable(user_ns, CAP_SYS_ADMIN). The decisive exception, per namespaces(7), is that “since Linux 3.8, no privilege is required to create a user namespace” — which is what allows an unprivileged user to create a user namespace first and then, as root within it, create all the other namespace types. This is the entire foundation of rootless containers (see User Namespaces and Rootless Containers).
Common Misunderstandings
“A namespace is the container.” No. A namespace virtualizes one resource. A container is the deliberate composition of several namespaces plus a cgroup, a pivoted rootfs, and confinement. Used alone, unshare -n gives you a network namespace and nothing resembling a container.
“A new namespace starts empty.” Only the PID and (mostly) network namespaces start “empty” in a meaningful sense. A new mount namespace starts as a copy of the parent’s mount table; a new UTS namespace starts with a copy of the parent’s hostname. Each type defines its own birth-state in its own note.
“/proc/[pid]/ns/ version in namespaces(7) is when the namespace was added.” This is the trap the two-column table above guards against. Those versions (3.0/3.8) are when the symlink interface appeared, not the namespace feature. Mount namespaces predate their ns/mnt symlink by a decade.
“Each process has one namespace and that’s that.” Each process has one of each type — eight memberships simultaneously. The nsproxy struct makes this concrete: it is eight pointers (plus the user namespace via credentials), not one.
See Also
- PID Namespaces —
CLONE_NEWPID; the init/PID-1 semantics and the unshare-then-fork rule in depth - Mount Namespaces —
CLONE_NEWNS; the private mount table, one of the load-bearing three - Network Namespaces —
CLONE_NEWNET; the private network stack, one of the load-bearing three - User Namespaces —
CLONE_NEWUSER; the privilege pivot and rootless-container foundation - UTS Namespaces · IPC Namespaces · Cgroup Namespaces · Time Namespaces — the narrower four
- clone unshare and setns — the three system calls that create, detach, and join namespaces
- The proc ns Directory and Namespace File Descriptors — the
/proc/[pid]/ns/handle in depth - Control Groups Overview — the other half of containers: resource budgets, not visibility
- Linux Containers and Isolation MOC — the parent map (section B owns this note)