【问题标题】:Explanation of Container From ScratchContainer From Scratch 的解释
【发布时间】:2020-09-24 08:32:01
【问题描述】:

我正在学习容器,尤其是 docker。我刚刚观看了这个 Liz Rice 视频,其中她从头开始创建了一个容器(repo 在 github.com/lizrice)。我无法完全遵循它,因为我是 Docker 和容器的新手,而且我不懂 Go 编程语言。但是,我想看看是否有人可以非常快速地解释代码中的这些项目是/试图完成什么:

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "os/exec"
    "path/filepath"
    "strconv"
    "syscall"
)

// go run main.go run <cmd> <args>
func main() {
    switch os.Args[1] {
    case "run":
        run()
    case "child":
        child()
    default:
        panic("help")
    }
}

func run() {
    fmt.Printf("Running %v \n", os.Args[2:])

    cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags:   syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
        Unshareflags: syscall.CLONE_NEWNS,
    }

    must(cmd.Run())
}

func child() {
    fmt.Printf("Running %v \n", os.Args[2:])

    cg()

    cmd := exec.Command(os.Args[2], os.Args[3:]...)
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    must(syscall.Sethostname([]byte("container")))
    must(syscall.Chroot("/home/liz/ubuntufs"))
    must(os.Chdir("/"))
    must(syscall.Mount("proc", "proc", "proc", 0, ""))
    must(syscall.Mount("thing", "mytemp", "tmpfs", 0, ""))

    must(cmd.Run())

    must(syscall.Unmount("proc", 0))
    must(syscall.Unmount("thing", 0))
}

func cg() {
    cgroups := "/sys/fs/cgroup/"
    pids := filepath.Join(cgroups, "pids")
    os.Mkdir(filepath.Join(pids, "liz"), 0755)
    must(ioutil.WriteFile(filepath.Join(pids, "liz/pids.max"), []byte("20"), 0700))
    // Removes the new cgroup in place after the container exits
    must(ioutil.WriteFile(filepath.Join(pids, "liz/notify_on_release"), []byte("1"), 0700))
    must(ioutil.WriteFile(filepath.Join(pids, "liz/cgroup.procs"), []byte(strconv.Itoa(os.Getpid())), 0700))
}

func must(err error) {
    if err != nil {
        panic(err)
    }
} 

特别是,我对容器的理解是,它是一个虚拟化的运行时环境,用户可以在其中将应用程序与底层系统隔离开来,并且容器只是在单个主机上运行的隔离进程组,它们满足一组“共同特征。我很清楚容器是什么,并试图在更广泛的意义上完成,但我想帮助理解这样一个具体的例子。如果有人很好地理解这一点 - 导入块中正在导入什么;主要功能中的情况是什么; run 函数中的语句有什么用,child 和 cg 函数正在完成什么?

我认为以我目前的理解和阅读 Docker 教程,加上一个从头开始的真实代码示例的解释将非常有益。只是为了确认一下 - 此代码与 Docker 本身无关,代码创建容器之外,Docker 是一种使创建容器更容易的技术。

【问题讨论】:

  • 我在想,这段代码是否以某种方式提供了依赖项或库,或者以某种方式(如 API)向操作系统提供这些项目的访问权限?
  • 如果你想了解你必须学习Go的代码(从围棋之旅开始),问“在导入块中导入了什么是没有意义的[?]”。 Liz 的谈话非常好,并且更详细地解释了 main 的作用和原因,比在 SO 上可以回答更好。试图通过阅读您无法阅读的代码来理解容器,因为您不了解语言以及 Linux 的工作原理是没有意义的。
  • 我不是想学习 Go 或 Linux——我只是想从一个实际的代码示例中学习——为了创建一个容器而实现了什么。我从初学者教程和阅读文章/教程中了解容器,但如果有人可以提供代码的简要解释并将其连接到容器完成的任务 - 我认为这将巩固我的(以及其他人学习容器和 Docker 等)理解无比。我不是在询问语法,而是想了解正在实现的内容以及它如何从代码中转换

标签: docker go containers


【解决方案1】:

她正在通过这样做来创建一种容器:

  • 她将执行 main.go 并传递要在容器中执行的命令
  • 为此,她运行了一个执行 run() 函数的进程
  • 在 run() 函数中,她准备了一个要分叉的进程,该进程将执行 child() 函数
  • 但在实际分叉之前,她通过 syscall.SysProcAttr 为:
    • “unix 分时”(syscall.CLONE_NEWUTS) 这实质上将允许在子进程中拥有单独的主机名
    • PID (syscall.CLONE_NEWPID) 以便在她正在创建的“容器”中拥有从 1 开始的新 PID
    • mounts (syscall.CLONE_NEWNS) 将使“容器”拥有单独的挂载
  • 接下来她执行 fork (cmd.Run())

在分叉的进程中,child() 函数在此处执行:

  • 她通过 cg() 准备了一个控制组,这将限制“容器”可用的资源,这是通过在 /sys/fs/cgroup/ 中编写一些适当的文件来完成的
  • 接下来,她使用传递给 main.go 的 args 准备要执行的命令
  • 她使用 chroot 进入 /home/liz/ubuntufs 下的新根目录
  • 她对特殊 fs proc 和另一个临时 fs 进行了讨论
  • 最后她执行了作为 args 提供给 main.go 的命令

