要创建一个完整的 docker 容器,实际上就是为待创建的用户进程做以下几件事情:
1、启用 Linux Namespace 配置
Namespace 是 Linux 创建新进程的一个可选参数,
当我们通过系统调用 clone() 创建一个新进程时,就可以在参数中指定 CLONE_NEWPID 参数,
比如:
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
这时,新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的 PID 是 1。
在创建容器进程时,
通过指定该进程所需启用的一组 Namespace 参数,容器就只能“看到” 当前 Namespace 所限定的资源、文件、设备、状态、配置等。
Namespace 技术实际上修改了应用进程看待计算机的“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容。
但对于宿主机来说,这些被 “隔离” 了的进程跟其他进程并没有太大区别。
所以一个正在运行的 Docker 容器,
本质上就是一个特殊的 PID=1 的进程 ,也是其他后续创建的所有进程的父进程。
这就意味着,在一个容器中,你没办法同时运行两个不同的应用,
除非你能事先找到一个公共的 PID=1 的程序来充当两个不同应用的父进程,
这也是有些人会用 systemd 或者 supervisord 这样的软件来代替应用本身作为容器的启动进程的原因。
2、设置指定的 Cgroups 参数
Linux Cgroups 的全称是 Linux Control Group。
它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等。
Linux Cgroups 其实就是一个子系统目录加上一组资源限制文件的组合。
而对于 Docker 等 Linux 容器项目来说,
它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),
然后在启动容器进程之后,把这个进程的 PID 填写到对应控制组的 tasks 文件中就可以了。
而至于在这些控制组下面的资源文件里具体填上什么值,就需要用户在执行 docker run 时的参数中指定了,
比如这样一条命令:
docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash
所以一个正在运行的 Docker 容器,
其实就是一个启用了多个 Linux Namespace 的应用进程,而且这个进程能够使用的资源量,受 Cgroups 配置的限制。
3、切换进程的根目录(Change Root)。
比如:可以通过如下执行 chroot 命令,告诉操作系统,我们将使用 $HOME/test 目录作为 /bin/bash 进程的根目录:
chroot $HOME/test /bin/bash
这时,如果再执行 “ls /”,就会看到,它返回的都是 $HOME/test 目录下面的内容,而不是宿主机的内容。
而且对于被 chroot 的进程来说,它并不会感受到自己的根目录已经被“修改”成 $HOME/test 了。
而 Mount Namespace 正是基于对 chroot 的不断改良才被发明出来的,它也是 Linux 操作系统里的第一个 Namespace。
为了能够让容器的这个根目录看起来更“真实”,
我们一般会在这个容器的根目录下挂载一个完整操作系统的文件系统,比如 Ubuntu16.04 的 ISO。
这样,在容器启动之后,我们在容器里通过执行 “ls /” 查看根目录下的内容,就是 Ubuntu 16.04 的所有目录和文件。
而这个挂载在容器根目录上用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像” 了,也叫作:rootfs(根文件系统)。
所以,一个最常见的 rootfs 或者 说容器镜像,会包括如下所示的一些目录和文件:
ls /
bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var
需要注意的是 , rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。
在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。
所以,对于容器来说,它的 rootfs 只包括了操作系统的 “躯壳(文件、配置和目录)”,而并没有包括操作系统的 “灵魂(内核)”。
同一台机器上的所有容器,共享宿主机操作系统的内核。
最后用一张图总结下对 Docker 容器的认识:
由上图可知:
这个容器进程 “python app.py”,运行在由 Linux Namespace 和 Cgroups 构成的隔离环境里;
而它运行所需要的各种文件,比如 python,app.py,以及整个操作系统文件,则由多个联合挂载在一起的 rootfs 层提供。
这些 rootfs 层的最下层,是来自 Docker 镜像的只读层。
在只读层之上,是 Docker 自己添加的 Init 层,用来存放被临时修改过的 /etc/hosts 等文件。
而 rootfs 的最上层是一个可读写层,它以 Copy-on-Write 的方式存放任何对只读层的修改,容器声明的 Volume 的挂载点,也出现在这一层。
参考文档:张磊老师的 深入剖析Kubernetes