动手实现一个docker引擎-3-实现文件系统隔离、Volume与镜像打包

学习自《自己动手写Docker》

作者:陈显鹭(花名:遥鹭)-阿里云高级研发工程师

京东购买链接:https://item.jd.com/10033552355433.html

其他链接:

  • https://zhuanlan.zhihu.com/p/101096040
  • https://www.chaochaogege.com/2019/09/11/2/

往期:

一、构造镜像

之前我们构造了一个简单版本的Run命令的docker容器引擎,但是功能上还有一个问题就是容器中使用的目录还是当前宿主机的目录,文件没有实现隔离。而且如果使用mount命令会发现,容器中继承了所有父进程的挂载点,这不是我们想要的容器实现,因为这里缺少了镜像这样一个重要的特性。

Docker中的镜像是一个伟大的创举,它使得容器的传递与迁移变得更加简单,所以在本章中,我们会制作一个简单的镜像,让容器跑在有镜像的环境中。

1. busybox

我们选择一个最精简的镜像busybox。其是一个继承了许多Unix工具的箱子,提供了很多在Unix下使用的命令

本节使用其作为容器内运行的文件系统

获得busybox文件系统的rootfs很简单,可以使用docker export将一个镜像打包成为一个tar包:

$ docker pull busybox
$ docker run -d busybox top -b
$ mkdir busybox && tar -xvf busybox.tar -C busybox/
$ cd busybox/ && ll -a
total 56
drwxr-xr-x 12 root   root     4096 Nov 11 22:24 ./
drwxr-xr-x  8 root   root     4096 Nov 11 22:24 ../
drwxr-xr-x  2 root   root    12288 Oct 27 02:46 bin/
drwxr-xr-x  4 root   root     4096 Nov 11 22:21 dev/
-rwxr-xr-x  1 root   root        0 Nov 11 22:21 .dockerenv*
drwxr-xr-x  3 root   root     4096 Nov 11 22:21 etc/
drwxr-xr-x  2 nobody nogroup  4096 Oct 27 02:46 home/
drwxr-xr-x  2 root   root     4096 Nov 11 22:21 proc/
drwx------  2 root   root     4096 Oct 27 02:46 root/
drwxr-xr-x  2 root   root     4096 Nov 11 22:21 sys/
drwxrwxrwt  2 root   root     4096 Oct 27 02:46 tmp/
drwxr-xr-x  3 root   root     4096 Oct 27 02:46 usr/
drwxr-xr-x  4 root   root     4096 Oct 27 02:46 var/

上面就是busybox镜像的文件结构

2. pivot_root

当我们fork新的进程,子进程会使用父进程的文件系统。

但如果我们想要把子进程的 / 文件系统修改成自定义的目录该怎么办呢?

