前言
在上一篇文章中,我们创建了新的namespace,并在cgroup hierachy上创建对应的节点。同时利用exec
将指定进程放置到了新建的namespace,替换成为了pid为1的上帝进程
,由此形成了一个容器
。
之前我们介绍了docker是如何使用aufs
和overlay
来组织镜像: 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_root
的man page介绍,pivot_root的command使用方法为:
pivot_root new_root put_old
当执行上面的命令后,会发生这么两件事:
- 当前进程的
new_root
会被设置成/
根目录。 - 原根目录会被移动到
put_old
目录下。
当然,这是有限制条件的:
- new_root和put_old必须是目录,且new_root必须是mount挂载点并且不能是当前根目录。
- put_old必须是new_root或者其子目录。
是不是感觉有点晕?那我们换个说法,pivot
这个单词表示枢轴、转动
,按照上面的描述,name各个路径的关系如下:
这个时候,我们把old_root
和new_root
之间的路径视为一个pivot
:
让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文件系统,并且其作为容器的文件根目录。这样一来,容器就脱离了宿主机的文件系统,在自己看来就是独占
了一个完整的存储设备。