Docker 作为一种流行的容器化技术,对于每一个程序开发者而言都具有重要性和必要性。因为容器化相关技术的普及大大简化了开发环境配置、更好的隔离性和更高的安全性,对于部署项目和团队协作而言也更加方便。本文将尝试使用 Go 语言编写一个极简版的容器,以此来了解容器的基本原理。
Docker 是基于 Linux 容器技术构建的,因此了解 Linux 操作系统的基本原理、命令和文件系统等知识对于理解本文乃至于Docker 源码非常重要。
了解容器技术的基本概念、原理和实现方式对于理解 Docker 源码非常有帮助。可以参考 Docker 官方文档[2]中的容器概述部分,以及相关的教程和文章。
Docker 的源码主要是用 Go 语言编写的,具体可以参考Go 语言官方文档[3]。
容器化是作为一种虚拟化技术,允许应用程序和其依赖的资源(如库、环境变量等)被封装在一个独立的运行环境中,称为容器。其核心概念主要包括:
容器使用操作系统级别的虚拟化技术,如Linux的命名空间和控制组(cgroup),实现隔离。每个容器都有自己的进程空间、文件系统、网络和用户空间,使得容器之间相互隔离,不会相互干扰。
相比传统的虚拟机(VM),容器更加轻量级。容器共享主机操作系统的内核,因此启动更快、占用更少的资源。
容器可以在不同的环境中运行,包括开发、测试和生产环境。容器以相同的方式运行,不受底层基础设施的影响,提供了更好的可移植性。
容器可以根据需求进行扩展和缩减。容器编排工具(如Kubernetes)可以自动管理容器的部署、伸缩和负载均衡,提供弹性和可扩展性。
"如果创建一个容器就像系统调用 create_container 一样简单就好了"
这里我们粗略的估算一下可能涉及到的步骤会有:导入必要的包、main函数、子进程及其命名空间、挂载文件系统、运行子进程命令等。
我们知道真正的容器实现要复杂得多。它可能会涉及更多的命名空间设置、资源限制、文件系统挂载、网络配置等方面的工作。
但是本文,“删繁就简”,主要是为了了解容器的基本原理。
按照这种实现的思路,我们开始一步步用代码实现:
package main
import (
"fmt"
"os"
"os/exec"
"syscall"
)
func main() {
// 根据命令行参数选择执行不同的操作
switch os.Args[1] {
case "run":
parent() // 执行parent函数
case "child":
child() // 执行child函数
default:
panic("wat should I do") // 抛出异常,程序无法继续执行
}
}
func parent() {
cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// 运行命令并检查错误
if err := cmd.Run(); err != nil {
fmt.Println("ERROR", err)
os.Exit(1)
}
}
func child() {
cmd := exec.Command(os.Args[2], os.Args[3:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// 运行命令并检查错误
if err := cmd.Run(); err != nil {
fmt.Println("ERROR", err)
os.Exit(1)
}
}
func must(err error) {
// 如果错误不为空,抛出panic异常
if err != nil {
panic(err)
}
}
我们从 main.go 开始,读取第一个参数。如果是 "run",我们就运行Parent函数,如果是 "child",我们就运行子方法。父方法运行"/proc/self/exe",这是一个包含当前可执行文件内存映像的特殊文件。
换句话说,我们重新运行自己,但将 child 作为第一个参数传递。
我们可以借此执行另外一个执行用户请求的程序(在 os.Args[2:]
中提供)。有了这个简单的脚手架,我们就可以创建一个容器了。
在 Linux 中,命名空间(Namespace)[6]是一种内核功能,用于隔离进程的资源视图。它允许在同一系统上运行的进程具有独立的资源副本,如进程 ID、网络接口、文件系统挂载点等。这种隔离性可以提供更好的安全性和资源管理。以下是一些常见的 Linux 命名空间类型:
Linux UTS Namespace[7]。在 UTS 命名空间中,每个命名空间都有自己的主机名和域名。UTS 命名空间的使用场景包括:容器化和网络隔离等。
要在程序中添加命名空间,我们只需在 parent() 方法的第二行,添加下面的这几行代码,以便于在Go运行子进程时传递给其一些额外的标识。
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS、
}
如果现在运行程序,程序将在 UTS、PID 和 MNT 命名空间内运行。
在 Docker 中,根文件系统是由 Docker 镜像提供的,并且在容器启动时被挂载到容器的根目录上。Docker 根文件系统一般具有分层结构、只读性和写时复制等特性。
现在,虽然我们的进程处于一组孤立的命名空间中,但文件系统看起来与主机相同。为了解决这个问题,我们需要以下四行代码来实现根文件系统:
must(syscall.Mount("rootfs", "rootfs", "", syscall.MS_BIND, ""))
must(os.MkdirAll("rootfs/oldrootfs", 0700))
// 将当前目录 `/` 移到 `rootfs/oldrootfs` 并将新的 rootfs 目录交换到 `/`
must(syscall.PivotRoot("rootfs", "rootfs/oldrootfs"))
must(os.Chdir("/"))
所以完整代码如下:
package main
import (
"fmt"
"os"
"os/exec"
"syscall"
)
func main() {
// 根据命令行参数选择执行不同的操作
switch os.Args[1] {
case "run":
parent() // 执行parent函数
case "child":
child() // 执行child函数
default:
panic("wat should I do") // 抛出异常,程序无法继续执行
}
}
func parent() {
cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
// 设置子进程的命名空间
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// 运行命令并检查错误
if err := cmd.Run(); err != nil {
fmt.Println("ERROR", err)
os.Exit(1)
}
}
func child() {
// 挂载文件系统
must(syscall.Mount("rootfs", "rootfs", "", syscall.MS_BIND, ""))
must(os.MkdirAll("rootfs/oldrootfs", 0700))
must(syscall.PivotRoot("rootfs", "rootfs/oldrootfs"))
must(os.Chdir("/"))
cmd := exec.Command(os.Args[2], os.Args[3:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// 运行命令并检查错误
if err := cmd.Run(); err != nil {
fmt.Println("ERROR", err)
os.Exit(1)
}
}
func must(err error) {
// 如果错误不为空,抛出panic异常
if err != nil {
panic(err)
}
}
是的,至此,基于golang实现的极简版的容器代码已经有了基本骨架。
Linux Cgroups[8] 在 Docker 容器化中起着重要的作用,它提供了对容器的资源限制和隔离,使得容器可以在共享的宿主机上运行而不会相互干扰:
通过 Cgroups,Docker 可以对容器的资源使用进行限制,如 CPU、内存、磁盘和网络等。这样可以避免容器过度占用宿主机资源,保证系统的稳定性和公平性。
Cgroups 提供了容器级别的资源隔离,每个容器都可以被分配和限制其使用的资源。这样,容器之间的资源使用不会互相干扰,一个容器的问题也不会影响其他容器或宿主机。
Docker 使用 Cgroups 对容器进行管理和监控。通过读取和设置 Cgroups 的属性,Docker 可以实时了解容器的资源使用情况,并可以调整资源限制以满足需求。
在cgroup(控制组)这部分,需要注意Cgroup 的挂载和层级结构等限制。
所以我们将Cgrous这一部分加入到代码实现中来如下:
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"strconv"
"syscall"
)
func main() {
// 创建 cgroup
err := createCgroup("mycontainer")
if err != nil {
fmt.Println("Failed to create cgroup:", err)
return
}
defer func() {
// 退出时删除 cgroup
err := deleteCgroup("mycontainer")
if err != nil {
fmt.Println("Failed to delete cgroup:", err)
}
}()
// 限制 CPU 使用率为 50%
err = setCPULimit("mycontainer", 50)
if err != nil {
fmt.Println("Failed to set CPU limit:", err)
return
}
// 在容器中运行命令
cmd := exec.Command("/bin/bash")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWPID | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWNET,
Cgroup: "mycontainer",
}
err = cmd.Run()
if err != nil {
fmt.Println("Failed to run command in container:", err)
}
}
func createCgroup(name string) error {
cgroupPath := "/sys/fs/cgroup/cpu/" + name
err := os.Mkdir(cgroupPath, 0755)
if err != nil {
return err
}
// 将当前进程加入到 cgroup 中
err = ioutil.WriteFile(cgroupPath+"/tasks", []byte(strconv.Itoa(os.Getpid())), 0644)
if err != nil {
return err
}
return nil
}
func deleteCgroup(name string) error {
cgroupPath := "/sys/fs/cgroup/cpu/" + name
err := os.Remove(cgroupPath)
if err != nil {
return err
}
return nil
}
func setCPULimit(name string, limit int) error {
cgroupPath := "/sys/fs/cgroup/cpu/" + name
err := ioutil.WriteFile(cgroupPath+"/cpu.cfs_quota_us", []byte(strconv.Itoa(limit*1000)), 0644)
if err != nil {
return err
}
return nil
}
在上面,我们将当前进程加入到新创建的"mycontainer" 的 cgroup,然后,设置该 cgroup 的 CPU 使用率限制为 50%。继而实现在容器中运行一个交互式的 shell。
编写一个容器(container)是一个相当复杂的任务,涉及到许多底层的概念和技术。回顾本文,使用golang一步步“还原”一个mini版的container所需步骤基本如下:
os/exec
和syscall
。syscall
包,设置容器的命名空间,如 PID 命名空间、网络命名空间等。这样可以将容器中的进程与主机系统的进程隔离开来。os
和io/ioutil
包来操作文件系统。os/exec
包,在容器的命名空间中启动一个新的进程, 并指定要运行的可执行文件和参数。除此之外,还需要考虑到安全性、权限管理、资源限制等多方面因素。
当然,实际的容器实现要更加复杂和完善。在实际项目应用中,我们可能还需要考虑到如文件系统隔离、网络隔离等远比这些复杂的场景。
本文由微信公众号奇舞精选原创,哈喽比特收录。
文章来源:https://mp.weixin.qq.com/s/Bh05LxVG5jeVQdsYY-ZSxA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。