这时候就要使用 [pivot_root](https://linux.die.net/man/2/pivot_root)

pivot_root(new_root, put_old)

它的作用是将子进程的 / 更改为 new_root,原 / 存放到 put_old 文件夹下。

基础概念

pivot_root是一个系统调用,主要的功能就是改变当前的root系统。可以将当前进程的root文件系统移动到put_old文件夹中,然后使new_root成为新的root文件系统。

对于pivot_root的更多手册信息:pivot_root(2) — Linux manual page

  1. pivot_root改变当前进程所在mount namespace内的所有进程的root mount移到put_old,然后将new_root作为新的root mount;
  2. pivot_root并没有修改当前调用进程的工作目录,通常需要使用chdir("/")来实现切换到新的root mount的根目录。

root mount可以理解为rootfs,也就是“/”,pivot_root将所在mount namespace中的所有进程的“/”改为了new_root
注意,pivot_root没有改变当前调用进程的工作目录
注意,pivot_root的调用前提需要明确在fork进程时指定mount namespace参数

对于pivot_root还有一些约束条件:

主要约束条件:

  1. new_rootput_old都必须是目录
  2. new_rootput_old不能与当前根目录在同一个挂载上。
  3. put_old必须是new_root,或者是new_root的子目录
  4. new_root必须是一个挂载点,但不能是"/"。还不是挂载点的路径可以通过绑定将路径挂载到自身上转换为挂载点。

使用流程

*pivot_root(new_root, put_old)*的一般使用流程:

  1. 首先创建一个new_root的临时子目录作为put_old,然后调用pivot_root实现切换
  2. chdir("/")
  3. umount put_old and clear

对比chroot、switch_root

chroot的主要区别在于:pivot_root是把整个系统切换到一个新的root目录,而移除对之前root系统的依赖,这样就可以unmount原先的root系统;而chroot是针对某个进程,系统的其他部分仍然运行于老的root目录中。

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

pivot_root改变当前mount namespace的“/”

switch_rootchroot类似,但是专门用来初始化系统时候使用的(initramfs),不仅会chroot,而且会删除旧根下的所有内容,释放内存,只能由pid=1的进程使用,其他地方用不到

3. Version4-实现pivot_root

这一节的版本实现了将镜像的目录设置为容器启动的根目录

本节代码获取:

$ git clone https://github.com/xwjahahahaha/myDocker.git
$ git checkout 2ad7a

项目结构:

.
├── README.md
├── cgroups
│   ├── cgroup_manager.go
│   └── subsystems
│       ├── cpu.go
│       ├── cpuset.go
│       ├── memory.go
│       ├── subsystem.go
│       └── utils.go
├── cmd
│   ├── commands.go
│   ├── init.go
│   └── root.go
├── container
│   ├── initContainer.go
│   ├── process.go
│   └── run.go
├── go.mod
├── go.sum
├── log
│   ├── init.go
│   └── log.go
├── main.go

1. 前置准备

首先复习一下mount的命令:

mount --bind olddir newdir:将文件系统层次结构的一部分重新挂载到其他地方,调用之后相同的内容可以在两个地方访问。

我们的设计的目标就是将当前的容器启动目录在容器内部就是镜像的目录(我们的镜像就是busybox)

在容器的当前启动目录下创建一个.pivot_root 文件夹即作为put_old (将当前init进程/容器进程的root文件系统/宿主机root文件系统(/root)存储到了put_old文件夹)

然后使用pivot_root让当前容器的启动目录成为new_root即新的根目录

我们需要考察一下是否满足约束条件:

1满足,3:.pivot_root是当前容器启动目录的子目录也满足

2与4都不满足,我们其实只要在上面的操作之前将当前启动目录重新mount bind一次,这样new_root就是一个挂载点并且第二点也同时满足了

然后,我们只需要使用chdir修改当前的工作目录到根目录即可,此时的根目录已经被换成了镜像的目录了。

最后,解除挂载点put_old并删除这个目录

2. 实现

首先我们将子进程启动的目录设置为之前busybox的目录:container/process.go

func NewParentProcess(tty bool) (*exec.Cmd, *os.File) {
    
    
	...
	// 如果设置了交互,就把输出都导入到标准输入输出中
	if tty {
    
    
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr 
	}
	// 设置进程启动的路径
	cmd.Dir = "/root/projects/golang_project/src/myDocker/busybox"
	...
}

注意:这里cmd.Dir依据你存放的busybox镜像的解压目录来

编写pivot_root的逻辑,在container/initContainer:

// pivotRoot
// @Description: 使用pivot_root更改当前root文件系统
// @param root	指定的新的根目录(一般就是容器的启动目录)
// @return error
func pivotRoot(root string) error {
    
    
	// 重新mount新的根目录
	if err := syscall.Mount(root, root, "bind", syscall.MS_BIND | syscall.MS_REC, ""); err != nil {
    
    
		return fmt.Errorf(" Mount rootfs to itself error: %v", err)
	}
	// 创建临时文件.pivot_root存储old_root
	pivotPath := filepath.Join(root, ".pivot_root")
	// 判断当前目录是否已有该文件夹
	if _ ,err := os.Stat(pivotPath); err == nil {
    
    
		// 存在则删除
		if err := os.Remove(pivotPath); err != nil {
    
    
			return err
		}
	}
	if err := os.Mkdir(pivotPath, 0777); err != nil {
    
    
		return err
	}
	// pivot_root将原根目录挂载到.pivot_root上,然后将root设置为新的根目录文件系统
	if err := syscall.PivotRoot(root, pivotPath); err != nil {
    
    
		return fmt.Errorf(" Pivot root err %v", err)
	}
	// 修改当前的工作目录到根目录
	if err := syscall.Chdir("/"); err != nil {
    
    
		return fmt.Errorf(" Chdir / %v", err)
	}
	// 取消临时文件.pivot_root的挂载并删除它
	pivotPath = filepath.Join("/", ".pivot_root")		// 注意当前已经在根目录下,所以临时文件的目录也改变了
	if err := syscall.Unmount(pivotPath, syscall.MNT_DETACH); err != nil {
    
    
		return fmt.Errorf(" Unmount .pivot_root dir %v", err)
	}
	return os.Remove(pivotPath)
}

func setUpMount()  {
    
    
	// 首先设置根目录为私有模式,防止影响pivot_root
	if err := syscall.Mount("/", "/", "", syscall.MS_REC | syscall.MS_PRIVATE, ""); err != nil {
    
    
		log.LogErrorFrom("setUpMount", "Mount proc", err)
	}
	// 获取当前路径
	pwd, err := os.Getwd()
	if err != nil {
    
    
		log.LogErrorFrom("setUpMount", "Getwd", err)
	}
	log.Log.Infof("Current location is %s", pwd)
	// 使用pivot root
	if err := pivotRoot(pwd); err != nil {
    
    
		log.LogErrorFrom("setUpMount", "pivotRoot", err)
	}
	// 设置一些挂载
	// 挂载/proc文件系统
	// 设置挂载点的flag
	defaultMountFlags :=  syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
	if err := syscall.Mount("", "/proc", "proc", uintptr(defaultMountFlags), ""); err != nil {
    
    
		log.LogErrorFrom("setUpMount", "Mount proc", err)
	}
	if err := syscall.Mount("tmpfs", "/dev", "tmpfs", syscall.MS_NOSUID | syscall.MS_STRICTATIME, "mode=755"); err != nil {
    
    
		log.LogErrorFrom("setUpMount", "Mount /dev", err)
	}
}

这里将之前的Mount提取并增加了pivotRoot的调用设置,在setUpMount函数的最开始,我们修改了根目录的挂载传播方式为私有,这与之前只设置/proc是不同的,原因在于:

pivot_root的Invalid argument错误

Docker runC 下有这么一段话

// Make parent mount PRIVATE if it was shared. It is needed for two
// reasons. First of all pivot_root() will fail if parent mount is
// shared. Secondly when we bind mount rootfs it will propagate to
// parent namespace and we don't want that to happen.

意思其实就是mount的传播问题:必须让父进程、子进程都不是分享模式。pivot root 不允许 parent mount point 和 new mount point 是 shared。因为相互之间会进行传播影响。在之前我们只让/proc私有挂载,我们只需要改动一个小地方让全部都是私有挂载

3. 测试

$ ./mydocker run -t sh
$ pwd
$ mount

afHbEQ

可以看到到容器启动时,已经将镜像的目录(busybox)虚拟到了容器的根目录。并且mount的信息中只有子进程的挂载的设备,而没有父进程之前挂载的设备。

我们在根目录下创建一个测试文件,然后在宿主机上查看:

$ ./mydocker run -t sh
$ cat > test.txt << EOF
> i am write in my container
> EOF
$ cat test.txt 
i am write in my container
$ exit

# 宿主机查看:
$ cd busybox
$ cat test.txt 
i am write in my container

说明虽然实现了将容器的根目录虚拟成了镜像的目录,但是文件系统还是宿主机的系统,没有实现文件操作的隔离。

4. Version5-实现AUFS

在之前学习AUFS的时候我们了解到,使用镜像启动一个容器的时候,会创建两个层:init只读层与一个读写层

然后将这两个层/文件夹以及相关的镜像层/目录都挂载到一个mnt的目录下,这个目录就是容器启动的目录

我们在上一个版本直接将镜像的目录作为了容器的根目录,但是在容器内的操作仍然会影响到宿主机的对应的镜像目录。本节就是进一步的通过AUFS实现容器与镜像文件系统的隔离

本节代码获取:

$ git clone https://github.com/xwjahahahaha/myDocker.git
$ git checkout b684d

1. 实现

在创建命令的时候创建各个层级的文件夹(镜像需要准备对应的tar压缩包):container/process

// NewWorkSpace
// @Description: 创建新的文件工作空间
// @param rootURL
// @param mntURL
func NewWorkSpace(rootURL, imageName, mntURL string) {
    
    
	CreateReadOnlyLayer(rootURL, imageName)      // 创建init只读层
	CreateWriteLayer(rootURL)                    // 创建读写层
	CreateMountPoint(rootURL, imageName, mntURL) // 创建mnt文件夹并挂载
}

// CreateReadOnlyLayer
// @Description: 通过镜像的压缩包解压并创建镜像文件夹作为只读层
// @param rootURL
// @param imageName
func CreateReadOnlyLayer(rootURL, imageName string) {
    
    
	imageName = strings.Trim(imageName, "/")
	imageDir := rootURL + imageName + "/"
	imageTarPath := rootURL + imageName + ".tar"
	if has, err := dirOrFileExist(imageTarPath); err == nil && !has {
    
    
		log.Log.Errorf(" Target image tar file not exist!")
		return
	}
	if has, err := dirOrFileExist(imageDir); err == nil && !has {
    
    
		// 创建文件夹
		if err := os.Mkdir(imageDir, 0777); err != nil {
    
    
			log.LogErrorFrom("createReadOnlyLayer", "Mkdir", err)
		}
	}
	if _, err := exec.Command("tar", "-xvf", imageTarPath, "-C", imageDir).CombinedOutput(); err != nil {
    
    
		log.LogErrorFrom("createReadOnlyLayer", "tar", err)
	}
}

// CreateWriteLayer
// @Description: 创建读写层
// @param rootURL
func CreateWriteLayer(rootURL string) {
    
    
	writeURL := rootURL + "writeLayer/"
	if has, err := dirOrFileExist(writeURL); err == nil && has {
    
    
		log.Log.Info("Write layer dir already exist. Delete and create new one.")
		// 如果存在则先删除掉之前的
		DeleteWriteLayer(rootURL)
	}
	if err := os.Mkdir(writeURL, 0777); err != nil {
    
    
		log.LogErrorFrom("createWriteLayer", "Mkdir", err)
	}
}

// CreateMountPoint
// @Description: 挂载到容器目录mnt
// @param rootURL
// @param imageName
// @param mntURL
func CreateMountPoint(rootURL, imageName, mntURL string) {
    
    
	if has, err := dirOrFileExist(mntURL); err == nil && has {
    
    
		log.Log.Info("mnt dir already exist. Delete and create new one.")
		DeleteMountPoint(mntURL)
	}
	if err := os.Mkdir(mntURL, 0777); err != nil {
    
    
		log.LogErrorFrom("CreateMountPoint", "Mkdir", err)
	}
	// 将读写层目录与镜像只读层目录mount到mnt目录下
	dirs := "dirs=" + rootURL + "writeLayer:" + rootURL + imageName
	cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "myDockerMnt", mntURL)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
    
    
		log.LogErrorFrom("createMountPoint", "mount", err)
	}
}
  • 根据镜像文件创建容器只读层
  • 手动创建读写层
  • 然后将这些目录都挂载到mnt文件夹,使之成为容器的启动目录

