change-rootfs-by-pivot-root.png

本文为从零开始写 Docker 系列第四篇,在mydocker run 基础上使用 pivotRoot 系统调用切换 rootfs 实现容器和宿主机之间的文件系统隔离。


完整代码见:https://github.com/lixd/mydocker

欢迎 Star


推荐阅读以下文章对 docker 基本实现有一个大致认识:


开发环境如下:

root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.2 LTS
Release: 20.04
Codename: focal
root@mydocker:~# uname -r
5.4.0-74-generic

注意:需要使用 root 用户


如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。

搜索公众号【探索云原生】即可订阅


1. 概述

前面几节中,我们通过 NamespaceCgroups 技术创建了一个简单的容器,实现了视图隔离和资源限制。

但是大家应该可以发现,容器内的目录还是当前运行程序的宿主机目录,而且如果运行一下 mount 命令可以看到继承自父进程的所有挂载点。

这貌似和平常使用的容器表现不同

因为这里缺少了镜像这么一个重要的特性。

Docker 镜像可以说是一项伟大的创举,它使得容器传递和迁移更加简单,那么这一节会做一个简单的镜像,让容器跑在有镜像的环境中。

即:本章会为我们切换容器的 rootfs,以实现文件系统的隔离

2. 准备 rootfs

Docker 镜像包含了文件系统,所以可以直接运行,我们这里就先弄个简单的,直接将某个镜像中的所有内容作为我们的 rootfs 进行挂载。

即:先在宿主机上某一个目录上准备一个精简的文件系统,然后容器运行时挂载这个目录作为 rootfs

首先使用一个最精简的镜像 busybox 来作为我们的文件系统。

busybox 是一个集合了非常多 UNIX 工具的箱子,它可以提供非常多在 UNIX 环境下经常使用的命令,可以说 busybox 提供了一个非常完整而且小巧的系统。

因此我们先使用它来作为第一个容器内运行的文件系统。

获得 busybox 文件系统的 rootfs 很简单,可以使用 docker export 将一个镜像打成一个 tar包,并解压,解压目录即可作为文件系统使用

首先拉取镜像

docker pull busybox

然后使用该镜像启动一个容器,并用 export 命令将其导出成一个 tar 包

# 执行一个交互式命令,让容器能一直后台运行
docker run -d busybox top
# 拿到刚创建的容器的 Id
containerId=$(docker ps --filter "ancestor=busybox:latest"|grep -v IMAGE|awk '{print $1}')
echo "containerId" $containerId
# export 从容器导出
docker export -o busybox.tar $containerId

最后将 tar 包解压

mkdir busybox
tar -xvf busybox.tar -C busybox/

这样就得到了 busybox 文件系统的 rootfs ,可以把这个作为我们的文件系统使用。

这里的 rootfs 指解压得到的 busybox 目录

busybox 中的内容大概是这样的:

[root@docker ~]# ls -l busybox
total 16
drwxr-xr-x 2 root      root      12288 Dec 29  2021 bin
drwxr-xr-x 4 root      root         43 Jan 12 03:17 dev
drwxr-xr-x 3 root      root        139 Jan 12 03:17 etc
drwxr-xr-x 2 nfsnobody nfsnobody     6 Dec 29  2021 home
drwxr-xr-x 2 root      root          6 Jan 12 03:17 proc
drwx------ 2 root      root          6 Dec 29  2021 root
drwxr-xr-x 2 root      root          6 Jan 12 03:17 sys
drwxrwxrwt 2 root      root          6 Dec 29  2021 tmp
drwxr-xr-x 3 root      root         18 Dec 29  2021 usr
drwxr-xr-x 4 root      root         30 Dec 29  2021 var

可以看到,内容和一个完整的文件系统基本是一模一样的。

注意:rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核

在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。

3. 挂载 rootfs

把之前的 busybox rootfs 移动到/root/busybox 目录下备用。

实现原理

使用pivot_root 系统调用来切换整个系统的 rootfs,配合上 /root/busybox 来实现一个类似镜像的功能。

pivot_root 是一个系统调用,主要功能是去改变当前的 root 文件系统

原型如下:

#include <unistd.h>

