Containers — [Part1]

Explore Linux PID and UTS namespaces with unshare, compare process IDs inside and outside, and inspect process trees with practical commands.
Linux
Containers
Author

spriyansh29

Published

August 30, 2026

Containers are often described as lightweight virtual machines. That gives a rough idea of how they feel to use, but it leaves out what is actually happening underneath. This is something I wanted to understand better. My background in academia has shaped how I approach things, so I tend to start from the bottom and work my way up. In this first part, I’ll start with a process and look at how namespaces change what it can see. Let’s go one by one.

Before starting

These commands are for a Debian or Ubuntu Linux machine with access to sudo. I’m using Betelgeuse for the examples. If you are on macOS or Windows, use a Linux VM for this. Open two terminals connected to the same machine. I’ll call them Terminal A and Terminal B.

Let’s first install a few tools to see what we are doing. Some may already be installed.

  • procps provides ps, pgrep and watch.
  • util-linux provides unshare.
  • psmisc provides pstree.
sudo apt-get update
sudo apt-get install procps util-linux psmisc

A process

A process is a running program. When an application starts, the kernel gives its process an ID, called a PID. Processes also have parents, and a program can create child processes. This means one application may involve several PIDs.

On a typical Debian or Ubuntu system, systemd runs as PID 1 and manages system startup. Most processes can be traced back to it through their parents. Let’s check what is running as PID 1 on this machine.

ps -p 1 -o pid=,comm=

This selects PID 1 and prints its ID and command name. The equals signs remove the column headings. On this system, the result is systemd. Inside a container or on a system with a different init program, it can be something else.

      1 systemd

A process exists in namespaces

A Linux namespace gives a group of processes its own view of part of the system. Different namespace types cover different things, such as process IDs, hostnames and networking. A child process normally shares its parent’s namespaces, but it can also be started in new ones.

To see this, we’ll start a Bash shell in new PID and UTS namespaces. The UTS namespace controls the hostname and domain name. The PID namespace gives processes their own process IDs. Its first process has PID 1. Processes inside can see others in that namespace and any nested PID namespaces, but they cannot see processes in the parent namespace. The host can still see them. The PID namespace documentation explains this relationship.

In Terminal A, let’s check the current hostname and the namespaces our shell belongs to. $$ is the current shell’s PID.

hostname
ls -l /proc/$$/ns

The hostname on my machine is betelgeuse. The namespace entries look like pid:[4026531836]. Your numbers may differ. Matching identifiers for the same namespace type mean two processes share that namespace.

shuttle@betelgeuse:~ $ hostname
betelgeuse
shuttle@betelgeuse:~ $ ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 shuttle shuttle 0 Oct  3 15:24 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 shuttle shuttle 0 Oct  3 15:24 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 shuttle shuttle 0 Oct  3 15:24 mnt -> 'mnt:[4026531832]'
lrwxrwxrwx 1 shuttle shuttle 0 Oct  3 15:24 net -> 'net:[4026531833]'
lrwxrwxrwx 1 shuttle shuttle 0 Oct  3 15:24 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 shuttle shuttle 0 Oct  3 15:24 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 shuttle shuttle 0 Oct  3 15:24 time -> 'time:[4026531834]'
lrwxrwxrwx 1 shuttle shuttle 0 Oct  3 15:24 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 shuttle shuttle 0 Oct  3 15:24 user -> 'user:[4026531837]'
lrwxrwxrwx 1 shuttle shuttle 0 Oct  3 15:24 uts -> 'uts:[4026531838]'
shuttle@betelgeuse:~ $

Start a shell in new namespaces

Let’s use unshare to create the new namespaces and start a shell. The name comes from the fact that the current shell already shares namespaces with other processes. Here we are asking for separate ones.

sudo unshare --uts --pid --fork --mount-proc bash --noprofile --norc

If sudo asks for a password, it normally expects the password for your current user. After that, a new shell prompt should appear in Terminal A.

The --uts and --pid options request new UTS and PID namespaces. --fork starts the shell as a child in the new PID namespace. --mount-proc also creates a mount namespace and mounts a new /proc, so commands such as ps show the processes visible inside. The Bash options --noprofile and --norc skip the usual startup files. These options are described in the unshare manual.

flowchart TB
    original["Original shell<br/>Host PID, UTS and mount namespaces"]
    child["New Bash shell<br/>New PID, UTS and mount namespaces"]
    shared["Network and user namespaces<br/>Still shared by both shells"]

    original -->|unshare| child
    original -.- shared
    child -.- shared

    classDef host fill:#DBEAFE,stroke:#2563EB,color:#1E3A8A
    classDef isolated fill:#CCFBF1,stroke:#0D9488,color:#134E4A
    classDef common fill:#F1F5F9,stroke:#64748B,color:#0F172A
    class original host
    class child isolated
    class shared common

Let’s check that split. Inside the new shell in Terminal A, run the same checks again and look at PID 1.