重新设置容器的启动目录:container/process

func NewParentProcess(tty bool) (*exec.Cmd, *os.File) {
    
    
	....
	// 如果设置了交互,就把输出都导入到标准输入输出中
	if tty {
    
    
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
	}
	// 创建新的工作空间
	rootUrl := "./"
	mntUrl := "./mnt"
	imageName := "busybox"
	NewWorkSpace(rootUrl, imageName, mntUrl)
	cmd.Dir = mntUrl 			// 设置进程启动的路径
	...
	return cmd, writePipe
}

在docker中容器被删除后需要删除掉之前创建的只读层与读写层,镜像的目录是不会删除的。在这里镜像的目录就是容器的只读层,所以我们编写代码实现当容器结束时删除掉读写层(在删除之前还需要解开挂载umount)即可:container/process

// DeleteWorkSpace
// @Description: 当容器删除时一起删除工作空间
// @param rootURL
// @param mntURL
func DeleteWorkSpace(rootURL, mntURL string) {
    
    
	// 镜像层的目录不需要删除
	DeleteMountPoint(mntURL)
	DeleteWriteLayer(rootURL)
}

// DeleteMountPoint
// @Description: 取消挂载点并删除mnt目录
// @param mntURL
func DeleteMountPoint(mntURL string) {
    
    
	// 取消mnt目录的挂载
	cmd := exec.Command("umount", mntURL)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
    
    
		log.LogErrorFrom("deleteMountPoint", "umount", err)
	}
	// 删除mnt目录
	if err := os.RemoveAll(mntURL); err != nil {
    
    
		log.LogErrorFrom("deleteMountPoint", "remove", err)
	}
}

