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

change-rootfs-by-pivot-root.png

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


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


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

  • 核心原理:深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs
  • 基于 namespace 的视图隔离:探索 Linux Namespace:Docker 隔离的神奇背后
  • 基于 cgroups 的资源限制
    • 初探 Linux Cgroups:资源控制的奇妙世界
    • 深入剖析 Linux Cgroups 子系统:资源精细管理
    • Docker 与 Linux Cgroups:资源隔离的魔法之旅
  • 基于 overlayfs 的文件系统:Docker 魔法解密:探索 UnionFS 与 OverlayFS
  • 基于 veth pair、bridge、iptables 等等技术的 Docker 网络:揭秘 Docker 网络:手动实现 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 /procdefaultMountFlags := 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_rootpivotDir := 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/721158.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

javascript作用域编译浅析

作用域思维导图 1&#xff1a;编译原理 分词/词法分析 如果词法单元生成器在判断a是一个独立的词法单元还是其他词法单元的一部分时&#xff0c;调用的是有状态的解析规则&#xff0c;那么这个过程就被称为词法分析。 解析/语法分析 由词法单元流转换成一个由元素逐级嵌套所组…

期货开户交易软件如何下单?

一、手机和电脑使用的交易软件 目前期货市场常用的软件有文华、博弈、快期、易盛、同花顺等&#xff0c;这5款电脑软件对应的手机端是文华随身行、博弈手机版、快期小Q、易盛易星、同花顺期货通&#xff0c;这些常用软件大部分期货公司都是支持的。 二、交易软件如何下单 软…

C打印内存16进制