hostname
ls -l /proc/$$/ns
ps -p 1 -o pid=,comm=
bash-5.2# hostname
betelgeuse
bash-5.2# ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 root root 0 Oct  3 15:30 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Oct  3 15:30 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Oct  3 15:30 mnt -> 'mnt:[4026533175]'
lrwxrwxrwx 1 root root 0 Oct  3 15:30 net -> 'net:[4026531833]'
lrwxrwxrwx 1 root root 0 Oct  3 15:30 pid -> 'pid:[4026533177]'
lrwxrwxrwx 1 root root 0 Oct  3 15:30 pid_for_children -> 'pid:[4026533177]'
lrwxrwxrwx 1 root root 0 Oct  3 15:30 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Oct  3 15:30 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Oct  3 15:30 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Oct  3 15:30 uts -> 'uts:[4026533176]'
bash-5.2# ps -p 1 -o pid=,comm=
      1 bash
bash-5.2#

The hostname is still betelgeuse, but the UTS namespace identifier has changed. A new UTS namespace starts with a copy of the existing hostname. The PID and mount namespace identifiers have changed too. This time, PID 1 is Bash. Other tools launched from this shell will also appear in its process list.

Change the hostname inside

Let’s change the hostname in Terminal A’s new shell.

hostname container-lab
hostname
container-lab

Now run hostname in Terminal B, which is still outside the new namespaces.

hostname
betelgeuse

The host’s name has stayed the same. The two shells now see different hostnames because they belong to different UTS namespaces.

Watch the processes from inside and outside

In Terminal A, start watch inside the namespace shell so we can see its process tree update.

watch -n 1 "pstree -p 1"
bash(1)---watch(226)---watch(281)---sh(282)---pstree(283)

This is an example of the tree below PID 1. The exact PIDs and number of helper processes can vary. We can see Bash, watch and the commands it starts. The short-lived processes change as watch refreshes. pstree shows the parent-child relationships, and -p includes each PID.

Leave that running. In Terminal B, find the unshare process and its child Bash shell. Run only one copy of this experiment at a time, because this command selects the most recently started unshare process.

unshare_pid=$(pgrep -n -x unshare)
namespace_shell_pid=$(pgrep -P "$unshare_pid" -x bash)
echo "Shell PID on the host: $namespace_shell_pid"
sudo pstree -p "$namespace_shell_pid"
Shell PID on the host: 2325617
bash(2325617)---watch(2337500)---watch(2337520)---sh(2337521)---pstree(2337522)

These are illustrative host PIDs. The tree is the same group of processes we saw inside, now shown with their host PIDs. The host can see the shell and all of its children, including watch and pstree. Since the last few commands are short-lived, they may finish before the host captures them.

To keep watching from Terminal B, run the following command. Press Ctrl+C when finished.

sudo watch -n 1 "pstree -p $namespace_shell_pid"

Compare the shell’s two PIDs

The trees show different PIDs. To check that they refer to the same shell, inspect its namespace PID information in Terminal B.

sudo awk '/^NSpid:/' "/proc/$namespace_shell_pid/status"
NSpid:  2325617  1

On this setup, the first number is the shell’s PID on the host and the second is its PID inside the new namespace. It is the same process with two IDs. If this experiment is run inside another PID namespace, there may be more numbers.

flowchart TB
    host["Host view<br/>Terminal B"]
    inside["Namespace view<br/>Terminal A"]
    shell["The same Bash process"]

    host -->|"PID 2325617"| shell
    inside -->|"PID 1"| shell

    classDef outside fill:#DBEAFE,stroke:#2563EB,color:#1E3A8A
    classDef isolated fill:#CCFBF1,stroke:#0D9488,color:#134E4A
    classDef process fill:#EDE9FE,stroke:#7C3AED,color:#4C1D95
    class host outside
    class inside isolated
    class shell process

We can also compare the PID namespace identifiers directly from Terminal B.

readlink /proc/$$/ns/pid
sudo readlink "/proc/$namespace_shell_pid/ns/pid"
pid:[4026531836]
pid:[4026533177]

The identifiers differ because Terminal B’s shell and the new Bash shell belong to different PID namespaces.

Leave the namespace shell

In Terminal A, press Ctrl+C to stop watch, then leave the shell.

exit

This exits the process that was PID 1 in the new namespace. Any remaining processes in that PID namespace are terminated. Terminal A is now back in its original shell. Check the hostname and PID 1 again.

hostname
ps -p 1 -o pid=,comm=
betelgeuse
      1 systemd

We have given a process a separate view of process IDs, the hostname and mounts. We have not created a separate network or replaced the host’s files, and the shell launched with sudo still has root privileges. This is one part of how a container works.

Container runtimes use namespaces to give processes these separate views. Resource limits are another part of the setup, which I’ll cover with cgroups in Part 2.


Have thoughts, questions, or suggestions? I’d love to hear them.