// DeleteWriteLayer
// @Description: 删除读写层目录
// @param rootURL
func DeleteWriteLayer(rootURL string) {
    
    
	writeURL := rootURL + "writeLayer/"
	if err := os.RemoveAll(writeURL); err != nil {
    
    
		log.LogErrorFrom("deleteWriteLayer", "remove", err)
	}
}

最后在对应wait的后面即容器结束时加入删除的语句: contianer/run.go

func Run(tty bool, cmdArray []string, res *subsystems.ResourceConfig, cgroupName string){
    
    
	...
	cgroupManager.Destroy()
	// 删除设置的AUFS工作目录
	rootUrl := "./"
	mntUrl := "./mnt"
	DeleteWorkSpace(rootUrl, mntUrl)
	os.Exit(1)
}

2. 使用流程

image-20211113151222885

3. 测试

我们创建一个文件,然后观察变化

$ ./mydocker run -t sh 
$ ls
$ cat > test.txt << EOF 
> hahaha
> EOF
$ cat test.txt 

CnKiFn

此时不要关闭容器,在另一个终端查看文件的变化:

先检查镜像只读文件夹busybox中是否有这个文件:

再检查读写层是否有这个文件:

posR22

我们可以发现,在容器中新创建的文件只会在读写层创建,而不会影响到镜像的文件

