Part 2: Linux Namespaces - PID Namespaces
Process Isolation
Welcome to the next part of our Linux Namespaces series.
In the previous part, we explored what Linux namespaces are, the different types available, and how they’re used by containers.
In this part, we’ll continue where we left off and take a closer look at one of the most fundamental namespace types, the PID namespace.
As you already know, every process running on Linux has a unique Process ID (PID).
If you run ps aux, you’ll see every process on the system, no matter who started it.
This global visibility is convenient, but it also means any process (with enough privileges) can observe or even interact with others by sending signals.
PID namespaces change that behavior.
They give processes their own private view of the system’s process tree.
Inside a PID namespace, process numbering starts from 1 again, and that process becomes the “init” process for that isolated environment.
This also means that two processes in different PID namespaces can share the same PID without conflict, each namespace maintains its own independent process ID space.
This is exactly how containers appear to have their own independent process lists, even though they all share the same kernel.
Docker, Podman, and other container runtimes rely heavily on PID namespaces to make each container behave like a self-contained system.
In this section, we’ll see how PID namespaces work in practice by creating one manually using unshare, and by examining how parent, child, and grandchild namespaces relate to one another.
Creating a New PID Namespace with unshare
In the previous part, we saw how to list all namespaces on a system using the lsns command.
Let’s start from there to remind ourselves what namespaces currently exist on the host.
$ sudo lsnsThis output lists all namespaces on the system, their types, and the processes associated with them.
Let’s focus specifically on the PID namespaces:
$ sudo lsns -t pidHere you can see there’s only one PID namespace, the initial or root namespace.
It contains every process that exists when the system boots.
We can confirm this by displaying the PID namespace each process belongs to:
$ sudo ps -eo pidns,pid,cmdYou’ll notice every process shares the same namespace ID (4026531836 in this case), meaning they all belong to the root PID namespace.
Now that we know what the root namespace looks like, let’s create a new one using the unshare command.
Before we begin, here’s how we’ll organize our setup:
parent-ns (red): the host terminal, representing the root PID namespace.
child-ns-01 (green): the second terminal, we’ll use this one to create a new PID namespace using the
unsharecommand.child-ns-02 (blue): the third terminal, for creating another PID namespace again.
To create our first PID namespace, we’ll use the unshare command.
unshare lets you start a process in one or more new namespaces without using any container tools.
In the second terminal (child-ns-01), run:
$ sudo unshare --pid --fork --mount-proc /bin/bashThis command tells Linux to start a new shell in its own PID namespace. Here’s what each option does:
sudo, required since creating namespaces needs root privileges.unshare, creates new namespaces and runs a program inside them.-pid, creates a new PID namespace where process numbering starts from 1.-fork, forks a child process to enter the new namespace (the parent cannot change its own PID).--mount-proc, mounts a fresh/procfilesystem inside the new namespace so that it only displays the processes that exist within that namespace. Without this option, commands likepsortopwould still read from the host’s/proc, showing all system processes instead of the isolated ones. Which defeats the purpose of PID namespace isolation./bin/bash, launches a new shell inside that namespace.
Once inside, list the processes:
# ps auxYou’ll see just two processes, bash and ps.
That’s because this new namespace has its own isolated process table.
To verify, check your shell’s PID:
# echo $$It prints 1, meaning this shell is now acting as the init process of the new namespace, responsible for reaping child processes and managing lifecycle events inside this isolated world.
From this short test, you’ve essentially recreated one of the key aspects of container behavior: a private process world with its own PID 1.
Inside the new PID namespace (child-ns-01), let’s create a few processes to see how they appear.
For simplicity, we’ll use the sleep command and run each instance in the background so they don’t block our terminal:
# sleep 10000 &
# sleep 9000 &
# sleep 8000 &Now check the process list again:
# ps auxFrom the output, each sleep command has its own unique PID within this namespace, starting from 1 for bash, then 12, 13, and 14 for the sleep processes and finally the 16 for the ps command we just ran.
We can confirm that this shell is running in its own PID namespace by listing PID namespaces again:
$ lsns -t pidHere, the namespace ID 4026532194 is different from the root namespace (4026531836).
This means processes here exist in their own isolated PID namespace, they can only see other processes within this namespace and any namespaces created below it.
Note
Before we move on, there’s one important point to note. If you had run lsns (without the
-t pidflag) in the child-ns-01 terminal, you would still see all the other namespaces listed. You might expect to see only the PID namespace we created, but that’s not the case.When we use the unshare --pid command, it creates a new PID namespace but does not automatically create new namespaces for network, UTS, IPC, user, or cgroup, those remain shared with the parent process. However, in our case, we also used the --mount-proc option. This flag causes the kernel to create a new mount namespace as well, so that a fresh /proc filesystem can be mounted inside the isolated environment without affecting the host’s global mount table.
So, when you run lsns inside the child shell, you’ll notice that the PID and mount namespace IDs differ from the host’s root process (/sbin/init), while the other namespaces (network, UTS, IPC, user, cgroup, etc.) have the same IDs as the parent, since they are still shared.
Now that we’ve confirmed the new namespace exists, let’s go back to the parent-ns terminal and check what it can see.
Run the following command to view all processes and their PID namespaces, filtering for the child namespace ID (4026532194 in this example):
$ sudo ps -eo pid,pidns,cmd | grep 4026532194Notice we can see all processes running in our new child namepaces (4026532194). This confirms that the root (parent) namespace can see the processes running inside the child namespace, because all PID namespaces ultimately descend from the root.
To double-check, you can list all PID namespaces on the system:
$ sudo lsns -t pidAs you can see, we now have to PID namespaces. The root namespace (4026531836) and the new child namespace (4026532194) now coexist, each with its own process table.
Sysxplore is an indie, reader-supported publication.
I break down complex technical concepts in a straightforward way, making them easy to grasp. A lot of research goes into every piece to ensure the information you read is as accurate and practical as possible.
To support my work, consider becoming a free or paid subscriber and join the growing community of tech professionals.
Process Visibility Between Parent and Child Namespaces
One of the most interesting aspects of PID namespaces is that they’re hierarchical.
When you create a new one, it becomes a child of the namespace in which it was created.
As mentioned earlier, PID namespaces allow the same PID number to exist in multiple namespaces. A process has a unique PID within its own namespace, but it’s also assigned additional PIDs in each parent namespace above it, all the way up to the root PID namespace.
Let’s see this in action.
Inside child-ns-01, list the processes we created earlier:
# ps auxNow return to the parent-ns terminal and check how these same processes appear from the host’s perspective:
$ sudo ps -eo pid,pidns,cmd | grep 4026532194Notice how the PIDs differ.
For example, a process that appears as PID 12 inside child-ns-01 appear as PID 2269 in the parent namespace.
This shows how PID namespaces can reuse the same process numbers independently.
To verify the relationship, inspect the namespace mapping of one of the sleep processes. Inside parent-ns run:
$ sudo cat /proc/2169/status | grep NSpidThis confirms that process 2269 in the parent namespace corresponds to PID 9 inside the child namespace, proving the hierarchical mapping between the two.
Creating Another Child Namespace
Now let’s move to the third terminal (child-ns-02) to extend this concept.
We’ll create another PID namespace to see how visibility works across multiple layers.
In child-ns-02, run:
$ sudo unshare --pid --fork --mount-proc /bin/bashVerify the new namespace:
$ sudo lsns -t pidWe now have a new PID namespace with ID 4026532196.
Let’s start a few background processes inside it:
# sleep 7000 &
# sleep 6000 &
# sleep 5000 &Check the process list:
# ps auxEach process has a PID unique to this namespace, starting from 1 for bash, followed by 13, 14, and 15 for the sleep commands.
Now go back to the parent-ns terminal and check what it can see:
$ sudo ps -eo pid,pidns,cmd | grep 4026532196 Again, the parent namespace can see all the processes from this new child namespace.
However, if you switch back to child-ns-01 and run:
$ ps auxYou’ll only see the processes that exist in child-ns-01, not those from the parent or child-ns-02.
This demonstrates a key rule of PID namespaces:
The parent namespace can always see processes inside its children.
The child namespace can see only its own processes and descendants, never those of the parent or siblings.
This hierarchical visibility is the foundation for how container runtimes manage and isolate processes while still maintaining control from the host.
Grandchild Namespaces and Deeper Hierarchies
PID namespaces don’t just stop at one level, they can be nested.
A child namespace can create another namespace inside it, forming a grandchild relationship.
Each layer becomes more isolated than the one above it.
Let’s build a small chain to see how this works in practice.
Inside the first child namespace (child-ns-02), run the unshare command again to create two more PID namespaces:
# unshare --pid --fork --mount-proc sleep 20000 &
# unshare --pid --fork --mount-proc sleep 10000 &Notice the ampersand (&) at the end, this runs the command in the background so we can continue using the current shell without immediately switching into the new namespace.
Run lsns -t pid inside child-ns-02 to verify the new namespaces that now exist:
$ sudo lsns -t pidHere, you can see multiple PID namespaces:
4026532196 → represents child-ns-02 (the first child)
4026532198 and 4026532200 → represent newly created nested (grandchild) namespaces
Each one is its own isolated environment with a separate process list.
Now, from the first child shell (child-ns-02), list the running processes:
# ps auxFrom this view, you can see both the child and grandchilds namespaces processes
However, if you were to switch into one of the (grandchild) shell and run ps aux, you would only see its own processes, not those of its parent or the host.
To demonstrate this, we can use the nsenter command to enter one of the grandchild namespaces by targeting a process running inside it. For example:
$ nsenter --target 13 --pid --mount /bin/bashHere, the --target 13 option tells nsenter to attach to the process with PID 13 (which is running inside the grandchild namespace). The --pid and --mount flags ensure we enter both its PID and mount namespaces, giving us a proper view of that isolated environment.
We’ll discuss how nsenter works in more detail later, but for now, this demonstrates how nested PID namespaces maintain strict process isolation.
To visualize the hierarchy:
Host (PID namespace level 0)
└── Child namespace (level 1)
└── Grandchild namespace (level 2)Each namespace sees only processes at its level and below:
The host can see all processes.
The child can see its own and any descendants.
The grandchild can see only itself.
This layered visibility model ensures isolation while keeping control in the hands of the parent namespace, the same principle container runtimes use when managing nested containers or sandboxes.
The Role of PID 1 Inside Namespaces
In Linux, the very first process started by the kernel during boot is assigned PID 1, traditionally /sbin/init or systemd.
This process is special because it acts as the parent of all other processes on the system and is responsible for reaping orphaned children and handling system shutdown.
The same rule applies inside PID namespaces.
Whenever a new PID namespace is created, the first process started inside it becomes PID 1 within that namespace, its own miniature version of the system’s init process.
This role carries similar responsibilities, behaves differently from ordinary processes, and is responsible for several critical tasks:
Reaping zombie processes: cleaning up processes that have finished executing but haven’t been removed from the process table.
Receiving orphaned processes: when a parent process exits, its orphaned children are adopted by PID 1.
Controlling the namespace lifecycle: when PID 1 exits, the entire namespace is destroyed, and all remaining processes are automatically terminated.
This mechanism keeps each namespace self-contained and prevents orphaned or lingering processes after it ends. One of the core principles that container runtimes rely on for process isolation and cleanup.
In containerized environments, PID 1 is often the main application process itself (like nginx, bash, or python), or a lightweight init system such as tini or dumb-init, which handles process reaping and signal forwarding on behalf of the container.
Using nsenter to Inspect Processes Inside Another Namespace
Let’s now discuss the nsenter command in greater detail.
The nsenter command allows you to enter an existing namespace from another namespace and interact with it directly.
It’s especially handy for troubleshooting, monitoring, or exploring what’s happening inside containers or isolated environments without using Docker or Podman.
Let’s see it in action.
First, go back to child-ns-01, which has the namespace ID 4026532194, and list all processes:
# ps auxNow switch back to the parent-ns terminal and find the corresponding host PIDs for the processes running inside that namespace:
$ sudo ps -eo pid,pidns,cmd | grep 4026532194From this, you can see that process 2256 on the host corresponds to the bash process inside the PID namespace 4026532194.
Now, from the parent-ns, use nsenter to attach to that process’s namespace:
$ sudo nsenter --target 2256 --pid --mount /bin/bashThis opens a new shell inside the same PID and mount namespaces as the process with PID 2256.
You can verify this by listing the processes again:
# ps auxYou’ll now see the same limited process list that exists inside the namespace, confirming that nsenter has successfully joined it.
This confirms that nsenter successfully joined the namespace.
You can use the same approach with any process in that namespace to achieve the same result.
This ability to “attach” to another process’s namespace is what makes nsenter invaluable for debugging containers, inspecting running processes, or performing forensic analysis without needing container-specific tooling.
Seeing It All Together
Now that you’ve seen how PID namespaces work manually, let’s look at how containers use them in practice.
We’ll use Docker for this demonstration, but you can try the same with Podman or any other container runtime.
Before we begin, make sure you’ve exited all previously created namespaces so that you’re back at the parent-ns terminal (the host).
If Docker isn’t installed, install it first, we’ll continue using it throughout the rest of this series.
Run an interactive container with a bash shell:
$ docker run -it --name pidns-demo nginx /bin/bashOnce inside the container, install the procps package (so we can use the ps command):
# apt update && apt install procps -yNow check the processes running inside the container:
# ps auxHere, the container has its own PID namespace, with bash as PID 1 and ps as PID 133.
Even though these PIDs overlap with ones on the host, they’re completely separate within the container.
Let’s create a few background processes:
# sleep 5000 &
# sleep 4000 &
# ps auxNow verify the namespace ID from inside the container:
$ lsns -t pidDocker has automatically created a new PID namespace (4026532196) for this container.
Let’s confirm that from another terminal by running:
$ sudo ps -eo pid,pidns,cmd | grep 4026532196As you can see, the host can view all processes running inside the container, because the root namespace can always see into its children.
To confirm, list all PID namespaces again:
$ sudo lsns -t pidThe host namespace (4026531836) and the container’s namespace (4026532196) coexist, the container simply lives inside its own isolated process world.
Containers are, in the end, just Linux processes running inside their own set of namespaces, PID, mount, network, user, and others.
They’re not special or separate from the host kernel, they simply have their own views of system resources.
And that’s exactly what we’ll explore in the next part of this series.
Looking Ahead
In this part, we explored how PID namespaces isolate process IDs, creating separate “worlds” where each namespace has its own process tree and its own PID 1.
You’ve seen how this mechanism forms the foundation of process isolation in containers, and how tools like unshare and nsenter give you direct control and visibility into that hierarchy.
In the next part of this series, we’ll take a closer look at how containers are actually just Linux processes, and use real examples to see how the host perceives and manages them.
Thanks for reading!
If you enjoyed this content, don’t forget to leave a comment, like ❤️ and subscribe to get more posts like this every week.





































Great article! Thanks!
very powerfull!