(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. 疯一样的向自己发问 - 剖析lsm 索引原理

    疯一样的向自己发问 - 剖析lsm 索引原理 lsm简析 lsm 更像是一种设计索引的思想.它把数据分为两个部分,一部分放在内存里,一部分是存放在磁盘上,内存里面的数据检索方式可以利用红黑树,跳表这种 ...

  2. Go接入kafka

    需要借助的库 github.com/Shopify/sarama // kafka主要的库* github.com/bsm/sarama-cluster // kafka消费组 生产者 package ...

  3. css中所有的选择器(包括比较少见的选择器)

    jQuery.CSS常用选择器 符号 描述 示例 说明 紧接无符号 相当于"并且"关系 input.k-textbox{   ...} 选出input并且包含k-textbox类的 ...

  4. VBA GET POST HTTP VBA网络爬虫 最新Excel自动获取股票信息源码 EXCEL自动获取网络数据 最新VBA自动抓取股票数据源码

    最新Excel自动获取股票信息源码 EXCEL自动获取网络数据 最新VBA自动抓取股票数据源码 通过接口获取股票数据内容的主要优点包括以下几点: 实时性高:通过访问股票数据接口,可以实时获取到股票的实 ...

  5. 分享一个修改了xml文件再也不用重启的项目mybatis-xmlreload

    自我18年使用 Mybaits 以来,开发环境中如果修改了 xml 文件后,只有重启项目才能生效,如果小项目重启还好,但是对于一个重启需要十几分钟的大型项目来说,这就非常耗时了.开发人员因为修改了xm ...

  6. 在Jupyter Notebook,沉浸式体验ChatGPT

    大家好,我是章北海mlpy 写代码,修Bug是 ChatGPT 目前最擅长的领域之一 今天向大家推荐一个刚刚开源的Python包 安装后可以直接在IPython和Jupyter Notebook中直接 ...

  7. Cmder: 懒癌必备!从此告别记事本记命令的日子

    前言 平时开发中遇到这样那样的命令需要记下来,一般做法是这样. 新建记事本 将需要记下的关键命令保存. 每次需要使用时,粘贴复制即可. 好像没什么毛病!直到遇到了 Cmder... 当看到同事分析问题 ...

  8. prometheus node-exporter安装

    目录 prometheus node-exporter安装 包安装在linux服务器 Daemonset安装 Docker安装 helm安装 prometheus node-exporter安装 安装 ...

  9. cocos2d-x场景间参数传递

    1>使用全局变量     这个就不详细说明了.   2>切换时传递     2.1>在secondScene.h 中加入成员变量,如 int sceneNum;         并在 ...

  10. 吃透Redis面试八股文

    Redis连环40问,绝对够全! Redis是什么? Redis(Remote Dictionary Server)是一个使用 C 语言编写的,高性能非关系型的键值对数据库.与传统数据库不同的是,Re ...