下面是一段C代码打印16进制 void print_hex(const char *msg, void *addr, int len) {uint8_t *p (uint8_t *)addr;printf("%s ,stat:%0x8, len:%d\n", msg, addr, len);for (int i 0; i < len / 16; i) {printf("0x%08x: ", p i * 16);for (int j …

【音视频开发好书推荐】《RTC程序设计:实时音视频权威指南》

1、WebRTC概述 WebRTC&#xff08;Web Real-Time Communication&#xff09;是一个由Google发起的实时音视频通讯C开源库&#xff0c;其提供了音视频采集、编码、网络传输&#xff0c;解码显示等一整套音视频解决方案&#xff0c;我们可以通过该开源库快速地构建出一个音视频通…

【小黑嵌入式系统第十七课】结课总结(一)——硬件部分(系统总线处理器外设通信)

上一课&#xff1a; 【小黑嵌入式系统第十六课】PSoC 5LP第三个实验——μC/OS-III 综合实验 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff1a;人工智能 文章目录 一、基础知识…

2024新版SonarQube+JenKins+Github联动代码扫描(1)-JenKins安装与配置

文章目录 前言一、官网下载二、访问ip:port三、安装推荐插件-自动下载四、创建用户名密码五、安装SonarQube插件六、配置全局工具总结 前言 Sonar是一个半开源的静态代码扫描工具&#xff0c;试用过一次觉得功能还算可以&#xff0c;所以记录一下SonarQube扫描的用法以及在中大…

IO接口 2月5日学习笔记

1.fgetc 用于从文件中读取一个字符&#xff0c;fgetc 函数每次调用将会返回当前文件指针所指向的字符&#xff0c;并将文件指针指向下一个字符。 int fgetc(FILE *stream); 功能: 从流中读取下一个字符 参数: stream:文件流指针 返回值: …

5分钟速成渐变色css

色彩的分支——渐变色定义&#xff1a;按照一定规律做阶段性变化的色彩&#xff08;抽象&#xff01;&#xff01;&#xff01;&#xff09; 我们可以将图片分为两块 以中心线为参考&#xff0c;再来看渐变色的定义&#xff1a;按照一定规律做阶段性变化的色彩 既然是按一定的…

使用Android Native Hook技术解决VLC播放器闪退的问题

文章目录 1.概述2.问题描述3.问题分析4.问题解决5.总结 1.概述 在做公司的一个TOB的需求时&#xff0c;发现调起Unity提供的3D播放器播放网络在线视频时闪退了&#xff0c;然后就拉着相关部门的人一起分析问题&#xff0c;最后定位到是VLC里面用到的系统日志打印函数在部分的系…

动态代理IP的并发处理技巧

目录 前言 一、什么是动态代理IP&#xff1f; 二、动态代理IP的并发处理技巧 1. 获取代理IP 2. 动态生成代理对象 3. 并发处理 总结 前言 在进行网络爬虫开发时&#xff0c;经常会遇到限制IP访问频率的情况。为了突破这个限制&#xff0c;我们可以通过使用代理IP来实现…

华为配置基于VLAN限速示例

华为配置基于VLAN限速示例 组网图形 图1 流量监管配置组网图 表1 Switch为上行流量提供的QoS保障 流量类型 CIR(kbps) PIR(kbps) DSCP优先级 语音 2000 10000 46 视频 4000 10000 30 数据 4000 10000 14 ^^^ 流分类简介配置注意事项组网需求配置思路操作步…

C#与欧姆龙PLC实现CIP通讯

参考文档&#xff1a; 欧姆龙PLC使用-CSDN博客 CIP通讯介绍&#xff08;欧姆龙PLC&#xff09;-CSDN博客 使用NuGet添加引用&#xff1a;CIPCompolet 基础参考我的CIP协议介绍&#xff0c;默认TCP端口为&#xff1a;44818 类NXCompolet 类的功能可以在安装PLC开发软件后帮…

vue svelte solid 虚拟滚动性能对比

前言 由于svelte solid 两大无虚拟DOM框架&#xff0c;由于其性能好&#xff0c;在前端越来越有影响力。 因此本次想要验证&#xff0c;这三个框架关于实现表格虚拟滚动的性能。 比较版本 vue3.4.21svelte4.2.12solid-js1.8.15 比较代码 这里使用了我的 stk-table-vue(np…

web坦克大战小游戏

H5小游戏源码、JS开发网页小游戏开源源码大合集。无需运行环境,解压后浏览器直接打开。有需要的订阅后,私信本人,发源码,含60+小游戏源码。如五子棋、象棋、植物大战僵尸、贪吃蛇、飞机大战、坦克大战、开心消消乐、扑鱼达人、扫雷、打地鼠、斗地主等等。 <!DOCTYPE htm…

【图论】Dijkstra 算法求最短路 - 构建邻接矩阵(带权无向图)

文章目录 例题&#xff1a;到达目的地的方案数题目描述代码与解题思路构建带权无向图的邻接矩阵 例题&#xff1a;到达目的地的方案数 题目链接&#xff1a;1976. 到达目的地的方案数 题目描述 代码与解题思路 func countPaths(n int, roads [][]int) int {g : make([][]int…

数据库和缓存如何保持一致性

目录 前言 更新数据库更新缓存&#xff1a; 1.在更新缓存前先加一个分布式锁 2.在更新完缓存时&#xff0c;给缓存加上较短的过期时间 Cache Aside策略 1.先删除缓存&#xff0c;再更新数据库 延迟双删 2.先更新数据库&#xff0c;再删除缓存 保证两个操作都能执行成功…

【译】WordPress Bricks主题安全漏洞曝光,25,000个安装受影响

WordPress的Bricks主题存在一个严重的安全漏洞,恶意威胁行为者正在积极利用该漏洞在易受攻击的安装上运行任意PHP代码。 该漏洞被跟踪为CVE-2024-25600(CVSS评分:9.8),使未经身份验证的攻击者能够实现远程代码执行。它影响Bricks的所有版本,包括1.9.6版本及更早版本。 该…

线程变量ThreadLocal用于解决多线程并发时访问共享变量的问题。

ThreadLocal介绍 ThreadLocal叫做线程变量&#xff0c;用于解决多线程并发时访问共享变量的问题。意思是ThreadLocal中填充的变量属于当前线程&#xff0c;该变量对其他线程而言是隔离的&#xff0c;也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建…

如何用生成式人工智能准备和制作吸引人的美食视频?

YouTube是一个全球性的视频分享平台&#xff0c;上面充满了各式各样的内容&#xff0c;其中美食内容因其视觉和味觉上的双重吸引而备受欢迎。作为一个想要进入这个领域的创作者&#xff0c;你需要知道如何准备和制作吸引人的美食视频。以下是一些基本步骤和技巧&#xff1a; 选…

UE4 Niagara 关卡3.4官方案例解析

Texture sampling is only supported on the GPU at the moment.(纹理采样目前仅在GPU上受支持) 效果&#xff1a;textures can be referenced within GPU particle systems。this demo maps a texture to a grid of particles&#xff08;纹理可以在GPU粒子系统中被引用这个演…