(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. 7个角度,用 ChatGPT 玩转机器学习

    大家好,我是机器学习科普创作者章北海mlpy,探索更高效的学习方法是我一直等追求.现在的初学者太幸福了,可以利用ChatGPT来帮助你学习机器学习的各个方面. 比如[个人首测]百度文心一言 VS GP ...

  2. 如何使用 vue + intro 实现后台管理系统的引导

    引言 为了让用户更好的适应新版,或更方便使用公司内部系统,可以加入新手指引功能.如果你也想在自己的网页加入用户指引,那就试试在 vue 中使用 Intro.js 吧,它能够很轻松的制作出新手指引的效果 ...

  3. 易基因:PIWI/piRNA在人癌症中的表观遗传调控机制(DNA甲基化+m6A+组蛋白修饰)|综述

    大家好,这里是专注表观组学十余年,领跑多组学科研服务的易基因. 2023年03月07日,南华大学衡阳医学院李二毛团队在<Molecular Cancer>杂志发表了题为"The ...

  4. NEFUOJ P903字符串去星问题

    Description 有一个字符串(长度小于100),要统计其中有多少个,并输出该字符串去掉后的新字符串. Input 输入数据有多组,每组1个连续的字符串; Output 在1行内输出该串内有多少 ...

  5. Schillace法则:使用LLM创建软件的最佳实践

    LLM(大语言模型)的发展正在改变软件开发的方式. 以前,开发人员需要编写大量的代码来实现其意图,但现在,随着语言模型的发展,开发人员可以使用自然语言来表达他们的意图,而无需编写大量的代码.这使得软件 ...

  6. CentOS7---部署Tomcat和安装Jpress

    总览需求 1. 简述静态网页和动态网页的区别. 2. 简述 Webl.0 和 Web2.0 的区别. 3. 安装tomcat8,配置服务启动脚本,部署jpress应用. 1.简述静态网页和动态网页的区 ...

  7. 基于sanic和爬虫创建的代理ip池

    搭建免费的代理ip池 需要解决的问题: 使用什么方式存储ip 文件存储 缺点: 打开文件修改文件操作较麻烦 mysql 缺点: 查询速度较慢 mongodb 缺点: 查询速度较慢. 没有查重功能 re ...

  8. 计算机常用的快捷键以及常用的Dos命令

    计算机的常用快捷键有哪些? 今天我重温了Java基础的课程,计算机的快捷键大家肯定不陌生.   计算机的常用快捷键 ctrl+c 复制 ctrl+v 粘贴 ctrl+s 保存 ctrl+x 剪切 ct ...

  9. 全平台数据(数据库)管理工具 DataCap 管理 Rainbond 上的所有数据库

    DataCap是用于数据转换.集成和可视化的集成软件,支持多种数据源.文件类型.大数据相关数据库.关系数据库.NoSQL数据库等.通过该 DataCap 可以实现对多个数据源的管理,对数据源下的数据进 ...

  10. Ubuntu-管理开机自启动服务

    1. 管理服务启停工具 systemctl -- 将应用程序抽象为一个service,然后对这个service进行创建.启停.状态查看.配合journalctl进行日志管理 子命令 效果 start ...