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
PID TTY STAT TIME COMMAND
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:
- unshare created a new child process to run the ps command that we supplied
- A new PID namespace was created and our child process joined it
- The child process is the init process of the new namespace
- The PID number space restarts
- 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
NS TYPE NPROCS PID USER COMMAND
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
NS TYPE NPROCS PID USER COMMAND
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
f0901c0b43329f6f997ec946597c3ab159c58a885cd081a8d0f855ae19c8188f
Next, let’s run the lsns command again to output the accessible PID namespaces:
$ sudo lsns -t pid
NS TYPE NPROCS PID USER COMMAND
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
PID USER TIME COMMAND
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.