(3)500行代码代码手写docker-将rootfs设置为只读镜像

本系列教程主要是为了弄清楚容器化的原理,纸上得来终觉浅,绝知此事要躬行,理论始终不及动手实践来的深刻,所以这个系列会用go语言实现一个类似docker的容器化功能,最终能够容器化的运行一个进程。

本章的源码已经上传到github,地址如下:

https://github.com/HobbyBear/tinydocker/tree/chapter3

前文提到,如果仅仅将ubuntu-base-16.04.6-base-amd64 目录作为容器的根目录, 那么当运行多个容器,就会同时修改到ubuntu-base-16.04.6-base-amd64目录,这样将达不到不同容器使用不同的根文件系统的目的。

所以这节我将会演示如何运行内核提供到联合文件系统的功能,来达到一份镜像,多次运行的目的。

这节代码运行效果:

可以看到我其实启动了两个容器 hello1 ,hello2 然后在hello1 下创建test目录,但是test目录在hello2容器里是不可见的。

联合文件系统原理

首先,来先简单的看看联合文件系统的概念。

联合文件系统可以把其他文件系统的文件和目录挂载到同一个挂载点下,形成统一的文件系统,在挂载点下形成统一的文件视图

在linux内核里,自带了一种叫做overlay类型的文件系统类型,它是一种联合文件系统,类似的还有aufs,不过本文还是用overlay 类型进行举例。

如下是一个挂载overlay 文件系统的mount命令

sudo mount -t overlay overlay -o lowerdir=image-layer1:image-layer2,upperdir=container-layer,workdir=work mnt/

其中contailber-layer 后续会作为容器的读写层,image-layer会作为镜像层,mnt作为overlay联合文件系统的挂载目录,而work后续会作为overlay联合文件系统的工作目录,这个目录是overlay自己用的,对用户不可见。挂载目录为mnt。

也就是说后续进程可以统一访问mnt目录就能看到image-layer 和contailber-layer 这两个目录的内容,但是对mnt目录进行修改的话,则只会将修改体现在contailber-layer这个目录下,image-layer这个目录永远不会变。

关于联合文件系统更详细的解释和命令演示可以参考之前我的一篇博文容器镜像原理- 联合文件系统实践

如何用go代码实现

接着,我们来看看如何对前文的代码进行改造。

已经知道了,当挂载一个overlay文件系统时,镜像层的文件是永远不会变的,所以ubuntu-base-16.04.6-base-amd64这个roofs目录毫无疑问将会作为镜像层进行参数传递,而我们还需要为容器创建其自身的可写层和工作层目录。因为可以运行多个容器,如何区分这些容器各自的可写层呢?最简单的方法就是拥有一个容器名,通过容器名创建属于他们自己的目录。

所以,现在运行命令的方式变了,之前我们是这样运行一个容器:

./tinydocker run /bin/sh

现在将变成这样

./tinydocker run 容器名 /bin/sh

先统一浏览下目前main方法中的代码

func main() {

	switch os.Args[1] {
case "run":
initCmd, err := os.Readlink("/proc/self/exe")
if err != nil {
fmt.Println("get init process error ", err)
return
}
// 获取容器名
containerName := os.Args[2]
os.Args[1] = "init"
cmd := exec.Command(initCmd, os.Args[1:]...)
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
}
cmd.Env = os.Environ()
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
fmt.Println(err)
}
// 容器结束后要清理掉它的挂载点和目录
workspace.DelMntNamespace(containerName)
return
case "init":
var (
containerName = os.Args[2]
cmd = os.Args[3]
)
// 创建挂载点和更换rootfs
if err := workspace.SetMntNamespace(containerName); err != nil {
fmt.Println(err)
return
}
syscall.Chdir("/")
defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), "")
err := syscall.Exec(cmd, os.Args[3:], os.Environ())
if err != nil {
fmt.Println("exec proc fail ", err)
return
}
fmt.Println("forever exec it ")
return
default:
fmt.Println("not valid cmd")
}
}

