Featured image of post 容器镜像根目录可见范围的实践

容器镜像根目录可见范围的实践

在使用容器过程中,我们都知道进入容器后,根目录会变化。在容器中看不到容器外的目录和文件,这本质上是命名空间隔离带来的能力。

对于容器镜像的结构,通常我们用只读层、读写层来表示不同层级在读写能力上的限制。使用 Docker 或者 Kubernetes 时,使用挂载 “卷” 来进行数据持久化,也就是保留容器中数据的变化,实现读写能力。而容器中本身带有的文件、目录,在销毁容器、重启容器后,它们又会恢复到最初的样子。

image-20230709213920094

本文使用Linux自带的一些工具来模拟容器中文件系统隔离的特性,将进程的根目录进行替换,替换成我们想给它指定的目录。

mount 命名空间

mount 命名空间隔离了每个进程可以看到的挂载目录。不同 mount 命名空间中的进程看到、控制的目录都可以是不同的。

mount 命名空间中有两个重要的概念:

  • “共享子树”,它用来解决挂载、卸载事件在不同的 mount namespace 中自动、可控的传递;
  • “对等组”,对等组是一组挂载点,本次实践涉及到共享子树中 MS_SHAREDMS_PRIVATE 两种传递类型,它们之间的含义是相反的,前者会和对等组共享挂载和卸载。

其中,我觉得难理解的、重要的地方就是 MS_SHAREDMS_PRIVATE 这两种不同的挂载点传递类型:

  • MS_SHARED:该挂载点和它的“对等组”共享挂载和卸载事件。当一个挂载点被删除或者添加到namespace中,这些事件会被传递到它的对等组。
  • MS_PRIVATE: 和共享挂载相反,标记为private的事件不会传递到任何的对等组。

这样一来,就能够控制容器中的文件系统的挂载是不是可以影响到其他mount 命名空间了。

环境

  • OS:Ubuntu22.04 5.19.0-46-generic
  • 需要使用的命令:pivot_rootunsharemount

实践

我们先看一下 OS 本身的根目录:

root@dev:/# ls
bin   cdrom  etc   lib    lib64   lost+found  mnt             opt   root  sbin  srv       sys  usr  workplace
boot  dev    home  lib32  libx32  media       namespace-feat  proc  run   snap  swapfile  tmp  var

我们最终要将这个 ssh 连接的 bash 进程的 mount 命名空间根目录变为在网上下载的 Ubuntu22.04 的 base 版本的目录。这里将其下载并进行解压。

image-20230709214918828

我这里把他解压放在这个目录中:

root@dev:/workplace/namespace-feat/mock-rootfs# ls
ubuntu-base-22.04-base-amd64

pivot_root

我们使用 pivot_root 可以实现容器中根目录可见性的效果:

root@dev:/# pivot_root -h

Usage:
 pivot_root [options] new_root put_old

Change the root filesystem.

Options:
 -h, --help     display this help
 -V, --version  display version

For more details see pivot_root(8).

特性

  • pivot_root命令用于将当前进程的根目录替换为指定目录;
  • 它需要两个参数,分别用于保存当前 mount 命名空间内进程的根挂载,和设置当前进程新的根挂载。分别叫做 put_oldnew_root
  • 挂载之后,它不会自动改变当前进程的根目录,可以使用 chdir("/") 显式更改到新的根目录。

注意

  1. new_rootput_old都必须是目录;
  2. new_rootput_old不在同一个mount namespace中;
  3. put_old必须是new_root,或者是new_root的子目录;
  4. new_root必须是mount point,且不能是当前mount namespace的 “/”。

chroot 和 pivot_root区别

  • chroot只改变当前进程的 “/”

  • pivot_root改变当前mount namespace的“/”

Step1 创建新的命名空间

root@dev:/workplace/namespace-feat/mock-rootfs# unshare -m

unshare -m 创建一个新的 mount 命名空间,并让当前进程进入。

Step2 为 pivot_root 命令准备 new_root 和 put_old

创建 put_old 对应的目录:

root@dev:/workplace/namespace-feat/mock-rootfs# mkdir -p ubuntu-base-22.04-base-amd64/.old

执行命令,但是会出现错误:

root@dev:/workplace/namespace-feat/mock-rootfs# pivot_root ubuntu-base-22.04-base-amd64/ ubuntu-base-22.04-base-amd64/.old/
pivot_root: failed to change root from `ubuntu-base-22.04-base-amd64/' to `ubuntu-base-22.04-base-amd64/.old/': Device or resource busy

这是为什么呢?上文提到过,new_root 和 put_old 必须在不同的 mount 命名空间中。而我们的ubuntu-base-22.04-base-amd64/ubuntu-base-22.04-base-amd64/.old/文件夹,都处于执行unshare -m之前的挂载命名空间的挂载目录中。

所以,我们需要再将ubuntu-base-22.04-base-amd64/挂载一次,因为我们之前执行了unshare -m,当前命令行正处于新的命名空间,因此,挂载后它就处于新的 mount 命名空间了:

root@dev:/workplace/namespace-feat/mock-rootfs# mount --bind ubuntu-base-22.04-base-amd64/ ubuntu-base-22.04-base-amd64/

Step3 执行 pivot_root 并查看根目录的改变

root@dev:/workplace/namespace-feat/mock-rootfs# pivot_root ubuntu-base-22.04-base-amd64/ ubuntu-base-22.04-base-amd64/.old/
root@dev:/workplace/namespace-feat/mock-rootfs# ls
ubuntu-base-22.04-base-amd64
root@dev:/workplace/namespace-feat/mock-rootfs# cd /
root@dev:/# ls
bin   dev  home  lib32  libx32  mnt  proc  run   srv  tmp  var
boot  etc  lib   lib64  media   opt  root  sbin  sys  usr

这样就可以成功切换了。上文提到,pivot_root 切换根目录后,不会自动切换目录。我们手动切到根目录查看文件,再与另一个终端进行对比:

image-20230709235112766

执行 pivot_root 后,实现了当前进程根目录的切换,就像我们进入容器中,查看根目录下的文件一样,同时宿主机(另一个终端)的根目录没有受到影响。

Ref:

容器镜像原理-根目录的替换

黄东升: mount namespace和共享子树

sparkdev

自认为是幻象波普星的来客
Built with Hugo
主题 StackJimmy 设计