容器里的进程看到的文件系统又是什么样子的呢?
Mount Namespace 修改的,是容器进程对文件系统“挂载点”的认知。
Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效。
Mount Namespace (/maʊnt/)正是基于对 chroot (/ʃəˈrʊt/)的不断改良才被发明出来的,它也是 Linux 操作系统里的第一个 Namespace。
为了能够让容器的这个根目录看起来更“真实”,我们一般会在这个容器的根目录下挂载一个完整操作系统的文件系统,比如 Ubuntu16.04 的 ISO。这样,在容器启动之后,我们在容器里通过执行 "ls /" 查看根目录下的内容,就是 Ubuntu 16.04 的所有目录和文件。
而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。
rootfs目录
一个常见的rootfs,或者说容器镜像下会包括下列目录
$ ls /
bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var
docker创建用户进程过程
对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:
-
启用 Linux Namespace 配置;
-
设置指定的 Cgroups 参数;
-
切换进程的根目录(Change Root)。
这样,一个完整的容器就诞生了。不过,Docker 项目在最后一步的切换上会优先使用 pivot_root 系统调用,如果系统不支持,才会使用 chroot。这两个系统调用虽然功能类似,但是也有细微的区别
chroot是只改变即将运行的 某进程的根目录。pviot_root主要是把整个系统切换到一个新的root目录,然后去掉对之前rootfs的依赖,以便于可以umount 之前的文件系统(pivot_root需要root权限)
rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。
正是由于 rootfs 的存在,容器才有了一个被反复宣传至今的重要特性:一致性。
由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。
对一个应用来说,操作系统本身才是它运行所需要的最完整的“依赖库”。
有了容器镜像“打包操作系统”的能力,这个最基础的依赖环境也终于变成了应用沙盒的一部分。这就赋予了容器所谓的一致性:无论在本地、云端,还是在一台任何地方的机器上,用户只需要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就被重现出来了。
问题:难道我每开发一个应用,或者升级一下现有的应用,都要重复制作一次 rootfs 吗?
Docker 在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。
一个正在运行的 Linux 容器,其实可以被“一分为二”地看待:
一组联合挂载在 /var/lib/docker/aufs/mnt 上的 rootfs,这一部分我们称为“容器镜像”(Container Image),是容器的静态视图;
一个由 Namespace+Cgroups 构成的隔离环境,这一部分我们称为“容器运行时”(Container Runtime),是容器的动态视图。