可以看到,在以新命名空间启动一个子进程后,在workspace.SetMntNamespace 里将会进行相关目录的挂载,然后在执行cmd.Run 的父进程中,等待子进程结束后,调用了workspace.DelMntNamespace清理了子进程的挂载点和相关目录。

而workspace.SetMntNamespace 的源码如下:

func SetMntNamespace(containerName string) error {
if err := os.MkdirAll(mntLayer(containerName), 0700); err != nil {
return fmt.Errorf("mkdir mntlayer fail err=%s", err)
}
if err := os.MkdirAll(workerLayer(containerName), 0700); err != nil {
return fmt.Errorf("mkdir work layer fail err=%s", err)
}
if err := os.MkdirAll(writeLayer(containerName), 0700); err != nil {
return fmt.Errorf("mkdir write layer fail err=%s", err)
} if err := syscall.Mount("overlay", mntLayer(containerName), "overlay", 0,
fmt.Sprintf("upperdir=%s,lowerdir=%s,workdir=%s",
writeLayer(containerName), imagePath, workerLayer(containerName))); err != nil {
return fmt.Errorf("mount overlay fail err=%s", err)
} if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil {
return fmt.Errorf("reclare rootfs private fail err=%s", err)
} if err := syscall.Mount(mntLayer(containerName), mntLayer(containerName), "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
return fmt.Errorf("mount rootfs in new mnt space fail err=%s", err)
}
if err := os.MkdirAll(mntOldLayer(containerName), 0700); err != nil {
return fmt.Errorf("mkdir mnt old layer fail err=%s", err)
}
if err := syscall.PivotRoot(mntLayer(containerName), mntOldLayer(containerName)); err != nil {
return fmt.Errorf("pivot root fail err=%s", err)
}
return nil
}

workspace.SetMntNamespace 先是根据容器名创建了执行overlay挂载所需要的目录,然后通过mount命令讲一个overlay类型的文件系统挂载到mntLayer(containerName)的路径下,然后mntLayer(containerName)路径下文件将作为容器的根文件系统,后续会对其进行pivot root调用,然后mntLayer(containerName)的目录将会成为新的mnt namespace的根目录了。

这样,不同容器名的容器将会有自己独立的根目录。即避免了镜像层文件的改变,又达到了各容器文件系统隔离的目的。