在视频containers from scratch 中,她很好地展示了这一切。 在那里,她在容器中执行了一个 bash,该容器看到了新的 PID,有一个新的主机名,并且限制为 20 个进程。

为了让它工作,她需要在 /home/liz/ubuntufs 下一个完整的 ubuntu fs 克隆。

要带回家的 3 个关键点是容器(以及她的“容器”)本质上是这样做的:

  • 使用命名空间来定义容器将看到的内容 PIDs/mounts(在这个容器示例中她没有处理网络)
  • 使用 chroot 将容器限制为文件系统的一部分
  • 使用 cgroups 来限制容器可以使用的资源

【讨论】:

  • 谢谢!这真的很有帮助,尤其是最后总结了 3 个关键点。可以肯定地说这是某种骨架(即一个容器,但没有附加实际资源)?您能否提供 1 或 2 个资源,这样的容器最终可能会用于或可能会被添加到其中?最后,当你说“为了让它工作,她需要在 /home/liz/ubuntufs 下有一个完整的 ubuntu fs 克隆”。 - 这是“图像”吗?
  • 好吧,它附加了资源:在文件系统中,她使用了一个专用于容器的文件夹,因此操作系统中的一小部分 fs 对容器“正在使用”。操作系统中的root可以访问它,其他并行运行的容器将无法访问它。在其中运行的每个进程都将使用线程、信号量、pid 等,因此这是操作系统资源。实际资源“按需”附加到它,具体取决于您在其中运行的内容。其他资源是网络资源,例如要由您运行的服务器打开的端口。见en.wikipedia.org/wiki/Linux_namespaces#Namespace_kinds
  • 例如Docker 进行端口重映射:docs.docker.com/config/containers/container-networking
  • 关于图像:是的,她使用的文件是一种图像。您可以在 docker 中按原样执行图像,也可以使用 dockerfile 构建自己的应用程序,当您在 dockerfile 中使用命令“FROM”时,您将从您要启动的图像命名,即通常是一个携带文件的操作系统需要作为基础
  • 确实该视频很有启发性,并且阅读周围我发现您可以在此使用单个命令“sudo unshare --fork --pid --mount-proc --uts bash”获得类似的东西执行“unshare ... bash”的情况下,执行 bash 取消共享指定的命名空间:这意味着从默认的名称转义中分离,因此在该 bash 中,您可以重命名主机,安装其他进程看不到的东西,您将拥有分离的 pid 也是如此
【解决方案2】:

由于我缺乏 GO 经验以及对自定义 docker 容器的经验有限,我无法确认这段代码的作用。

虽然这不是直接回答标题中的问题,但我想提供一个答案,帮助您学习 docker 的基础知识以帮助您入门。

您对容器的理解是正确的。尝试查找使用您熟悉的语言的更简单示例的教程。

让您入门的一个简单示例是创建一个您喜欢的 linux 操作系统的容器,将 docker 容器附加到您当前的终端,然后在容器中运行一些特定于操作系统的命令(例如在容器中安装软件或任何linux命令)。

【讨论】:

  • "该代码似乎与运行或创建容器没有任何关系,它更像是在容器内运行的示例代码。" - 不。该代码实际上创建了一个容器。它与运行容器时的 Docker 所做的基本相同,尽管大大简化了。该代码设置了一个“容器”,然后运行相同的二进制文件,使用不同的参数只是为了触发实际代码而不是容器设置代码。
  • @jeremy - 该代码正在正式创建一个容器。上面的答案是错误的。
  • 不太确定,但它似乎正在创建和安装一些文件夹。这可能是更高级的自定义容器设置的一部分,但它不是初学者教程的一部分。由于我缺乏 GO 经验以及对自定义 docker 容器的经验有限,我没有完整的答案,更多的是朝着学习 docker 的正确方向轻推。
  • 嗨@BrunoReis。我已经删除了我对答案的假设,同时保持了相关性。虽然我看不到这段代码是如何用于创建容器的,但在我假设任何一种方式之前,我都应该了解有关创建自定义容器的更多信息。
  • @jeremy 简而言之,该程序最初应该使用“run”参数运行(如主函数上方的注释所示)。 main 函数检查第一个参数是“run”还是“child”。如果它是“运行”,它会进行一些初始设置,然后使用 exec.Command 从同一个二进制文件启动另一个进程,但现在使用“子”参数。这个新过程将再次从 main 开始,但现在使用“child”大小写匹配。然后它使用大量的系统调用来设置一个容器:一个进程将运行的隔离环境。这种孤立的环境(...)
猜你喜欢
  • 2019-09-16
  • 2018-05-04
  • 2022-11-11
  • 2018-06-22
  • 2021-09-02
  • 2020-03-18
  • 2021-11-30
  • 1970-01-01
  • 2019-12-01
相关资源
最近更新 更多