本文为从零开始写 Docker 系列第四篇,在mydocker run
基础上使用 pivotRoot
系统调用切换 rootfs 实现容器和宿主机之间的文件系统隔离。
完整代码见:https://github.com/lixd/mydocker
欢迎 Star
推荐阅读以下文章对 docker 基本实现有一个大致认识:
-
核心原理:深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs -
基于 namespace 的视图隔离:探索 Linux Namespace:Docker 隔离的神奇背后 -
基于 cgroups 的资源限制 -
初探 Linux Cgroups:资源控制的奇妙世界 -
深入剖析 Linux Cgroups 子系统:资源精细管理 -
Docker 与 Linux Cgroups:资源隔离的魔法之旅
-
-
基于 overlayfs 的文件系统:Docker 魔法解密:探索 UnionFS 与 OverlayFS -
基于 veth pair、bridge、iptables 等等技术的 Docker 网络:揭秘 Docker 网络:手动实现 Docker 桥接网络
开发环境如下:
root@mydocker:~#lsb_release-a
NoLSBmodulesareavailable.
DistributorID:Ubuntu
Description:Ubuntu20.04.2LTS
Release:20.04
Codename:focal
root@mydocker:~#uname-r
5.4.0-74-generic
注意:需要使用 root 用户
如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。
搜索公众号【探索云原生】即可订阅
1. 概述
前面几节中,我们通过 Namespace
和 Cgroups
技术创建了一个简单的容器,实现了视图隔离和资源限制。
但是大家应该可以发现,容器内的目录还是当前运行程序的宿主机目录,而且如果运行一下 mount 命令可以看到继承自父进程的所有挂载点。
这貌似和平常使用的容器表现不同
因为这里缺少了镜像
这么一个重要的特性。
Docker 镜像可以说是一项伟大的创举,它使得容器传递和迁移更加简单,那么这一节会做一个简单的镜像,让容器跑在有镜像的环境中。
即:本章会为我们切换容器的 rootfs,以实现文件系统的隔离。
2. 准备 rootfs
Docker 镜像包含了文件系统,所以可以直接运行,我们这里就先弄个简单的,直接将某个镜像中的所有内容作为我们的 rootfs 进行挂载。
即:先在宿主机上某一个目录上准备一个精简的文件系统,然后容器运行时挂载这个目录作为 rootfs。
首先使用一个最精简的镜像 busybox
来作为我们的文件系统。
busybox 是一个集合了非常多 UNIX 工具的箱子,它可以提供非常多在 UNIX 环境下经常使用的命令,可以说 busybox 提供了一个非常完整而且小巧的系统。
因此我们先使用它来作为第一个容器内运行的文件系统。
获得 busybox 文件系统的 rootfs 很简单,可以使用 docker export 将一个镜像打成一个 tar包,并解压,解压目录即可作为文件系统使用。
首先拉取镜像
dockerpullbusybox
然后使用该镜像启动一个容器,并用 export 命令将其导出成一个 tar 包
#执行一个交互式命令,让容器能一直后台运行
dockerrun-dbusyboxtop
#拿到刚创建的容器的Id
containerId=$(dockerps--filter"ancestor=busybox:latest"|grep-vIMAGE|awk'{print$1}')
echo"containerId"$containerId
#export从容器导出
dockerexport-obusybox.tar$containerId
最后将 tar 包解压
mkdirbusybox
tar-xvfbusybox.tar-Cbusybox/
这样就得到了 busybox 文件系统的 rootfs ,可以把这个作为我们的文件系统使用。
这里的 rootfs 指解压得到的 busybox 目录
busybox 中的内容大概是这样的:
[root@docker~]#ls-lbusybox
total16
drwxr-xr-x2rootroot12288Dec292021bin
drwxr-xr-x4rootroot43Jan1203:17dev
drwxr-xr-x3rootroot139Jan1203:17etc
drwxr-xr-x2nfsnobodynfsnobody6Dec292021home
drwxr-xr-x2rootroot6Jan1203:17proc
drwx------2rootroot6Dec292021root
drwxr-xr-x2rootroot6Jan1203:17sys
drwxrwxrwt2rootroot6Dec292021tmp
drwxr-xr-x3rootroot18Dec292021usr
drwxr-xr-x4rootroot30Dec292021var
可以看到,内容和一个完整的文件系统基本是一模一样的。
注意:rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。
在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。
3. 挂载 rootfs
把之前的 busybox rootfs 移动到/root/busybox
目录下备用。
实现原理
使用pivot_root
系统调用来切换整个系统的 rootfs,配合上 /root/busybox
来实现一个类似镜像的功能。
pivot_root
是一个系统调用,主要功能是去改变当前的 root 文件系统。
原型如下:
#include
intpivot_root(constchar*new_root,constchar*put_old);
-
new_root
:新的根文件系统的路径。 -
put_old
:将原根文件系统移到的目录。
使用 pivot_root
系统调用后,原先的根文件系统会被移到 put_old
指定的目录,而新的根文件系统会变为 new_root
指定的目录。这样,当前进程就可以在新的根文件系统中执行操作。
注意:new_root 和 put_old 不能同时存在当前 root 的同一个文件系统中。
pivotroot 和 chroot 有什么区别?
-
pivot_root 是把整个系统切换到一个新的 root 目录,会移除对之前 root 文件系统的依赖,这样你就能够 umount 原先的 root 文件系统。
-
而 chroot 是针对某个进程,系统的其他部分依旧运行于老的 root 目录中。
具体实现
具体实现如下:
/*
*
Init挂载点
*/
funcsetUpMount(){
pwd,err:=os.Getwd()
iferr!=nil{
log.Errorf("Getcurrentlocationerror%v",err)
return
}
log.Infof("Currentlocationis%s",pwd)
//systemd加入linux之后,mountnamespace就变成sharedbydefaul服务器托管网t,所以你必须显示
//声明你要这个新的mountnamespace独立。
//如果不先做privatemount,会导致挂载事件外泄,后续执行pivotRoot会出现invalidargument错误
err=syscall.Mount("","/","",syscall.MS_PRIVATE|syscall.MS_REC,"")
err=pivotRoot(pwd)
iferr!=nil{
log.Errorf("pivo服务器托管网tRootfailed,detail:%v",err)
return
}
//mount/proc
defaultMountFlags:=syscall.MS_NOEXEC|syscall.MS_NOSUID|syscall.MS_NODEV
_=syscall.Mount("proc","/proc","proc",uintptr(defaultMountFlags),"")
//由于前面pivotRoot切换了rootfs,因此这里重新mount一下/dev目录
//tmpfs是基于件系使用RAM、swap分区来存储。
//不挂载/dev,会导致容器内部无法访问和使用许多设备,这可能导致系统无法正常工作
syscall.Mount("tmpfs","/dev","tmpfs",syscall.MS_NOSUID|syscall.MS_STRICTATIME,"mode=755")
}
funcpivotRoot(rootstring)error{
/**
NOTE:PivotRoot调用有限制,newRoot和oldRoot不能在同一个文件系统下。
因此,为了使当前root的老root和新root不在同一个文件系统下,这里把root重新mount了一次。
bindmount是把相同的内容换了一个挂载点的挂载方法
*/
iferr:=syscall.Mount(root,root,"bind",syscall.MS_BIND|syscall.MS_REC,"");err!=nil{
returnerrors.Wrap(err,"mountrootfstoitself")
}
//创建rootfs/.pivot_root目录用于存储old_root
pivotDir:=filepath.Join(root,".pivot_root")
iferr:=os.Mkdir(pivotDir,0777);err!=nil{
returnerr
}
//执行pivot_root调用,将系统rootfs切换到新的rootfs,
//PivotRoot调用会把old_root挂载到pivotDir,也就是rootfs/.pivot_root,挂载点现在依然可以在mount命令中看到
iferr:=syscall.PivotRoot(root,pivotDir);err!=nil{
returnerrors.WithMessagef(err,"pivotRootfailed,new_root:%vold_put:%v",root,pivotDir)
}
//修改当前的工作目录到根目录
iferr:=syscall.Chdir("/");err!=nil{
returnerrors.WithMessage(err,"chdirto/failed")
}
//最后再把old_rootumount了,即umountrootfs/.pivot_root
//由于当前已经是在rootfs下了,就不能再用上面的rootfs/.pivot_root这个路径了,现在直接用/.pivot_root这个路径即可
pivotDir=filepath.Join("/",".pivot_root")
iferr:=syscall.Unmount(pivotDir,syscall.MNT_DETACH);err!=nil{
returnerrors.WithMessage(err,"unmountpivot_rootdir")
}
//删除临时文件夹
returnos.Remove(pivotDir)
}
然后再 build cmd 的时候指定:
funcNewParentProcess(ttybool)(*exec.Cmd,*os.File){
cmd:=exec.Command("/proc/self/exe","init")
//..省略其他代码
//指定cmd的工作目录为我们前面准备好的用于存放busyboxrootfs的目录
cmd.Dir="/root/busybox"
returncmd,writePipe
}
到此这一小节就完成了,测试一下。
4. 测试
测试比较简单,只需要执行 ls 命令,即可根据输出内容确定文件系统是否切换了。
root@mydocker:~/feat-rootfs/mydocker#gobuild.
root@mydocker:~/feat-rootfs/mydocker#./mydockerrun-it/bin/ls
{"level":"info","msg":"resConf:u0026{0}","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"commandallis/bin/ls","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"initcomeon","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"Currentlocationis/root/busybox","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"Findpath/bin/ls","time":"2024-01-12T16:19:32+08:00"}
bindevetchomeprocrootsystmpusrvar
可以看到,现在打印出来的就是/root/busybox
目录下的内容了,说明我们的 rootfs 切换完成。
如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。
搜索公众号【探索云原生】即可订阅
5.小结
本章核心如下:
-
准备 rootfs:将运行中的 busybox 容器导出并解压后作为 rootfs -
挂载 rootfs:使用 pivotRoot
系统调用,将前面准备好的目录作为容器的 rootfs 使用
在切换 rootfs 之后,容器就实现了和宿主机的文件系统隔离。
本章使用 pivotRoot 实现文件系统隔离,加上前面基于 Namespace 实现的视图隔离,基于 Cgroups 实现的资源限制,至此我们已经实现了一个 Docker 容器的几大核心功能。
完整代码见:https://github.com/lixd/mydocker
欢迎 Star
相关代码见 feat-rootfs
分支,测试脚本如下:
需要提前在 /root/busybox 目录准备好 rootfs,具体看本文第二节。
#克隆代码
gitclone-bfeat-rootfshttps://github.com/lixd/mydocker.git
cdmydocker
#拉取依赖并编译
gomodtidy
gobuild.
#测试查看文件系统是否变化
./mydockerrun-it/bin/ls
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net