1. Overview

The Docker engine starts a container on a new process. In addition, Docker uses the namespaces feature of the Linux kernel to isolate the container’s execution from its parent process. As a result, the container’s process has two process IDs. The first can be found inside the container and the second in the host.

In this tutorial, we’ll first understand the concept of namespaces. Then, we’ll see the mapping between the internal and host process IDs.

2. Namespaces

A namespace is a kernel feature that can isolate some resources for a group of processes. For example, such resources are the process ID number space, users, network interfaces, etc.

There are eight namespace types, each corresponding to a resource type:

  • Cgroup
  • IPC
  • Network
  • Mount
  • PID (Process ID)
  • Time
  • User
  • UTS (Hostnames, domain names, etc)

To explore namespaces, we’ll use the unshare command. The unshare command creates new namespaces and executes a given program in them. For this purpose, we’ll create a child process that executes the ps command on a new PID namespace:

$ sudo unshare --pid --mount-proc --fork ps ax
      1 pts/2    R+     0:00 ps ax

Here, we used three options:

  • –fork: creates a new child process
  • –pid: creates a new PID namespace
  • –mount-proc: mounts the proc filesystem in the /proc folder

Furthermore, the ps ax command prints all processes and background jobs. Notably, it printed only one process, the one we created with the unshare command. Also, this process became the init process in our namespace, with the ID value of 1.

To sum up, let’s revise what happened:

  1. unshare created a new child process to run the ps command that we supplied
  2. A new PID namespace was created and our child process joined it
  3. The child process is the init process of the new namespace
  4. The PID number space restarts
  5. The child process isn’t aware of processes outside its PID namespace

Finally, we can expand our example and add more namespace types, if we use the other options of the unshare command.

3. The lsns Command

The lsns command lists information about accessible namespaces. When invoked with no options, it prints all accessible namespaces.

Moreover, we can reduce the output to a particular namespace type with the -t option:

$ sudo lsns -t pid
4026531836 pid     100     1 root   /sbin/init

Here, the command printed the root PID namespace. The first column in the output is the namespace identifier which is an inode number.

In our case, the root PID namespace identifier is 4026531836. Let’s create a new PID namespace with the unshare command:

$ sudo unshare --pid --mount-proc --fork sleep 1000 &
[1] 1835

As we can see, instead of the ps command, we run the sleep command. The new PID namespace will exist as long as the sleep command is running.

Let’s execute the lsns command again:

$ sudo lsns -t pid
4026531836 pid     102     1 root /sbin/init
4026532149 pid       1  1837 root sleep 1000

This time two PID namespaces are printed. The first is the root namespace and the second is the new namespace that we created with the unshare command. The identifier of the new namespace is 4026532149.

Furthermore, the new namespace we created is a child namespace of the root. This makes the root PID namespace parent of the child namespace.

4. Parent and Child Namespaces

Another key point is that the parent namespace has access to its child namespaces and their processes. This was evident in the previous section with the lsns command that printed the child namespace we created.

Similarly, we can output the processes running in child namespaces with the ps command:

$ ps aux | grep sleep
 1882 root     sudo unshare --pid --mount-proc --fork sleep 1000
 1883 root     unshare --pid --mount-proc --fork sleep 1000
 1884 root     sleep 1000

Here, we ran the ps command from the root namespace. As we expected, we can see the process with ID 1884 running in the child namespace. Notably, this process will have a different ID in its namespace.

As a result, the process will have two process IDs, one in its namespace and one in its parent’s.

5. Docker Example

Now, let’s see how PID namespaces work in Docker. We’ll create and start a new detached Alpine Linux container:

$ sudo docker run -d alpine:latest sleep 1000

Next, let’s run the lsns command again to output the accessible PID namespaces:

$ sudo lsns -t pid
4026531836 pid     115     1 root /sbin/init
4026532153 pid       1  2083 root sleep 1000

As expected, Docker created a new PID namespace with the identifier 4026532153. Now, let’s run ps:

$ ps aux | grep sleep
 2083 root     sleep 1000

Indeed, we can see a process that’s running the sleep command with PID 2083 in the parent namespace.

To find out the process ID of this process in its namespace we’ll execute the ps command in the container:

$ sudo docker container exec f0901 ps ax
    1 root      0:00 sleep 1000
    6 root      0:00 ps ax

As can be seen, the process ID of the process running the sleep command in the child namespace is 1. 

6. Process ID Mapping

In the previous section, we verified that a process running in a container has different process IDs depending on the PID namespace we’re in. Furthermore, we can find out the mapping of these process IDs in the status file of the /proc filesystem in the parent namespace:

$ sudo cat /proc/2083/status | grep NSpid
NSpid:  2083    1

The status file contains many fields. In this case, we’re interested in the NSpid field. The NSpid field holds a list with all the IDs of this process in the namespaces in which it participates. Consequently, the sleep process has a process ID 1 in the Docker container and 2083 in the host.

7. Conclusion

In this article, we learned about Linux namespaces and how Docker creates a new PID namespace when it starts a container. Moreover, we looked at some useful commands that deal with namespaces, like lsns and unshare. Finally, we saw how we can find the host process ID of a process running in a container.