miniDocker(2) - 文件系统

 

前言

在上一篇文章中,我们创建了新的namespace,并在cgroup hierachy上创建对应的节点。同时利用exec将指定进程放置到了新建的namespace,替换成为了pid为1的上帝进程,由此形成了一个容器

之前我们介绍了docker是如何使用aufsoverlay来组织镜像: Docker是如何生成/保存镜像的?。但此时我们并未在文件系统上做额外处理,所以容器中的进程仍然可以访问到完整的宿主机文件视图,这显然不是我们期望的。

所以本文就来解答这个问题: 我该如何修改容器的文件视图,让容器内的进程看起来就像独占了一整个操作系统?

核心点

容器的文件视图组织

在项目的/config/global_config.go文件中,我们可以看到miniDocker对容器文件系统路径的相关定义:

const (
    PathMnt       = "/var/lib/mdocker/overlay2/mnt"
    PathReadWrite = "/var/lib/mdocker/overlay2/rw"
    PathImage     = "/var/lib/mdocker/overlay2/image"
    PathWorkDir   = "/var/lib/mdocker/overlay2/workdir"
)
路径 说明
PathMnt 容器根目录挂载点,即overlay2的联合挂载点
PathReadWrite 容器的读写层, 会在容器销毁时删除
PathImage 镜像的存储路径(如busybox)解压路径
PathWorkDir 容器的overlay2 workdir目录

我们以创建一个busybox容器为例:

  • 利用以下命令生成mnt挂载点:
    mount -t overlay overlay \ 
    > lowerdir=<PathImage>/busybox,\
    > upperdir=<PathRW>/containerName,\
    > workdir=<PathWorkDir>/containerName <PathMnt>/containerName
    

pivot-root

现在我们得到了一个”看起来像”是操作系统的目录:

/ # ls
bin dev etc home proc root sys tmp usr var

那如何让容器的根目录挂载到overlay2生成的文件视图上呢?答案就是pivot_root这个黑魔法。

我们先来看看pivot_rootman page介绍,pivot_root的command使用方法为:

pivot_root new_root put_old

当执行上面的命令后,会发生这么两件事:

  1. 当前进程的new_root会被设置成/根目录。
  2. 原根目录会被移动到put_old目录下。

当然,这是有限制条件的:

  1. new_root和put_old必须是目录,且new_root必须是mount挂载点并且不能是当前根目录。
  2. put_old必须是new_root或者其子目录。

是不是感觉有点晕?那我们换个说法,pivot这个单词表示枢轴、转动,按照上面的描述,name各个路径的关系如下:

图1: pivot_root示意图(1)

这个时候,我们把old_rootnew_root之间的路径视为一个pivot:

图2: pivot_root示意图(2)

让pivot旋转以下,是不是就像描述的那样,new_root旋转到了根目录的位置上。这样容器的overlay2挂载点就成为了容器namespace里的root dir。

但是pivot_root并不会改动当前调用进程的workdir,所以我们需要进行手动修改: chdir /

构建新的文件系统

在利用pivot_root修改了根目录的挂载点后,剩余的事情就简单很多了:

  • 卸载old_root这个挂载点,使用detach的方式移除原文件系统的挂载。由于当前系统必然依赖原文件系统,所以使用detach懒卸载的方式移除调原文件系统。
  • 重新挂载/proc/dev等系统级目录。其中/dev使用tmpfs,这是一个基于内存的文件系统。

代码实现讲解

总体流程

图3: mdocker文件系统创建流程

创建overlay2文件系统

overlay2文件系统的创建在mdocker run进程中执行:

  • 根据imageName和用户volume参数,生成overlay2文件系统mnt point。
  • 创建mdocker init进行,并将其工作目录设置为刚才创建的overlay2文件系统mnt point。

调整容器(namespace)的mount结构

本节的主体逻辑在/container/container_init/mount_init.go中,这个逻辑在mdocker init命令的进程中完成。

逻辑调用链如下:

// /cmd/init_cmd.go
func initCmdAction(ctx *cli.Context) error {
	return container_init.ContainerProcessInit()
}

// /container/container_init/container_process.go
func ContainerProcessInit() error {
	// ...

	// 初始化容器mount
	if err := mountInit(); err != nil {
		return fmt.Errorf("mount init failed: %v", err)
	}

	// ...

	return nil
}

// /container/container_init/mount_init.go
func mountInit() error {
	// 改变当前Namespace的Mount传播模式
	err := syscall.Mount("", "/", "", uintptr(config.MountFlagsPrivate), "")
	if err != nil {
		return err
	}

	// ...

	// 调用pivot切换rootfs
	err = pivotRoot(pwd)
	if err != nil {
		return err
	}

	// ...

	return nil
}

值得注意的是,在调整容器的mount结构前,需要先将namespace下的mount传播模式改为private,否则这些操作会被传播到外部。

结束语

本讲我们降到如何为容器生成overlay2文件系统,并且其作为容器的文件根目录。这样一来,容器就脱离了宿主机的文件系统,在自己看来就是独占了一个完整的存储设备。