在使用容器过程中,我们都知道进入容器后,根目录会变化。在容器中看不到容器外的目录和文件,这本质上是命名空间隔离带来的能力。
对于容器镜像的结构,通常我们用只读层、读写层来表示不同层级在读写能力上的限制。使用 Docker 或者 Kubernetes 时,使用挂载 “卷” 来进行数据持久化,也就是保留容器中数据的变化,实现读写能力。而容器中本身带有的文件、目录,在销毁容器、重启容器后,它们又会恢复到最初的样子。
本文使用Linux自带的一些工具来模拟容器中文件系统隔离的特性,将进程的根目录进行替换,替换成我们想给它指定的目录。
mount 命名空间
mount 命名空间隔离了每个进程可以看到的挂载目录。不同 mount 命名空间中的进程看到、控制的目录都可以是不同的。
mount 命名空间中有两个重要的概念:
- “共享子树”,它用来解决挂载、卸载事件在不同的 mount namespace 中自动、可控的传递;
- “对等组”,对等组是一组挂载点,本次实践涉及到共享子树中
MS_SHARED
和MS_PRIVATE
两种传递类型,它们之间的含义是相反的,前者会和对等组共享挂载和卸载。
其中,我觉得难理解的、重要的地方就是 MS_SHARED
和 MS_PRIVATE
这两种不同的挂载点传递类型:
MS_SHARED
:该挂载点和它的“对等组”共享挂载和卸载事件。当一个挂载点被删除或者添加到namespace中,这些事件会被传递到它的对等组。MS_PRIVATE
: 和共享挂载相反,标记为private的事件不会传递到任何的对等组。
这样一来,就能够控制容器中的文件系统的挂载是不是可以影响到其他mount 命名空间了。
环境
- OS:Ubuntu22.04 5.19.0-46-generic
- 需要使用的命令:
pivot_root
,unshare
,mount
等
实践
我们先看一下 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
版本的目录。这里将其下载并进行解压。
我这里把他解压放在这个目录中:
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_old
和new_root
; - 挂载之后,它不会自动改变当前进程的根目录,可以使用
chdir("/")
显式更改到新的根目录。
注意
new_root
和put_old
都必须是目录;new_root
和put_old
不在同一个mount namespace
中;put_old
必须是new_root
,或者是new_root
的子目录;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 切换根目录后,不会自动切换目录。我们手动切到根目录查看文件,再与另一个终端进行对比:
执行 pivot_root 后,实现了当前进程根目录的切换,就像我们进入容器中,查看根目录下的文件一样,同时宿主机(另一个终端)的根目录没有受到影响。
Ref: