手写docker—文件系统隔离(四)
容器文件系统隔离
前面实现了 container 容器创建及容器内部的资源管理和限制,但是容器内部的文件系统仍然和宿主机文件系统有关联,对容器内部文件系统的修改会影响到宿主机文件系统,因此需要单独隔离容器内部文件系统。
ufs介绍
Union File System:UFS 是一个将其它文件系统联合到一个联合挂载点的文件系统服务。它可以将许多文件系统例如 aufs、overlay 联合挂载到一个目录下形成单一的文件系统。
这些文件系统分支是只读或者可读写的,所以对这个虚拟后的联合文件系统进行写操作的时候,系统是真正写到了一个新的文件中。这个虚拟联合文件系统是可以对任何文件进行操作的,但它并没有真正地改变原文件,而是采用写时复制技术管理资源。
比如,现在有两个目录,目录结构如下:
$ tree
.
├── A
│ ├── a
│ └── x
└── B
├── b
└── x
通过联合挂载模式,将两个目录挂载到一个公共的目录上:
mkdir C
mount -t aufs -o dirs=./A:./B none ./C
查看挂载目录结构,发现多个文件系统下的文件挂载到了同一个目录:
$ tree ./C
./C
├── a
├── b
└── x
aufs文件系统
AUFS 只是 Docker 使用的存储驱动的一种,除了 AUFS 之外,Docker 还支持了不同的存储驱动,包括 aufs、devicemapper、overlay2、zfs 和 vfs 等等,在最新的 Docker 中,overlay2 取代了 aufs 成为了推荐的存储驱动,但是在没有 overlay2 驱动的机器上仍然会使用 aufs 作为 Docker 的默认驱动。
overlay2文件系统
Overlayfs 是一种堆叠文件系统,它依赖并建立在其它的文件系统之上(例如 ext4fs 和 xfs 等等),并不直接参与磁盘空间结构的划分,仅仅将原来底层文件系统中不同的目录进行“合并”,然后向用户呈现。
文件系统搭建
overlay 文件系统具有如下特点:
- 上下层同名目录合并;
- 上下层同名文件覆盖;
lower dir只提供数据不能写,upper dir可以读写;lower dir文件写时拷贝;
搭建一个 overlay2 文件系统目录:
[root@localhost overlay2]# mkdir ./{merged,work,upper,lower}
[root@localhost overlay2]# touch ./upper/{a,b}
[root@localhost overlay2]# touch ./lower/{a,c}
[root@localhost overlay2]# tree
.
├── lower
│ ├── a
│ └── c
├── merged
├── upper
│ ├── a
│ └── b
└── work
# 挂载文件目录
sudo mount -t overlay overlay -o lowerdir=./lower,upperdir=./upper,workdir=./work ./merged
# 挂载后文件目录结构
[root@localhost overlay2]# tree
.
├── lower
│ ├── a
│ └── c
├── merged
│ ├── a
│ ├── b
│ └── c
├── upper
│ ├── a
│ └── b
└── work
└── work
.assets/20240204212836.png)
overlay 实际文件目录结构如上图,其中 lower-dir、upper-dir 都存在 文件a,因此 lower-dir 中的文件被覆盖了,只显示 upper-dir 中的文件。
修改文件
lower 为底层目录,只提供数据不能写;upper 上层目录,可以读写。
# 修改 文件b、文件c
echo "will-persist" > ./merged/b
echo "wont-persist" > ./merged/c
# 从不同视图查看
[root@localhost overlay2]# echo "will-persist" > ./merged/b
[root@localhost overlay2]# echo "wont-persist" > ./merged/c
[root@localhost overlay2]# cat ./merged/b
will-persist
[root@localhost overlay2]# cat ./merged/c
wont-persist
[root@localhost overlay2]# cat ./upper/b
will-persist
[root@localhost overlay2]# cat ./lower/c
# 查看 upper 目录
[root@localhost overlay2]# ll ./upper/
总用量 8
-rw-r--r--. 1 root root 0 2月 4 21:19 a
-rw-r--r--. 1 root root 13 2月 4 21:41 b
-rw-r--r--. 1 root root 13 2月 4 21:41 c
[root@localhost overlay2]# cat ./upper/c
wont-persist
可以发现,overlay 文件系统对 lower-dir 层面文件修改不会在原目录生效,而是会在 upper-dir 中新建文件并修改。
overlay 文件系统采用 COW(copy-on-write) 写时复制技术,在对只读文件修改时复制数据到 upper-dir 目录中。
.assets/20240204220802.png)
删除文件
[root@localhost lower]# echo fff >> f
[root@localhost lower]# ls ../merged/
a b c f
[root@localhost lower]# cd ../merged/
[root@localhost merged]# ls ../lower/
a c f
[root@localhost merged]# ls
a b c f
[root@localhost merged]# rm -rf f
[root@localhost merged]# ls ../lower/
a c f
[root@localhost merged]# ls ../upper/
a b c f
[root@localhost merged]# rm -rf ../upper/f
[root@localhost merged]# ls
a b c f
[root@localhost merged]# ls ../upper/
a b c
[root@localhost merged]# ls
a b c f
[root@localhost merged]# cat f
fff
可以发现 overlay 删除 lower-dir 中的文件,其实也是在 upper-dir 中创建一个标记,表示这个文件已经被删除了,而不是删除真正的 lower-dir 的文件。
删除文件或文件夹时,会在 upper-dir 中添加一个同名标识文件,这个文件叫做 whiteout 文件。
添加文件
添加新文件,尝试在挂载目录中新增文件 g,查看各层级目录中文件情况:
[root@localhost merged]# echo ggg >> g
[root@localhost merged]# ls
a b c f g
[root@localhost merged]# ls ../upper/
a b c g
[root@localhost merged]# cat ../upper/g
ggg
overlay 挂载目录中新增文件实际上是在 upper-dir 目录中新增文件,并不会影响到其它层级目录。
如果删除 upper-dir 目录中的文件,挂载目录 merge-dir 中对应文件也会删除:
[root@localhost merged]# rm -rf ../upper/g
[root@localhost merged]# ls
a b c f
.assets/20240204225551.png)
容器镜像原理
我们在使用 docker 时会发现,docker 的文件修改不会影响到原宿主机,其次 docker 内部的文件系统是怎么来的?这里我们探究下 docker 镜像的实现原理。
文件系统
容器中的文件系统是基于 Mount Namespace 进行隔离的,在实现挂载操作后,容器内进程的视图会发生变化,所以我们在容器内看到的文件或目录都是独立于宿主机的。
rootfs
Mount Namespace 会修改容器进程对文件系统挂载点的认知,而这个挂载在容器根目录上,用来为容器进程提供隔离后执行环境的文件系统,就是容器镜像(又名 rootfs)。rootfs 只是提供了操作系统的文件、配置和目录,并不包括系统内核。docker 中所有容器都单独拥有 rootfs,但是都使用同一个操作系统内核。
.assets/20240225220909.png)
镜像层
docker 镜像的设计采用了镜像层的思想,比如我们通过 dockerfile 制作镜像的每一步操作,都会生成一个镜像层。镜像层的结构方便实现 rootfs 复用,不必每次都新建一个 rootfs,而是在原 rootfs 上增量更新。
组成 docker 镜像的目录可能分布在系统不同地方,通过 UFS 技术将不同位置的目录联合挂载(union mount)到同一个目录下。
.assets/20240225221611.png)
如上是容器内 rootfs 组成结构,除了底层只读的镜像层(Image layers),还包括可读写层(存储容器中产生的实时数据),init层(容器启动时初始化修改的数据部分)。
容器 rootfs 隔离
准备 rootfs
Docker 镜像中包含了文件系统,可以直接挂载运行,这里我们需要先准备自己的文件系统然后进行挂载。
即宿主机上首先在某一个目录下准备一个精简的文件系统,然后容器运行时挂载这个目录作为 rootfs。busybox 文件系统作为一个简单且完整的文件系统,我们可以使用 docker export 将一个运行的容器 busybox 打包成一个 tar压缩包,接着解压这个压缩包并将结果作为容器的文件系统使用。
# 拉取镜像
docker pull busybox
# 运行容器
docker run -d busybox top
# 获取运行的容器ID,打包成 tar 压缩包
containerId=$(docker ps --filter "ancestor=busybox:latest" | grep -v IMAGE|awk '{print $1}')
echo "containerId" $containerId
# 导出运行的容器
docker export -o busybox.tar $containerId
# 解压压缩包获取目标文件系统
mkdir busybox
tar -xvf busybox.tar -C busybox/
其中,busybox 文件系统的目录结构如下:
[root@docker ~]# ls -l busybox
total 16
drwxr-xr-x 2 root root 12288 Dec 29 2021 bin
drwxr-xr-x 4 root root 43 Jan 12 03:17 dev
drwxr-xr-x 3 root root 139 Jan 12 03:17 etc
drwxr-xr-x 2 nfsnobody nfsnobody 6 Dec 29 2021 home
drwxr-xr-x 2 root root 6 Jan 12 03:17 proc
drwx------ 2 root root 6 Dec 29 2021 root
drwxr-xr-x 2 root root 6 Jan 12 03:17 sys
drwxrwxrwt 2 root root 6 Dec 29 2021 tmp
drwxr-xr-x 3 root root 18 Dec 29 2021 usr
drwxr-xr-x 4 root root 30 Dec 29 2021 var
挂载 rootfs
将容器的 rootfs 挂载到刚刚创建好的 busybox 目录下,实际挂载效果可以通过 pivot_root 函数来实现。
#include <unistd.h>
int pivot_root(const char *new_root, const char *put_old);
new_root:新的根文件系统路径;put_old:存储旧文件系统的路径。
执行pivot_root系统调用,新的根文件系统会变为new_root,并且保存旧的文件系统到put_old。
这里需要注意 pivot_root 系统调用和 chroot 系统调用的区别:
pivot_root系统调用将整个系统切换到新的 root 目录,并且移除旧文件系统的依赖,方便umount旧文件系统;chroot系统调用是针对某个进程,系统的其它部分仍运行在旧的文件系统下。
代码
在由子进程执行容器初始化操作时,首先执行根文件系统 rootfs的挂载、proc 目录的挂载:
func mountProc() {
pwd, err := os.Getwd()
if err != nil {
log.Errorf("Get current location failed %v", err)
return
}
log.Infof("Current location is %s", pwd)
// change mount transferMode for private
if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil {
log.Errorf("mount default namespace failed, err = %v", err)
return
}
// remount rootfs
if err = privotRoot(pwd); err != nil {
log.Errorf("reMount failed %v", err)
}
// mount proc
defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
if err := syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), ""); err != nil {
log.Errorf("mount proc failed, err = %v", err)
return
}
}
容器宿主机写操作隔离
上一步已经实现了使用宿主机某个文件目录 /root/busybox 作为容器的根目录,但是容器内的修改等操作依然会影响到宿主机的原始目录。所以需要对容器和镜像进一步隔离,使得容器中的操作不会影响到原宿主机目录 /root/busybox。
Docker 在使用镜像启动一个容器时会使用 init-layer、read-write-layer。其中 init-layer 存储容器启动时修改的系统数据,read-write-layer 存储容器执行过程中产生的数据。最后将上述两个层级关联的目录统一挂载到一个 mnt 目录下,然后将这个 mnt 目录当作容器启动的根目录。所以我们在实现时,也需要先统一挂载好 overlayfs 目录,然后将当前进程切换到挂载好的 merge 目录下。
UFS 是一种将多个文件系统联合挂载的技术,UFS 有多种实现,包括 AUFS、Overlays。这里使用 Overlays 来实现联合挂载,Overlays 一般分为 lower、upper、merged、work 4 个目录:
lower:只读层,只负责提供数据;upper:读写层,所有的数据修改发生在这一层;merged:视图层,可以看到所有lower、upper层内容;work:overlays内部使用。
为了隔离容器和宿主机的写操作,我们将镜像目录(/root/busybox)作为 lower 目录,这样保证对镜像内容的修改都在 upper 目录中新生成一份副本。merged 目录作为容器 rootfs 保证可以看到所有文件内容。
docker 在启动时会创建两个 layer:write layer、container-init layer。其中 write layer 为可读写层,container-init-layer 为只读层。在容器启动前我们需要先 mount 好 overlays 目录然后执行 privotRoot 时切换到 mount 好的 overlays merge 目录。
基于 overlays 进行文件系统挂载,需要执行包括 createLower()、createDirs()、mountOverlayFS() 三个步骤:
- 准备
busybox目录,并且解压busybox.tar文件,解压后的文件目录作为容器只读层(可以理解为容器结构中最底层的镜像层); - 准备
mount overlays目录,创建好挂载overlays需要的upper、work、merged目录(可以理解为容器结构中最顶部的可读写层); - 实现
mount overlays,将merged目录作为挂载点,然后把busybox、upper挂载到merged目录; - 更新
rootfs为挂载点/root/merged目录,并切换宿主机目录root/busybox为/root/merged目录;
Overlays 挂载
联合挂载
首先创建好容器使用的 upper-dir、lower-dir、work-dir、merged-dir 四个基础目录,其中 lower-dir 就是上一节创建的文件系统目录。然后通过 mount 命令对 upper-dir、lower-dir、work-dir、merged-dir 系统目录进行联合挂载到 merged 目录。
/**
* create an Overlay fileSystem as container root workingspace
* 1)create lower-dir;
* 2)create upper-dir、work-dir;
* 3)create merged-dir and mount as overlayFS;
* 4)mount volume if exists;
*/
func NewWorkSpace(rootPath string, mntUrl string) {
createLower(rootPath)
createDirs(rootPath)
mountOverlayfs(rootPath, mntUrl)
}
/**
* create readOnly directory of lower-dir
*/
func createLower(rootPath string) {
busyboxUrl := rootPath + "busybox/"
busyboxTarUrl := rootPath + "busybox.tar"
_, err := os.Stat(busyboxUrl)
if err != nil && os.IsNotExist(err) {
log.Errorf("volume::createLower can't find %s file", busyboxUrl)
if err = os.Mkdir(busyboxUrl, Perm0755); err != nil {
log.Warnf("volume::createLower directory %s failed", busyboxUrl)
}
if _, err := exec.Command("tar", "-xvf", busyboxTarUrl, "-C", busyboxUrl).CombinedOutput(); err != nil {
log.Warnf("volume::createLower untar %s failed %v", busyboxTarUrl, err)
}
}
}
/**
* create upper-dir and work-dir of overlayFS
*/
func createDirs(rootPath string) {
upperUrl := rootPath + "upper/"
if err := os.Mkdir(upperUrl, Perm0755); err != nil {
log.Warnf("mkdir upper-dir %s error %v", upperUrl, err)
}
workUrl := RootUrl + "work/"
if err := os.Mkdir(workUrl, Perm0755); err != nil {
log.Warnf("mkdir work-dir %s error %v", workUrl, err)
}
}
/**
* mountOverlayFS
* mount -t overlay overlay -o lowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work merged
*/
func mountOverlayfs(rootUrl string, mntUrl string) {
// create mntUrl
if err := os.Mkdir(mntUrl, Perm0777); err != nil {
log.Warnf("mkdir mntUrl %s failed %v", mntUrl, err)
}
// combine arguments
// e.g. lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/work
dirs := "lowerdir=" + rootUrl + "busybox" + ",upperdir=" + rootUrl + "upper" + ",workdir=" + rootUrl + "work"
cmd := exec.Command("mount", "-t", "overlay", "overlay", "-o", dirs, mntUrl)
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Errorf("%v", err)
}
}
上述联合挂载操作需要在初始化 container 进程时执行:
rootDir := "/root/"
mntDir := "/root/merged/"
processCmd.ExtraFiles = []*os.File{readPipe}
processCmd.Dir = mntDir
processCmd.Env = append(os.Environ(), envSlice...)
// create overlay2 fileSystem as container root workingspace
NewWorkSpace(rootDir, mntDir)
取消挂载目录
Docker 会在删除容器时删除容器对应的 write layer、container-init layer,而保留镜像的所有内容。我们在退出并删除容器进程时,需要执行如下操作:
umount overlays:取消联合挂载目录;- 删除其它目录:删除
upper-dir、work-dir、merged-dir,保留lower-dir。
/**
* Delete overlayfs workingPlace
* 1)uninstall volume;
* 2)uninstall and delete merged directory;
* 3)uninstall and delete upper-dir、work-dir;
*/
func DeleteWorkSpace(rootUrl, mntUrl string) {
unmountOverlayfs(mntUrl)
removeDirs(rootUrl)
}
/**
* unmount overlayfs
*/
func unmountOverlayfs(mntUrl string) {
cmd := exec.Command("umount", mntUrl)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Errorf("unMountDir %s failed %v", mntUrl, err)
}
if err := os.RemoveAll(mntUrl); err != nil {
log.Errorf("removeMountDir %s error %v", mntUrl, err)
}
log.Infof("volume::unmountOverlayfs unmountFS %s successfully", mntUrl)
}
/**
* remove directories(lower-dir、upper-dir、work-dir) but save (lower-dir)
*/
func removeDirs(rootUrl string) {
writeDir := rootUrl + "upper/"
if err := os.RemoveAll(writeDir); err != nil {
log.Errorf("volume::removeDirs remove upper-dir %s failed %v", writeDir, err)
}
workDir := rootUrl + "work/"
if err := os.RemoveAll(workDir); err != nil {
log.Errorf("volume::removeDirs remove work-dir %s failed %v", workDir, err)
}
log.Infof("volume::removeDirs upper-dir %s work-dir %s successfully", writeDir, workDir)
}
测试
# 启动 docker 容器
[root@localhost Mydocker]# ./Mydockker run -it /bin/sh
{"level":"info","msg":"resourceConfig: \u0026{ 0 }","time":"2024-02-06T01:15:07+08:00"}
{"level":"warning","msg":"mkdir mntUrl /root/merged/ failed mkdir /root/merged/: file exists","time":"2024-02-06T01:15:07+08:00"}
{"level":"info","msg":"run::sendInitCommands all commands:/bin/sh","time":"2024-02-06T01:15:07+08:00"}
{"level":"info","msg":"exec init command","time":"2024-02-06T01:15:07+08:00"}
{"level":"info","msg":"Current location is /root/merged","time":"2024-02-06T01:15:07+08:00"}
{"level":"info","msg":"init::ContainerResourceInit execuatble path=/bin/sh","time":"2024-02-06T01:15:07+08:00"}
# 向容器内 /tmp/hello.txt 文件中写入数据
/ # echo helloWorld > tmp/hello.txt
# 查看宿主机 /root 目录
[root@localhost ~]# ls -al
总用量 129792
drwxr-xr-x. 13 root root 161 2月 5 16:18 busybox
-rw-------. 1 root root 4497408 2月 4 16:44 busybox.tar
drwxr-xr-x. 1 root root 29 2月 6 01:15 merged
drwxr-xr-x. 4 root root 29 2月 6 01:15 upper
drwxr-xr-x. 3 root root 18 2月 6 01:15 work
# 查看上述修改对宿主机的影响
[root@localhost ~]# ls busybox/tmp/
[root@localhost ~]# ls upper/tmp
hello.txt
[root@localhost ~]# ls work/work/
[root@localhost ~]# ls merged/
bin dev etc home lib lib64 proc root sys tmp usr var
[root@localhost ~]# cat upper/tmp/hello.txt
helloWorld
可以发现我们对容器内的文件操作并不会影响宿主机的源文件目录 /root/busybox,而是会在 upper-dir 中新创建文件 hello.txt,这也符合 overlayfs 文件系统的特点。
# 容器内执行退出操作
/ # exit
{"level":"info","msg":"volume::unmountOverlayfs unmountFS /root/merged successfully","time":"2024-02-06T01:21:32+08:00"}
{"level":"info","msg":"volume::removeDirs upper-dir /root/upper/ work-dir /root/work/ successfully","time":"2024-02-06T01:21:32+08:00"}
# 切换宿主机查看目录
[root@localhost ~]# ls -al
总用量 129792
dr-xr-x---. 19 root root 4096 2月 6 01:21 .
dr-xr-xr-x. 17 root root 224 5月 18 2023 ..
-rw-r--r--. 1 root root 0 5月 11 2023 0a
-rw-r--r--. 1 root root 0 5月 11 2023 1a
-rw-r--r--. 1 root root 0 5月 11 2023 1a?1a?2a?2:q
drwxr-xr-x. 4 root root 62 4月 8 2023 6.824
-rw-------. 1 root root 1418 11月 16 2022 anaconda-ks.cfg
-rw-------. 1 root root 12149 2月 6 00:13 .bash_history
-rw-r--r--. 1 root root 18 12月 29 2013 .bash_logout
-rw-r--r--. 1 root root 176 12月 29 2013 .bash_profile
-rw-r--r--. 1 root root 176 12月 29 2013 .bashrc
drwxr-xr-x. 9 root root 4096 4月 9 2023 boost_1_71_0
-rw-r--r--. 1 root root 118520935 4月 9 2023 boost_1_71_0.tar.gz
drwxr-xr-x. 13 root root 161 2月 5 16:18 busybox
-rw-------. 1 root root 4497408 2月 4 16:44 busybox.tar
drwxr-xr-x. 7 root root 88 1月 6 18:36 .cache
drwxr-xr-x. 3 root root 18 1月 25 23:23 cgroup
drwxrwxr-x. 15 root root 4096 4月 4 2023 cmake-3.22.3
-rw-r--r--. 1 root root 9779118 4月 4 2023 cmake-3.22.3.tar.gz
drwx------. 4 root root 27 1月 6 18:36 .config
-rw-r--r--. 1 root root 100 12月 29 2013 .cshrc
drwx------. 3 root root 63 6月 5 2023 .docker
-rw-r--r--. 1 root root 14686 8月 10 23:08 .gdbinit
drwxr-xr-x. 3 root root 19 4月 4 2023 .local
drwxr-xr-x. 1 root root 6 2月 5 17:04 merged
发现在退出容器进程时,会自动删除 upper-dir、work-dir、merged 目录,保留原宿主机的 lower-dir 目录。

浙公网安备 33010602011771号