笔记 | 用go写个docker

仅作为自己学习过程的记录,不具备参考价值

前言

看到一段非常有意思的话:

很多人刚接触docker的时候就会感觉非常神奇,感觉这个技术非常新颖,其实并不然,docker使用到的技术都是之前已经存在过的,只不过旧酒换了新瓶罢了。简单来说docker本质其实是一个特殊的进程,这个进程特殊在它被NamespaceCgroup技术做了装饰,Namespace将该进程与Linux系统进行隔离开来,让该进程处于一个虚拟的沙盒中,而Cgroup则对该进程做了一系列的资源限制,两者配合模拟出来一个沙盒的环境。

本文的学习地址/参考文档:

  • 从零自制docker
  • manpages.ubuntu.com
  • github
  • segmentfault.com
  • juejin.im
  • 地鼠文档
感谢大佬的写作,受益良多。自认一介尘民做喜欢且能安身立命之本乃人生一大幸事
本文只是对照其进行的拙劣模仿 以及自己半猜测式的研究记录。如有疑问 欢迎指出,感谢

代码环境配置

因为我是在Windows里面写代码,然后进行交叉编译到Linux,所以这里要更改下环境,因为在不同的环境中,go导入的文件也是不同,如果我们的环境使用的Windows,那么使用os/exec包时,导入的将是exec_windows.go,而如果我们的环境是Linux,那么将会导入exec_linux.go文件,因为只有Linux才会给创建进程时提供这个隔离参数,所以我们需要把环境改成Linux
GoLand配置


进程隔离

clone系统调用

  • CLONE_NEWPID:
当程序代码调用clone时,设定了CLONE_NEWPID,就会创建一个新的PID Namespace,clone出来的新进程将成为Namespace里的第一个进程。一个PID Namespace为进程提供了一个独立的PID环境,PID Namespace内的PID将从1开始,在Namespace内调用fork,vfork或clone都将产生一个在该Namespace内独立的PID。新创建的Namespace里的第一个进程在该Namespace内的PID将为1,就像一个独立的系统里的init进程一样。该Namespace内的孤儿进程都将以该进程为父进程,当该进程被结束时,该Namespace内所有的进程都会被结束。PID Namespace是层次性,新创建的Namespace将会是创建该Namespace的进程属于的Namespace的子Namespace。子Namespace中的进程对于父Namespace是可见的,一个进程将拥有不止一个PID,而是在所在的Namespace以及所有直系祖先Namespace中都将有一个PID。系统启动时,内核将创建一个默认的PID Namespace,该Namespace是所有以后创建的Namespace的祖先,因此系统所有的进程在该Namespace都是可见的。
  • CLONE_NEWIPC:
当调用clone时,设定了CLONE_NEWIPC,就会创建一个新的IPC Namespace,clone出来的进程将成为Namespace里的第一个进程。一个IPC Namespace有一组System V IPC objects 标识符构成,这标识符有IPC相关的系统调用创建。在一个IPC Namespace里面创建的IPC object对该Namespace内的所有进程可见,但是对其他Namespace不可见,这样就使得不同Namespace之间的进程不能直接通信,就像是在不同的系统里一样。当一个IPC Namespace被销毁,该Namespace内的所有IPC object会被内核自动销毁。
  • PID Namespace和IPC Namespace:
PID Namespace和IPC Namespace可以组合起来一起使用,只需在调用clone时,同时指定CLONE_NEWPID和CLONE_NEWIPC,这样新创建的Namespace既是一个独立的PID空间又是一个独立的IPC空间。不同Namespace的进程彼此不可见,也不能互相通信,这样就实现了进程间的隔离。
  • CLONE_NEWNS:
当调用clone时,设定了CLONE_NEWNS,就会创建一个新的mount Namespace。每个进程都存在于一个mount Namespace里面,mount Namespace为进程提供了一个文件层次视图。如果不设定这个flag,子进程和父进程将共享一个mount Namespace,其后子进程调用mount或umount将会影响到所有该Namespace内的进程。如果子进程在一个独立的mount Namespace里面,就可以调用mount或umount建立一份新的文件层次视图。该flag配合pivot_root系统调用,可以为进程创建一个独立的目录空间。
  • CLONE_NEWNET:
当调用clone时,设定了CLONE_NEWNET,就会创建一个新的Network Namespace。一个Network Namespace为进程提供了一个完全独立的网络协议栈的视图。包括网络设备接口,IPv4和IPv6协议栈,IP路由表,防火墙规则,sockets等等。一个Network Namespace提供了一份独立的网络环境,就跟一个独立的系统一样。一个物理设备只能存在于一个Network Namespace中,可以从一个Namespace移动另一个Namespace中。虚拟网络设备(virtual network device)提供了一种类似管道的抽象,可以在不同的Namespace之间建立隧道。利用虚拟化网络设备,可以建立到其他Namespace中的物理设备的桥接。当一个Network Namespace被销毁时,物理设备会被自动移回init Network Namespace,即系统最开始的Namespace。
  • CLONE_NEWUTS:
当调用clone时,设定了CLONE_NEWUTS,就会创建一个新的UTS Namespace。一个UTS Namespace就是一组被uname返回的标识符。新的UTS Namespace中的标识符通过复制调用进程所属的Namespace的标识符来初始化。Clone出来的进程可以通过相关系统调用改变这些标识符,比如调用sethostname来改变该Namespace的hostname。这一改变对该Namespace内的所有进程可见。CLONE_NEWUTS和CLONE_NEWNET一起使用,可以虚拟出一个有独立主机名和网络空间的环境,就跟网络上一台独立的主机一样。
  • 集合
以上所有clone flag都可以一起使用,为进程提供了一个独立的运行环境。LXC正是通过在clone时设定这些flag,为进程创建一个有独立PID,IPC,FS,Network,UTS空间的container。一个container就是一个虚拟的运行环境,对container里的进程是透明的,它会以为自己是直接在一个系统上运行的。

Namespace隔离

package mainimport ("log""os""os/exec""syscall"
)func main() {cmd := exec.Command("sh")// 设置新的Namespacecmd.SysProcAttr = &syscall.SysProcAttr{//设置了系统调用的属性,特别是Cloneflags,它指定了新进程将使用哪些Namespace。//syscall.CLONE_NEWNS隔离了挂载点//syscall.CLONE_NEWUTS隔离了主机名和域名//syscall.CLONE_NEWPID隔离了进程ID//syscall.CLONE_NEWNET隔离了网络Cloneflags: syscall.CLONE_NEWNS |syscall.CLONE_NEWUSER |syscall.CLONE_NEWIPC |syscall.CLONE_NEWUTS |syscall.CLONE_NEWPID |syscall.CLONE_NEWNET,}//将新命令的输入、输出和错误输出重定向到当前进程的对应文件描述符cmd.Stdin = os.Stdincmd.Stdout = os.Stdoutcmd.Stderr = os.Stderrif err := cmd.Run(); err != nil {log.Fatal(err)}
}

构建过程不多赘述,直接丢到ubuntu上测试一下:

SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=amd64
go build -o main

效果如下,外部的主机名并没有被改变,说明我们的go进程成功将自身的hostname与外部hostname进行了隔离。
命名空间隔离运行效果

设置容器的UID和GID

Linux系统中,每个进程都与特定的用户ID(UID)和组ID(GID)关联,这些ID决定了进程对文件、设备和系统资源的访问权限。在传统的Linux系统中,这些ID是全局的,意味着系统中的每个UIDGID在任何时候都指向相同的用户或用户组。

随着容器技术的发展,出现了一种对这些ID进行隔离的需求,以便在容器环境中提供安全性和多租户隔离。用户命名空间(User Namespaces)Linux内核的一个特性,使这种隔离成为可能。

当你创建一个新的用户命名空间时,可以定义一个UIDGID的映射,这个映射告诉内核如何将命名空间内的ID转换为命名空间外的主机系统ID。这样,即使是容器内部以root身份运行的进程,在宿主机中也可以被限制为非特权用户,从而提高了安全性。

