前言
就我目前的对容器的了解, 使用namespace
技术实现隔离, 使用cgroups
技术实现资源限制. 但是具体是如何实现却从未深究过.
闲来无事, 挑其中的Mount Namespace
来康康, 容器是如何实现目录隔离的.
目录隔离
在耗子叔的这篇文章中对此技术进行了介绍.
在c
函数库中, 可通过如下方式实现目录的隔离:
int container_main(void* arg)
{
// 调用 mount 方法, 触发目录隔离机制. 将根目录替换为 /root/tmp
mount("/root/tmp", "/", "tmpfs", 0, "");
// dosomething
return 1;
}
void main(){
// 调用 clone 创建子进程
// 传递 CLONE_NEWNS 标识, 标明需要创建目录隔离
clone(container_function, stack, CLONE_NEWNS | SIGCHLD , NULL)
}
如果想在命令行中测试目录隔离, 也可以如此操作:
# 创建用于挂载的临时目录
mkdir -p mount/bin
mkdir -p mount/lib64
mkdir -p mount/lib
# 将执行文件放进去
cp /bin/ls mount/bin
# 将依赖的链接库放入 (依赖库可通过命令 ldd /bin/ls 查看)
cp /lib/x86_64-linux-gnu/libselinux.so.1 mount/lib
cp /lib/x86_64-linux-gnu/libc.so.6 mount/lib/
cp /lib/x86_64-linux-gnu/libpcre.so.3 mount/lib/
cp /lib/x86_64-linux-gnu/libdl.so.2 mount/lib/
cp /lib64/ld-linux-x86-64.so.2 mount/lib64/
cp /lib/x86_64-linux-gnu/libpthread.so.0 mount/lib/
# 替换运行进程的根目录
# 执行此命令时, chroot 命令会将 ls 命令的运行根目录替换为 ./mount 目录
# 可以尝试着执行 /bin/ls 命令查看
chroot ./mount /bin/ls
至此, 虽然举的例子很简单, 但依然足够我们理解目录隔离了. 容器启动后, 会将整个进程的根目录换掉, 甚至直接挂载整个操作系统的ISO. 这也就解释了为什么容器只是一个运行在宿主机上的进程, 却可以表现为不同的操作系统.
在docker
中, 容器和镜像的文件系统目录, 保存在宿主机的/var/lib/docker/overlay2
.
你可以通过命令docker inspect <container_id>
来查看容器的层级关系.
至于docker
是如何将镜像的多层进行聚合, 最终展现给容器的, 简单说是通过UnionFS 技术, 将多个目录挂载到同一个目录下, 且可以设置优先级. 因为使用了union mount
技术, 因此在overlay2
中是看不到容器的完整文件系统的. 它实际上并没有在磁盘上创建一个包含所有层文件的单一目录。相反,当你查看容器的文件系统时,Docker 和 Linux 内核会动态地将所有层组合在一起,使它们看起来像一个单一的文件系统。因此,即使你可以在 /var/lib/docker/overlay2
下找到每个层的文件,你也不能直接在这里找到一个包含容器完整文件系统的单一目录.
虽然在overlay2
目录下没有容器的完整文件系统, 但其实在宿主机的/proc/<pid>/root
目录中可以看到, 不过前面也说了, 完整的文件系统是动态组合的, 因此/proc/<pid>/root
目录也只是一个软连接. 至于容器的pid
, 可以通过命令docker inspect --format '{{ .State.Pid }}' <container_id>
获取.
exec
目录隔离使得容器启动后文件系统自成一派. 那么执行exec
命令进入容器时, 又是如何做到的呢?
那必然是系统已经支持的啦. 在/proc/<pid>/ns
目录下, 记录了命令空间隔离的数据.
其中的mnt
记录的就是目录隔离. 可以使用命令行nsenter -t <pid> -m
来进入到特定进程的目录命名空间. (-m 表示要进入 mount namespace). 命令执行后, 再执行ls
命令, 就会发现已经进入到容器的文件系统中了.
在c
的函数库中, 则使用setns
函数来实现此功能. 函数具体使用不做赘述.
至此, 容器是如何做到目录隔离的, 有了一个大致模糊的印象. 收工.