下面我们在容器中将这个文件删除:

$ rm -rf test.txt 
$ ls

bin   dev   etc   home  proc  root  sys   tmp   usr   var

发现容器中找不到这个文件了,我们再从新的终端查看宿主机的文件情况:

可以发现对应的文件被删除了:

MLkrJP

如果我们在容器中删除镜像本来就自带的文件,那么会在读写层创建一个对应名称的.wh文件来屏蔽这个文件不可读实现假删除,例如我们在容器中删除了tmp

在宿主机上查看:

AOQmqL

5. Version6-实现Volume数据卷

上一节实现了AUFS的文件隔离,但是一旦退出了容器,所有的容器层数据就会被清理掉。在某些场景下希望有数据的持久化存储。docker的volume技术就实现了这样的功能。这一节我们就将实现这个功能。

本节代码获取:

$ git clone https://github.com/xwjahahahaha/myDocker.git
$ git checkout d026a

结构变化:

.
├── README.md
├── cgroups
│   ├── cgroup_manager.go
│   └── subsystems
│       ├── cpu.go
│       ├── cpuset.go
│       ├── memory.go
│       ├── subsystem.go
│       └── utils.go
├── cmd
│   ├── commands.go
│   ├── init.go
│   └── root.go
├── container
│   ├── initContainer.go
│   ├── process.go
│   ├── run.go
│   ├── utils.go
│   └── workspace.go
├── go.mod
├── go.sum
├── log
│   ├── init.go
│   └── log.go
├── main.go

workspace就是从process中分出来的专门负责文件工作空间函数的文件

1. 实现

添加一个run命令运行的flag -v后面跟需要创建的数据卷

cmd/init.go

runDocker.Flags().StringVarP(&Volume, "volume", "v", "", "add a volume")

修改run函数的参数,传递数据卷字符串分别到NewWorkSpaceDeleteWorkSpace

cmd/commands

var runDocker = &cobra.Command{
    
    
	Use:   "run [command]",
	Short: runUsage,
	Long:  runUsage,
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
    
    
		// 获取交互flag值与command, 启动容器
		container.Run(tty, strings.Split(args[0], " "), ResourceLimitCfg, CgroupName, Volume)
	},
}

container/run.go

func Run(tty bool, cmdArray []string, res *subsystems.ResourceConfig, cgroupName string, volume string){
    
    
	// 获取到管道写端
	parent, pipeWriter := NewParentProcess(tty, volume)
  ...
  ...
  DeleteWorkSpace(rootUrl, mntUrl, volume)
	os.Exit(1)
}

container/process.go