UidMappings 和 GidMappings 字段

UidMappingsGidMappings字段是在创建新的用户命名空间时使用的,它们定义了容器内部ID和宿主机ID之间的映射关系。这些字段是syscall.SysProcAttr结构体的一部分,当使用CLONE_NEWUSER标志创建新的用户命名空间时,需要设置这些字段。

举个栗子
UidMappings: []syscall.SysProcIDMap{{ContainerID: 1,HostID:      0,Size:        1,},
},
GidMappings: []syscall.SysProcIDMap{{ContainerID: 1,HostID:      0,Size:        1,},
},

这里的映射定义了以下关系:

  • ContainerID: 1:这是命名空间内部使用的UID/GID。在此例中,我们使用的是编号为1的ID。
  • HostID: 0:这是宿主机上的UID/GID,编号0通常代表root用户/组。
  • Size: 1:这表示映射的范围。大小为1意味着只有一个UID/GID被映射。

在这个映射中,我们说命名空间内部的UID/GID 1对应于宿主机的root用户/组。这意味着,在此用户命名空间中运行的进程,尽管它可能以UID 1执行,但它在命名空间外部(宿主机上)被视为root用户。因此,这个进程在宿主机角度看来有root权限,但是这通常不是我们期望的。通常,我们希望在容器内部拥有较高权限的进程,在宿主机上对应为较低权限的用户,以提供更强的安全隔离。

实践

在实际的容器环境中,通常会将容器内部的root用户(UID 0)映射到宿主机上的一个非特权用户。例如,UID映射可能如下所示:

UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, // 容器内的root用户HostID:      1000, // 宿主机上的非特权用户Size:        1,},
},

在这个例子中,容器内部的root用户(ContainerID: 0)实际上是宿主机上的UID 1000,这通常是一个普通用户。这样,即使容器内部的进程以root身份运行,它也只有宿主机上普通用户的权限,从而限制了它可能造成的安全风险。
在新增用户映射后代码如下:

