如何浅显易懂的把容器技术讲明白之-namespace

我相信很多熟悉 Docker 的同学应该都知道如何安装 Docker、编写 Dockerfile 然后把应用跑起来,但是你真的理解 Docker 吗?

在很多的入门级Docker 资料中一定会出现一张这样的图

img

作者会告诉你虚拟机和 Docker 容器的区别,并且说明 Docker 容器相比较传统虚拟机启动速度快、部署方便等等优势,可是事实上右侧描述 Docker 的图是有问题,你知道吗?

如果你有接触过类似KVM、Vmware 等虚拟化产品,你一定知道,传统的虚拟机其实是模拟真实计算机硬件然后需要独立安装一个单独的操作系统,这个操作系统可以是 Linux 或者是 windows ,而 Docker 容器则不需要你去安装动则几十个 G 的操作系统,他提供的镜像启动后能够做到很小,比如几M大小,当我们执行

1
docker exec -it docker 进程 /bin/bash

进入到 docker 容器中,得到一个和宿主机一样的 shell 终端,和我们连接虚拟机得到的终端几乎没什么不同,我们还可以在终端中执行例如:

1
apt-get update

等命令,似乎这就是一台虚拟机,可是事实上真的是这样吗?

今天我们就一起来聊一聊 Docker,揭开 Docker 容器技术的本质。

进程

容器其实本质上就是一个进程,但是容器技术的核心功能,就是通过约束和修改进程的动态表现,创造出一个“边界”,通过“障眼法”让人觉得他是一个独立的系统。大多数容器都是使用 Cgroups 技术来约束进程,通过 namespace 技术来修改进程的视图。

那么什么是 namespace 和 cgroup 呢?

我们通过一个案例来讲解,这里我们使用 docker 运行一个 busybox 的容器

1
2
3
4
5
6
# docker run -it busybox /bin/sh
/ # ps
PID USER TIME COMMAND
1 root 0:00 /bin/sh
6 root 0:00 ps
/ #

我们可以看到在容器中,只有2 个进程在云信,一个PID 为 1 的进程,他就是我们的sh 程序。

我们在重新打开一个窗口执行执行 ps aux | grep docker

1
2
3
# ps aux | grep docker
root 3490 0.0 5.7 843208 58032 ? Ssl Aug14 0:22 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root 14669 0.0 6.3 709900 63612 pts/0 Sl+ 14:36 0:00 docker run -it busybox /bin/sh

可以看到进程14669 正在执行docker run -it busybox /bin/sh,事实上这个 14669 才是这个 docker 容器的真正 PID

那他是如何做到在我们 exec 进入容器之后把进程 ID 改成 1 的呢?事实上进程 ID=1 正是操作系统的第一号进程,他是所有进程的父进程。我们可以通过ps aux | grep systemd 查看会发现 systemd 为 1 号进行

1
2
ps aux | grep systemd
root 1 0.0 0.7 225308 7828 ? Ss Jun20 3:12 /lib/systemd/systemd --system --deserialize 39

现在 docker 把这个/bin/sh 的程序运行在容器中,就需要给这个 ID=14669 的进程做一些手脚,把他自己变成 1 号进程来骗过其他进程。这种机制就是对隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程 ID,可是实际上他还是宿主机的 14669 号进程。这种技术被称为 namespace。namespace 其实是在创建新进程时候加了一个可选参数,他利用 Linux 的系统调用 clone() 为新创建的进程指定一个 CLONE_NEWPID 的参数,那么新创建的进程就会看到一个全新的进程空间,在这个进程空间里面它的 PID 就是 1。

namespace 除了可以模拟 PID 之外,还提供了 Mout、UTS、IPC、Network 和 User 等,在不同的进程上下文做隔离操作。

而这些就是 Linux 容器的基本实现原理。

因此,我们说容器只不过是一种特殊的进程。