func NewParentProcess(tty bool, volume string) (*exec.Cmd, *os.File) {
    
    
  ...
  NewWorkSpace(rootUrl, imageName, mntUrl, volume)
  ...
}

重点在于NewWorkSpaceDeleteWorkSpace两个函数的修改:

container/workspace

如果传递的volume不是空且可以通过校验,那么就比之前多执行MountVolume函数

func NewWorkSpace(rootURL, imageName, mntURL, volume string) {
    
    
	CreateReadOnlyLayer(rootURL, imageName)      // 创建init只读层
	CreateWriteLayer(rootURL)                    // 创建读写层
	CreateMountPoint(rootURL, imageName, mntURL) // 创建mnt文件夹并挂载
	if volume != "" {
    
    
		// 数据卷操作
		volumeUrls, err := volumeUrlExtract(volume)
		if err != nil {
    
    
			log.Log.Warn(err)
			return
		}
		// 挂载Volume
		MountVolume(mntURL, volumeUrls)
		log.Log.Infof("success establish volume : %s", strings.Join(volumeUrls, ""))
	}
}

数据卷的格式如下:<宿主机目录>:<容器目录>校验函数如下:

func volumeUrlExtract(volume string) ([]string, error)  {
    
    
	volumeAry := strings.Split(volume, ":")
	if len(volumeAry) != 2 || volumeAry[0] == "" || volumeAry[1] == "" {
    
    
		return nil, fmt.Errorf(" Invalid volume string!")
	}
	return volumeAry, nil
}

再看MountVolume函数:

总体上的思路就是三步走:

  1. 根据数据卷的宿主机目录,创建宿主机文件目录(如果没有的话)
  2. 根据数据卷的容器目录,在容器目录中创建挂载点目录
  3. 将宿主机的文件目录挂载到容器挂载点

这样我们在进入容器之前就将这样的挂载设置好了,自然在容器启动后可以看到宿主机数据卷中的数据

func MountVolume(mntUrl string, volumeUrl []string)  {
    
    
	// 1. 创建宿主机文件目录
	parentUrl, containerUrl := volumeUrl[0], filepath.Join(mntUrl, volumeUrl[1])
	if has, err := dirOrFileExist(parentUrl); err == nil && !has {
    
    
		// 当宿主机没有此文件时,创建文件夹
		if err := os.Mkdir(parentUrl, 0777); err != nil {
    
    
			log.LogErrorFrom("MountVolume", "Mkdir", err)
			return
		}
	}
	// 2. 在容器目录中创建挂载点目录
	if has, err := dirOrFileExist(containerUrl); err == nil && has {
    
    
		// 如果有此文件夹,则先删除
		if err := os.RemoveAll(containerUrl); err != nil {
    
    
			log.LogErrorFrom("MountVolume", "RemoveAll", err)
			return
		}
	}
	// 容器中创建文件夹
	if err := os.Mkdir(containerUrl, 0777); err != nil {
    
    
		log.LogErrorFrom("MountVolume", "Mkdir", err)
		return
	}
	// 3. 将宿主机的文件目录挂载到容器挂载点
	dirs := "dirs=" + parentUrl
	cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "myDockerVolume", containerUrl)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
    
    
		log.Log.Errorf("Mount volume failed. %v", err)
	}
}

那么对应删除的时候的逻辑就如函数DeleteWorkSpace

func DeleteWorkSpace(rootURL, mntURL, volume string) {
    
    
	if volume != "" {
    
    
		// 当volume不为空的时候,
		volumeUrls, err := volumeUrlExtract(volume);
		if err != nil {
    
    
			// 解析错误
			log.Log.Warn(err)
			DeleteMountPoint(mntURL)
		}else {
    
    
			DeleteMountPointWithVolume(mntURL, volumeUrls)
		}
	}else {
    
    
		DeleteMountPoint(mntURL)
	}
	DeleteWriteLayer(rootURL)
}

DeleteMountPointWithVolume其实只是比DeleteMountPoint多做了一步,那就是将容器中的volume目录先给取消挂载,然后就是将整个mnt取消挂载随后删除的过程了。