cmd := exec.Command("sh")cmd.SysProcAttr = &syscall.SysProcAttr{Cloneflags: syscall.CLONE_NEWNS |syscall.CLONE_NEWUSER |syscall.CLONE_NEWIPC |syscall.CLONE_NEWUTS |syscall.CLONE_NEWPID |syscall.CLONE_NEWNET,// 设置容器的UID和GIDUidMappings: []syscall.SysProcIDMap{{ContainerID: 0,HostID: 1000,Size:   1,},},GidMappings: []syscall.SysProcIDMap{{ContainerID: 0,HostID: 1000,Size:   1,},},}

资源限制

cgroups(控制组)是一种内核特性,用于限制、记录和隔离进程组所使用的物理资源(如CPU、内存、磁盘I/O等)。在Go语言中,我们可以通过操作/sys/fs/cgroup下的文件来与cgroups交互。这涉及到文件系统的操作,比如创建目录、写入文件等,这些都可以通过Go标准库中的os包来实现。

基础概念

在开始编写代码之前,我们需要了解cgroups的一些基础概念:

  • Cgroup子系统:cgroups将其功能按资源类型划分为多个子系统,如cpumemoryblkio
  • Cgroup层级:每个子系统可以挂载到一个或多个层级,每个层级可以创建多个cgroup
  • Cgroup:每个cgroup代表一组进程,并且每个cgroup都可以设置资源限制或统计

使用Go操作cgroups大致可以分为以下几个步骤:

  • 挂载cgroup子系统:通常在Linux系统启动时,cgroup子系统就已经被挂载。你可以在/sys/fs/cgroup目录下看到各种资源类型的目录
  • 创建cgroup:通过在相应的子系统目录下创建新目录来创建cgroup
  • 添加进程到cgroup:将进程ID写入到cgroup目录的cgroup.procs文件中
  • 设置资源限制:通过修改或写入特定的配置文件(如memory.limit_in_bytes)来设置资源限制
  • 清理:任务完成后,删除cgroup以释放资源
实际在ubuntu上操作一下,首先创建一个挂载点目录:
mkdir CgroupTest
挂载hierarchy
mount -t cgroup -o none,name=CgroupTest CgroupTest ./CgroupTest/

查看内容:
查看内容

  • cgroup.clone_childrensubsystem会读取该文件,如果该文件里面的值为1的话,那么子 cgroup将会继承父cgroupcpuset配置
  • cgroup.procs:记录当前节点cgroup中的进程组ID
  • task: 标识该cgroup下的进程ID,如果将某个进程的ID写到该文件中,那么便会将该进程加入到当前的cgroup
新建子cgroup

只要在挂载了hierarchy的目录下,新建一个子目录,那么新的子目录会被自动标记为该cgroup的子cgroup
新建子cgroup
这个目录1就是CgroupTest的子cgroup,默认情况下,他会继承父cgroup的配置

通过subsystem 限制cgroup中进程的资源

上述创建的hierarchy并没有关联到任何的subsystem,所以没办法通过上面的hierarchy中的cgroup节点来限制进程的资源占用,其实系统默认已经为每个subsystem创建了一个默认的hierarchy,它在Linux/sys/fs/cgroup路径下:
/sys/fs/cgroup
如果想限制某个进程ID的内存,那么就在/sys/fs/cgroup/memory目录下创建一个限制mermorycgroup,只要创建一个文件夹即可,kernel会自动把该文件夹标记为一个cgroup,我们来尝试一下:
内存限制
可以看到该目录下,自动给我们创建出来了很多限制资源文件,我们只要将进程ID写到该文件夹下的task文件中,然后修改名叫meory.limit_in_bytes的文件内容,就能限制该进程的内存使用。
如果在这时你打算删掉这些测试目录,可能会发现:即使你使用了root用户,依旧无法删除/sys/fs/cgroup/memory/下的目录文件:
删除
此时你可以去检查该cgroup的连接:

lsof | grep /sys/fs/cgroup/memory/CgroupTest1

如果查到有连接直接kill掉,然后使用rmdir命令进行删除:

rmdir CgroupTest1/

rmdir删除

Go中使用Cgroup

package mainimport ("fmt""io/ioutil""os""os/exec""path""strconv""syscall"
)const (// 挂载memory subsystem的hierarchy的根目录位置cgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory"
)func main() {if os.Args[0] == "/proc/self/exe" {//容器进程fmt.Printf("current pid %d \n", syscall.Getpid())cmd := exec.Command("sh", "-c", "stress --vm-bytes 200m --vm-keep -m 1")cmd.SysProcAttr = &syscall.SysProcAttr{}cmd.Stdin = os.Stdincmd.Stdout = os.Stdoutcmd.Stderr = os.Stderrif err := cmd.Run(); err != nil {fmt.Printf("Error running stress command: %v\n", err)return}}cmd := exec.Command("/proc/self/exe")cmd.SysProcAttr = &syscall.SysProcAttr{Cloneflags: syscall.CLONE_NEWNS |syscall.CLONE_NEWUSER |syscall.CLONE_NEWIPC |syscall.CLONE_NEWUTS |syscall.CLONE_NEWPID |syscall.CLONE_NEWNET,}if err := cmd.Start(); err != nil {fmt.Printf("Error starting process: %v\n", err)return}// 得到 fork出来进程映射在外部命名空间的pidfmt.Printf("New process PID: %+v\n", cmd.Process.Pid)// 创建子cgroupnewCgroup := path.Join(cgroupMemoryHierarchyMount, "Cgroup-Test")if err := os.Mkdir(newCgroup, 0755); err != nil {fmt.Printf("Error creating cgroup: %v\n", err)return}defer os.RemoveAll(newCgroup)// 将容器进程放到子cgroup中if err := ioutil.WriteFile(path.Join(newCgroup, "tasks"), []byte(strconv.Itoa(cmd.Process.Pid)), 0644); err != nil {fmt.Printf("Error adding process to cgroup: %v\n", err)return}// 限制cgroup的内存使用if err := ioutil.WriteFile(path.Join(newCgroup, "memory.limit_in_bytes"), []byte("100m"), 0644); err != nil {fmt.Printf("Error setting memory limit: %v\n", err)return}
}

未完待续

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

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

相关文章

操作系统——信号

将信号分为以上四个阶段 1.信号注册:是针对信号处理方式的规定,进程收到信号时有三种处理方式:默认动作,忽略,自定义动作。如果不是自定义动作,这一步可以忽略。这个步骤要使用到signal/sigaction接口 2.…

5. 条件和递归

5. 条件和递归 本章主要话题是if表达式, 它根据程序的状态执行不同的代码. 但首先介绍两个操作符号: 向下取整除法操作符和求模操作符.5.1 向下取整除法操作符和求模操作符 向下取整除法操作符(//)对两个数除法运算, 并向下取整得到一个整数. 假设, 一个电影的播放时长为105分…

Java | Leetcode Java题解之第150题逆波兰表达式求值

题目&#xff1a; 题解&#xff1a; class Solution {public int evalRPN(String[] tokens) {int n tokens.length;int[] stack new int[(n 1) / 2];int index -1;for (int i 0; i < n; i) {String token tokens[i];switch (token) {case "":index--;stack…

vi/vim使用命令

你是否在编辑文件时以为键盘坏了&#xff0c;为什么不能删除呢&#xff0c;为什么不能敲代码呢&#xff0c;等你初识vi&#xff0c;会觉得这个东西为什么设计得这么难用&#xff0c;这篇教程带你熟练得用上这款经典的工具 Vi 是在 Unix 系统上广泛使用的编辑器&#xff0c;Vim …

Unity与Js通信交互

目录 1.Js给Unity传递消息 2.Unity给Js传递消息 简介: Unity 与 JavaScript 通信交互是指在 Unity 项目中实现与 JavaScript 代码进行数据交换和功能调用的过程。 在 Unity 中&#xff0c;可以通过特定的接口和技术来与外部的 JavaScript 环境进行连接。这使得 Unity 能够利…

进击算法工程师深度学习课程

"进击算法工程师深度学习课程"旨在培养学员在深度学习领域的专业技能和实战经验。课程涵盖深度学习基础理论、神经网络架构、模型优化方法等内容&#xff0c;通过项目实践和算法实现&#xff0c;帮助学员掌握深度学习算法原理和应用&#xff0c;提升在算法工程师领域…

EasyExcel文件导出,出现有文件但没有数据的问题

一开始由于JDK版本过高&#xff0c;我用的17&#xff0c;一直excel没有数据&#xff0c;表头也没有&#xff0c;后来摸索了好久&#xff0c;找了资料也没有&#xff0c;后来改了代码后报了一个错误&#xff08;com.alibaba.excel.exception.ExcelGenerateException: java.lang.…

【抽代复习笔记】19-群(十三):奇偶置换、循环置换的几个定理及例题

定义&#xff1a; ①在Sn中&#xff0c;能够表示为奇数多个对换乘积的置换称为“奇置换”&#xff0c;能够表示为偶数多个对换乘积的置换称为“偶置换”&#xff1b; ②所有偶置换的集合记为An。 例1&#xff1a;&#xff08;1&#xff09;计算S1和S2中奇、偶置换的数目&…

数据中台-知识图谱平台

【数据分析小兵】专注数据中台产品领域,覆盖开发套件,包含数据集成、数据建模、数据开发、数据服务、数据可视化、数据治理相关产品以及相关行业的技术方案的分享。对数据中台产品想要体验、做二次开发、关注方案资料、做技术交流的朋友们&#xff0c;可以关注我。 1. 概述 随着…

ResNet——Deep Residual Learning for Image Recognition(论文阅读)

1.什么是ResNet ResNet是一种残差网络&#xff0c;咱们可以把它理解为一个子网络&#xff0c;这个子网络经过堆叠可以构成一个很深的网络。下面是ResNet的结构。 2.为什么要引入ResNet 理论上来说&#xff0c;堆叠神经网络的层数应该可以提升模型的精度。但是现实中真的是这…

智慧守护 畅游无忧——北斗应急呼叫柱,为景区安全加码

在大自然的怀抱中&#xff0c;中型及大型公园、景区以其壮丽风光吸引着成千上万的游客前来探索&#xff0c;成为了人们休闲娱乐的好去处。然而&#xff0c;广袤的区域、复杂的地形和分散的人流也给安全保障带来了前所未有的挑战。传统的巡逻方式难以覆盖每一个角落&#xff0c;…

【第六篇】SpringSecurity的权限管理

一、权限管理的实现 服务端的各种资源要被SpringSecurity的权限管理控制可以通过注解和标签两种方式来处理。 放开了相关的注解后在Controller中就可以使用相关的注解来控制了 JSR250注解 /*** JSR250*/ @Controller @RequestMapping("/user") public class UserC…

物理隔离后数据怎么导入和导出?安全U盘一键解决

政府单位、军工和科研所、航空航天企业、金融机构、医疗单位、电力企业、生物制药实验室等企业及单位&#xff0c;因研发和生产过程、或日常经营中涉及大量敏感信息和技术&#xff0c;需要通过物理隔离来确保网络的安全性。因此&#xff0c;多采用物理隔离的方式进行网络建设。…

前端 CSS 经典:在 Vue3 中使用渐进式图片

1. 什么是渐进式图片 当我们网站会加载很多图片的时候&#xff0c;有些图片尺寸很大&#xff0c;加载就会很慢&#xff0c;会导致页面长时间陷入白屏状态&#xff0c;用户体验很不好。所以可以使用渐进式图片&#xff0c;先给用户展示模糊图&#xff0c;这些图尺寸小&#xff…

django学习入门系列之第二点《浏览器能识别的标签3》

文章目录 列表表格往期回顾 列表 无序列表 <!-- <ul </ul> 无序列表 --> <ul><li> 内容1 </li><li> 内容2 </li><li> 内容3 </li><li> 内容4 </li> </ul>有序列表 <!-- <ol> &…

WordPress、Typecho 站点如何让 CloudFlare 缓存加速

众所周知 WordPress、Typecho 都是著名动态博客站点(一个最简单的判断依据就是都要依赖结合数据库),这类站点在 CDN 缓存上都有一个致命的缓存弊端就是动静态请求的区分,理论上要让 CDN 绕过所有的动态请求,缓存所有的静态请求,否则就会造成前端登录和非登录状态的混乱,…

小程序制作成本是多少?揭秘隐藏费用!

在日常生活中&#xff0c;有很多小程序&#xff0c;其中我们最熟悉的是微信小程序&#xff0c;所以小程序对我们每个人来说并不陌生。那么&#xff0c;你知道制作一个小程序要花多少钱吗&#xff1f;制作小程序的成本是多少&#xff1f;今天&#xff0c;本文将带领您揭示小程序…

禁渔期水域监管:EasyCVR视频智能监控方案

一、背景与需求分析 根据农业部印发的《中国渔政亮剑2024系列专项执法行动方案》&#xff0c;我国将持续推进长江十年禁渔、海洋伏季休渔、黄河等内陆重点水域禁渔等专项行动。根据四川省相关规定&#xff0c;每年3月1日至6月30日为禁渔期&#xff0c;在此期间&#xff0c;四川…

颠覆与创新:探寻Facebook未来的发展路径

Facebook&#xff0c;这个曾经引领社交网络革命的巨头&#xff0c;在如今竞争激烈的科技市场中&#xff0c;正面临着前所未有的挑战和机遇。如何在不断变化的数字世界中保持竞争力&#xff0c;成为业界领先者&#xff0c;这是摆在Facebook面前的重要课题。本文将探寻Facebook未…

单位经常要你加班却不发加班费,你想到这一招没有?

单位经常要你加班却不发加班费&#xff0c;你想到这一招没有&#xff1f; 你也许经常在忙碌的工作和繁重的加班中度过&#xff0c;然而&#xff0c;却从未得到过应有的加班费。但你又不想离开这个单位&#xff0c;或许单位的工作环境人性化&#xff0c;同事之间的关系融洽&…