500行代码代码手写docker-将rootfs设置为只读镜像的更多相关文章

  1. 一个小时,200行代码,手写Spring的IOC、DI、MVC

    一.概述 配置阶段:主要是完成application.xml配置和Annotation配置. 初始化阶段:主要是加载并解析配置信息,然后,初始化IOC容器,完成容器的DI操作,已经完成HandlerM ...

  2. java代码完全手写模仿qq登录界面

    这是我模仿QQ2015版界面,实现的基本功能有登陆验证,重置等,当然直接复制代码运行是不一样的,还要注意自己插入自己的图片. 结果截图如下所示: import java.awt.BorderLayou ...

  3. 转载【React Native代码】手写验证码倒计时组件

    实例代码: import React, { Component , PropTypes} from 'react'; import { AppRegistry, StyleSheet, Text, V ...

  4. 用500行Julia代码开始深度学习之旅 Beginning deep learning with 500 lines of Julia

    Click here for a newer version (Knet7) of this tutorial. The code used in this version (KUnet) has b ...

  5. 常见的JS手写函数汇总(代码注释、持续更新)

    最近在复习面试中常见的JS手写函数,顺便进行代码注释和总结,方便自己回顾也加深记,内容也会陆陆续续进行补充和改善. 一.手写深拷贝 <script> const obj1 = { name ...

  6. JavaScript之Promise实现原理(手写简易版本 MPromise)

    手写 Promise 实现 Promise的基本使用 Promise定义及用法详情文档:Promise MAD文档 function testPromise(param) { return new P ...

  7. 一个老程序员是如何手写Spring MVC的

    人见人爱的Spring已然不仅仅只是一个框架了.如今,Spring已然成为了一个生态.但深入了解Spring的却寥寥无几.这里,我带大家一起来看看,我是如何手写Spring的.我将结合对Spring十 ...

  8. 看看一个老程序员如何手写SpringMVC!

    人见人爱的Spring已然不仅仅只是一个框架了.如今,Spring已然成为了一个生态.但深入了解Spring的却寥寥无几.这里,我带大家一起来看看,我是如何手写Spring的.我将结合对Spring十 ...

  9. 我是这样手写 Spring 的(麻雀虽小五脏俱全)

    人见人爱的 Spring 已然不仅仅只是一个框架了.如今,Spring 已然成为了一个生态.但深入了解 Spring 的却寥寥无几.这里,我带大家一起来看看,我是如何手写 Spring 的.我将结合对 ...

  10. 我是这样手写Spring的,麻雀虽小五脏俱全

    人见人爱的Spring已然不仅仅只是一个框架了.如今,Spring已然成为了一个生态.但深入了解Spring的却寥寥无几.这里,我带大家一起来看看,我是如何手写Spring的.我将结合对Spring十 ...

随机推荐

  1. Android进度表示

    在连接上数据库之后,一切都变得简单了呢! 开心,很轻松地就能够将APP里面的相关内容写完啦! 尝试了好久的连接Mysql数据库,最后还是没有成功: 虽然Android studio里面自带的SQLit ...

  2. SpringBoot笔记--配置->profile的配置

    profile--动态配置切换 profile配置方式: 使用spring.profile.active=进行激活.properties文件 直接使用一个.yml文件代替多文件配置 使用---分隔符分 ...

  3. 论文解读(CosFace)《CosFace: Large Margin Cosine Loss for Deep Face Recognition》

    论文信息 论文标题:CosFace: Large Margin Cosine Loss for Deep Face Recognition论文作者:H. Wang, Yitong Wang, Zhen ...

  4. ChatGPT搭建AI网站实战

    1.概述 ChatGPT是一款基于GPT-3.5架构的大型语言模型,它能够进行自然语言处理和生成对话等任务.作为一款智能化的聊天机器人,ChatGPT有着广泛的应用场景,如在线客服.智能助手.个性化推 ...

  5. 基于docker和cri-dockerd部署k8sv1.26.3

    cri-dockerd是什么? 在 Kubernetes v1.24 及更早版本中,我们使用docker作为容器引擎在k8s上使用时,依赖一个dockershim的内置k8s组件:k8s v1.24发 ...

  6. 系统评价——主成分分析PCA的R语言实现(六)

    主成分分析(Principal Component Analysis,PCA),是将多个变量通过线性变换以选出较少个数重要变量的一种多元统计分析方法,起到数据约减和集成的作用.在许多领域的研究与应用中 ...

  7. 二进制安装 Kubernetes(k8s)

    二进制安装 Kubernetes(k8s) Kubernetes 开源不易,帮忙点个star,谢谢了 介绍 kubernetes(k8s) 二进制安装 后续尽可能第一时间更新新版本文档 1.23.3 ...

  8. kubernetes(k8s)中部署dashboard可视化面板

    Web 界面 (Dashboard) Dashboard 是基于网页的 Kubernetes 用户界面.你可以使用 Dashboard 将容器应用部署到 Kubernetes 集群中,也可以对容器应用 ...

  9. Spring源码系列:初探底层,手写Spring

    前言 在学习Spring框架源码时,记住一句话:源码并不难,只需要给你各种业务场景或者项目经理,你也能实现自己的Spring.虽然你的实现可能无法与开源团队相媲美,但是你肯定可以实现一个0.0.1版本 ...

  10. spring事务里面开启线程插入,报错了是否会回滚?

    1.前言 一道非常有意思的面试题目.大概是这样子的,如果在一个事务中,开启线程进行插入更新等操作,如果报错了,事务是否会进行回滚 2.代码 示例1 @RequestMapping("/tes ...