int pivot_root(const char *new_root, const char *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 挂载点
*/
func setUpMount() {
 pwd, err := os.Getwd()
 if err != nil {
  log.Errorf("Get current location error %v", err)
  return
 }
 log.Infof("Current location is %s", pwd)

 // systemd 加入linux之后, mount namespace 就变成 shared by default, 所以你必须显示
 // 声明你要这个新的mount namespace独立。
 // 如果不先做 private mount,会导致挂载事件外泄,后续执行 pivotRoot 会出现 invalid argument 错误
 err = syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, "")

 err = pivotRoot(pwd)
 if err != nil {
  log.Errorf("pivotRoot failed,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")
}

func pivotRoot(root string) error {
 /**
   NOTE:PivotRoot调用有限制,newRoot和oldRoot不能在同一个文件系统下。
   因此,为了使当前root的老root和新root不在同一个文件系统下,这里把root重新mount了一次。
   bind mount是把相同的内容换了一个挂载点的挂载方法
 */
 if err := syscall.Mount(root, root, "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
  return errors.Wrap(err, "mount rootfs to itself")
 }
 // 创建 rootfs/.pivot_root 目录用于存储 old_root
 pivotDir := filepath.Join(root, ".pivot_root")
 if err := os.Mkdir(pivotDir, 0777); err != nil {
  return err
 }
 // 执行pivot_root调用,将系统rootfs切换到新的rootfs,
 // PivotRoot调用会把 old_root挂载到pivotDir,也就是rootfs/.pivot_root,挂载点现在依然可以在mount命令中看到
 if err := syscall.PivotRoot(root, pivotDir); err != nil {
  return errors.WithMessagef(err, "pivotRoot failed,new_root:%v old_put:%v", root, pivotDir)
 }
 // 修改当前的工作目录到根目录
 if err := syscall.Chdir("/"); err != nil {
  return errors.WithMessage(err, "chdir to / failed")
 }

 // 最后再把old_root umount了,即 umount rootfs/.pivot_root
 // 由于当前已经是在 rootfs 下了,就不能再用上面的rootfs/.pivot_root这个路径了,现在直接用/.pivot_root这个路径即可
 pivotDir = filepath.Join("/", ".pivot_root")
 if err := syscall.Unmount(pivotDir, syscall.MNT_DETACH); err != nil {
  return errors.WithMessage(err, "unmount pivot_root dir")
 }
 // 删除临时文件夹
 return os.Remove(pivotDir)
}

然后再 build cmd 的时候指定:

func NewParentProcess(tty bool) (*exec.Cmd, *os.File) {
    cmd := exec.Command("/proc/self/exe", "init")
    // .. 省略其他代码
    // 指定 cmd 的工作目录为我们前面准备好的用于存放busybox rootfs的目录
    cmd.Dir = "/root/busybox"
    return cmd, writePipe
}

到此这一小节就完成了,测试一下。

4. 测试

测试比较简单,只需要执行 ls 命令,即可根据输出内容确定文件系统是否切换了。

root@mydocker:~/feat-rootfs/mydocker# go build .
root@mydocker:~/feat-rootfs/mydocker# ./mydocker run -it  /bin/ls
{"level":"info","msg":"resConf:\u0026{ 0  }","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"command all is /bin/ls","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"init come on","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"Current location is /root/busybox","time":"2024-01-12T16:19:32+08:00"}
{"level":"info","msg":"Find path /bin/ls","time":"2024-01-12T16:19:32+08:00"}
bin   dev   etc   home  proc  root  sys   tmp   usr   var

可以看到,现在打印出来的就是/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,具体看本文第二节。

# 克隆代码
git clone -b feat-rootfs https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依赖并编译
go mod tidy
go build .
# 测试 查看文件系统是否变化
./mydocker run -it  /bin/ls

从零开始写 Docker(四)---使用 pivotRoot 切换 rootfs 实现文件系统隔离的更多相关文章

  1. 从零开始写一个前端脚手架四、初始化进程提示(chalk)

    我们之前说过bin里面的index.js文件是作为入口文件存在的.实际上的初始化内容在.action里面操作的,为了方便管理,我们把实际操作的代码抽出来放一块儿管理 创建指令文件 在根目录创建一个co ...

  2. 从零开始学习 Docker

      这篇文章是我学习 Docker 的记录,大部分内容摘抄自 <<Docker - 从入门到实践>> 一书,并非本人原创.学习过程中整理成适合我自己的笔记,其中也包含了我自己的 ...

  3. 从零开始学习docker之在docker中搭建redis(集群)

    docker搭建redis集群 docker-compose是以多容器的方式启动,非常适合用来启动集群 一.环境准备 云环境:CentOS 7.6 64位 二.安装docker-compose #需要 ...

  4. 从零开始学习jQuery (四) 使用jQuery操作元素的属性与样式

    本系列文章导航 从零开始学习jQuery (四) 使用jQuery操作元素的属性与样式 一.摘要 本篇文章讲解如何使用jQuery获取和操作元素的属性和CSS样式. 其中DOM属性和元素属性的区分值得 ...

  5. 深入浅出React Native 3: 从零开始写一个Hello World

    这是深入浅出React Native的第三篇文章. 1. 环境配置 2. 我的第一个应用 将index.ios.js中的代码全部删掉,为什么要删掉呢?因为我们准备从零开始写一个应用~学习技术最好的方式 ...

  6. 从零开始写一个武侠冒险游戏-7-用GPU提升性能(2)

    从零开始写一个武侠冒险游戏-7-用GPU提升性能(2) ----把地图处理放在GPU上 作者:FreeBlues 修订记录 2016.06.21 初稿完成. 2016.08.06 增加对 XCode ...

  7. 从零开始写一个武侠冒险游戏-6-用GPU提升性能(1)

    从零开始写一个武侠冒险游戏-6-用GPU提升性能(1) ----把帧动画的实现放在GPU上 作者:FreeBlues 修订记录 2016.06.19 初稿完成. 2016.08.05 增加对 XCod ...

  8. 【Java EE】从零开始写项目【总结】

    从零开发项目概述 最近这一直在复习数据结构和算法,也就是前面发出去的排序算法八大基础排序总结,Java实现单向链表,栈和队列就是这么简单,十道简单算法题等等... 被虐得不要不要的,即使是非常简单有时 ...

  9. 一起学习造轮子(一):从零开始写一个符合Promises/A+规范的promise

    本文是一起学习造轮子系列的第一篇,本篇我们将从零开始写一个符合Promises/A+规范的promise,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Pr ...

  10. Docker学习第四天(Docker四种网络模式)

    Docker四种网络模式 实现原理 Docker使用Linux桥接(参考<Linux虚拟网络技术>),在宿主机虚拟一个Docker容器网桥(docker0),Docker启动一个容器时会根 ...

随机推荐

  1. 【K哥爬虫普法】网盘用的好,“艳照门”跑不了

    我国目前并未出台专门针对网络爬虫技术的法律规范,但在司法实践中,相关判决已屡见不鲜,K哥特设了"K哥爬虫普法"专栏,本栏目通过对真实案例的分析,旨在提高广大爬虫工程师的法律意识,知 ...

  2. [置顶] k8s,docker,微服务,监控

    综合 第一篇:k8s服务A内部调用服务B的方式 第二篇:go-zero grpc 第一篇:grpc,protobuf安装 第二篇:grpc签发证书 第三篇:golang-grpc 第四篇:python ...

  3. 6.8 Windows驱动开发:内核枚举Registry注册表回调

    在笔者上一篇文章<内核枚举LoadImage映像回调>中LyShark教大家实现了枚举系统回调中的LoadImage通知消息,本章将实现对Registry注册表通知消息的枚举,与LoadI ...

  4. 8.3 C++ 定义并使用类

    C/C++语言是一种通用的编程语言,具有高效.灵活和可移植等特点.C语言主要用于系统编程,如操作系统.编译器.数据库等:C语言是C语言的扩展,增加了面向对象编程的特性,适用于大型软件系统.图形用户界面 ...

  5. linux(centos) 下搭建svn服务器

     1. 使用yum安装svn yum -y install subversion 安装完成之后,验证安装结果 此命令会全自动安装svn服务器相关服务和依赖,安装完成会自动停止命令运行 若需查看svn安 ...

  6. MASA学习和总结

    一.MASA概述 MASA是温州数闪科技推出的开源产品,目前有三个产品线,分别是MASA Stack,MASA Framework,MASA Blazor. MASA Stack:是一个开源.企业级. ...

  7. 资深工程师 VSCode C/C++ 必备开发插件

    1.前言 俗话说"工欲善其事,必先利其器",下面介绍几个VSCode提高开发效率的插件,资深工程师必备. 2.基础插件 2.1.Chinese(Simplified) vscode ...

  8. 23.1 SEH之终止处理--《Windows核心编程》结构化异常处理

    (structured exception handing)SEH 包含终止处理和异常处理.本章讨论终止处理. 一.终止处理 终止处理程序确保不管一个代码块(被保护代码)如何退出的,另一个代码块(终止 ...

  9. 《ASP.NET Core 与 RESTful API 开发实战》-- (第8章)-- 读书笔记(中)

    第 8 章 认证和安全 8.2 ASP.NET Core Identity Identity 是 ASP.NET Core 中提供的对用户和角色等信息进行存储与管理的系统 Identity 由3层构成 ...

  10. 如何使用graalvm为带有反射功能的java代码生成native image

    译自Configure Native Image with the Tracing Agent graal官方文档 , 以下所有命令需要在linux环境下操作,graalvm也支持windows. 要 ...