func DeleteMountPointWithVolume(mntURL string, volumeUrls []string)  {
    
    
	// 其实相比而言就是多了一步卸载volume的挂载点
	containerUrl := filepath.Join(mntURL, volumeUrls[1])
	cmd := exec.Command("umount", containerUrl)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
    
    
		log.LogErrorFrom("deleteMountPointWithVolume", "umount", err)
	}
	DeleteMountPoint(mntURL)
}

2. 工作流程

image-20211113164150216

3. 测试

启动容器:

$ ./mydocker run -t -v ./volume:/containerVolume sh
$ ls

bin              dev              home             root             tmp              var
containerVolume  etc              proc             sys              usr

可以看到映射的数据卷存在.

在另一个终端中查看宿主机:

$ ls
busybox  busybox.tar  config.json  mnt  mydocker  volume  writeLayer

宿主机中也创建了volume文件夹

我们在容器中向数据卷映射文件夹中创建一个文件:

$ cat > /containerVolume/test1.txt << EOF
> hahahaha11111
> EOF
$ cat containerVolume/test1.txt 

hahahaha11111

cat > /containerVolume/test2.txt << EOF
> hahahaha2222
> EOF

然后我们在宿主机中的volume中就可以看到这个文件:

$ cat volume/test1.txt
hahahaha11111

同样的在宿主机创建文件test2.txt在容器中也可看到

$ cat > volume/test2.txt << EOF
> hahahah222222
> EOF
$ cat volume/test2.txt 

hahahah222222
# 容器中
$ cat containerVolume/test2.txt 
hahahah222222

当关闭容器后宿主机的volume文件夹中的文件也不会被删除

# 容器中
$ exit
# 宿主机
$ ls
busybox  busybox.tar  config.json  mydocker  volume
$ ls volume/
test1.txt  test2.txt

可以再次启动容器查看之前的数据卷文件是否存在

2Ecc3n

至此,我们实现了volume数据卷的基本功能。

6. Version7-实现简单镜像打包

我们期望有时候可以保存当前容器为一个镜像,所以本节会在上一节删除读写层之间把容器的运行状态内容存储起来当作镜像保存下来。

本节代码获取:

$ git clone https://github.com/xwjahahahaha/myDocker.git
$ git checkout 8daca

结构变化:

.
├── README.md
├── cgroups
│   ├── cgroup_manager.go
│   └── subsystems
│       ├── cpu.go
│       ├── cpuset.go
│       ├── memory.go
│       ├── subsystem.go
│       └── utils.go
├── cmd
│   ├── commands.go
│   ├── init.go
│   └── root.go
├── container
│   ├── commit.go
│   ├── initContainer.go
│   ├── process.go
│   ├── run.go
│   ├── utils.go
│   └── workspace.go
├── go.mod
├── go.sum
├── log
│   ├── init.go
│   └── log.go
├── main.go

1. 实现

本节很简单,就是实现一个tar打包的命令。首先添加commit命令

var commitCommand = &cobra.Command{
    
    
	Use:   "commit [image_name]",
	Short: "commit a container into image",
	Long:  "commit a container into image",
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
    
    
		container.CommitContainer(args[0])
	},
}

新创建一个container/commit.go文件

// CommitContainer
// @Description: 打包一个容器
// @param imageName
func CommitContainer(imageName string) {
    
    
	mntUrl := "./mnt"
	imageTarUrl := "./" + imageName + ".tar"
	if _, err := exec.Command("tar", "-czf", imageTarUrl, "-C", mntUrl, ".").CombinedOutput(); err != nil {
    
    
		log.LogErrorFrom("CommitContainer", "tar", err)
	}
}

2. 使用流程

image-20211113170630093

3. 测试

先启动一个容器, 并创建一个文件

$ ./mydocker run -t sh
$ cat > test.txt << EOF
> 123
> EOF
$ cat test.txt 
123

然后不要关闭容器,在另一个终端打包镜像

$ ./mydocker commit mybusybox
$ ls 

busybox  busybox.tar  config.json  mnt  mybusybox.tar  mydocker  volume  writeLayer

可以看到打包好了mybusybox.tar,解压可以看到内容与容器一致


觉得不错的话,请点赞关注呦~~你的关注就是博主的动力
关注公众号,查看更多go开发、密码学和区块链科研内容:
2DrbmZ

猜你喜欢

转载自blog.csdn.net/weixin_43988498/article/details/121307202