0
点赞
收藏
分享

微信扫一扫

构造容器 实现run版本的容器

花明 2021-09-24 阅读 91
PaaS

概述

首先实现一个简单版本的 run 命令,类似于 docker run -ti [command] 。然后再后续逐步添加 network mount filesystem 等功能。为了方便了解Docker 启动容器的原理,该简单版本的实现参考 runC 实现。

源码结构

目前的代码文件结构如下。

mydocker/
|-- container
|   |-- container_process.go
|   `-- init.go
|-- Godeps
|   |-- Godeps.json
|   `-- Readme
|-- main_command.go
|-- main.go
|-- network
|   `-- test_linux.go
|-- README.md
|-- run.go
`-- vendor

源码解析

main.go

package main

import (
        log "github.com/Sirupsen/logrus"
        "github.com/urfave/cli"
        "os"
)

const usage = `mydocker is a simple container runtime implementation.
                           The purpose of this project is to learn how docker works and how to write a docker by ourselves
                           Enjoy it, just for fun.`

func main() {
        app := cli.NewApp()
        app.Name = "mydocker"
        app.Usage = usage

        app.Commands = []cli.Command{
                initCommand,
                runCommand,
        }

        app.Before = func(context *cli.Context) error {
                // Log as JSON instead of the default ASCII formatter.
                log.SetFormatter(&log.JSONFormatter{})

                log.SetOutput(os.Stdout)
                return nil
        }

        if err := app.Run(os.Args); err != nil {
                log.Fatal(err)
        }
}

使用 github.com/urfave/cli 提供的命令行工具,定义了mydocker 的几个 基本命令 ,包括runCommand和initCommand ,然后在 app Before 内初始化 logrus 的日志。

main_command.go

package main

import (
        "fmt"
        log "github.com/Sirupsen/logrus"
        "github.com/urfave/cli"
        "github.com/xianlubird/mydocker/container"
)

var runCommand = cli.Command{
        Name: "run",
        Usage: `Create a container with namespace and cgroups limit
                        mydocker run -ti [command]`,
        Flags: []cli.Flag{
                cli.BoolFlag{
                        Name:  "ti",
                        Usage: "enable tty",
                },
        },
        Action: func(context *cli.Context) error {
                if len(context.Args()) < 1 {
                        return fmt.Errorf("Missing container command")
                }
                cmd := context.Args().Get(0)
                tty := context.Bool("ti")
                Run(tty, cmd)
                return nil
        },
}

var initCommand = cli.Command{
        Name:  "init",
        Usage: "Init container process run user's process in container. Do not call it outside",
        Action: func(context *cli.Context) error {
                log.Infof("init come on")
                cmd := context.Args().Get(0)
                log.Infof("command %s", cmd)
                err := container.RunContainerInitProcess(cmd, nil)
                return err
        },
}
  • runCommand 定义了 Flags ,其作用类似于运行命令时使用:指定参数;
  • Action: func是 run 命令执行的真正函数。
  1. 判断参数是否包含 command
  2. 获取用户指定的 command
  3. 调用 Run function 去准备启动容器
  • initCommand 的操作为内部方法,禁止外部调用
  1. 获取传递过来的 command 参数
  2. 执行容器初始化操作

container_process.go

package container
import (
        "syscall"
        "os/exec"
        "os"
)

func NewParentProcess(tty bool, command string) *exec.Cmd {
        args := []string{"init", command}
        cmd := exec.Command("/proc/self/exe", args...)
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
                syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
    }
        if tty {
                cmd.Stdin = os.Stdin
                cmd.Stdout = os.Stdout
                cmd.Stderr = os.Stderr
        }
        return cmd
}

这里是父进程,也就是当前进程执行的内容。

  1. 这里的/proc/self/exe 调用中,/proc/self/指的是当前运行进程自己的环境, exec 其实就是自己调用了自己,使用这种方式对创建出来的进程进行初始化;
  2. 后面的 args 是参数,其中 init 是传递给本进程的第1个参数,在本例中,其实就是会去调用 initCornmand去初始化进程的一些环境和资源;
  3. 下面的 clone 参数就是去 fork 出来一个新进程,并且使用了 namespace 隔离新创建的进程和外部环境;
  4. 如果用户指定了-ti 参数,就需要把当前进程的输入输出导入到标准输入输出上。

run.go

package main
import (
        "github.com/xianlubird/mydocker/container"
        log "github.com/Sirupsen/logrus"
        "os"
)

func Run(tty bool, command string) {
        parent := container.NewParentProcess(tty, command)
        if err := parent.Start(); err != nil {
                log.Error(err)
        }
        parent.Wait()
        os.Exit(-1)
}

这里的 Start 方法是真正开始前面创建好的 command 的调用,它首先会 clone 出来 namespace 隔离的进程,然后在子进程中,调用/proc/self/exe ,也就是调用自己,发送 init 参数,调用我们写的init 方法,去初始化容器的一些资源。

init.go

这里的 init 函数是在容器内部执行的,也就是说代码执行到这里后容器所在进程其实就已经创建出来了,这是本容器执行的第1个进程。
使用 mount 先去挂载 proc 文件系统,以便后面通过 ps 等系统命令去查看当前进程资源的情况。

package container

import (
        "os"
        "syscall"
        "github.com/Sirupsen/logrus"
)

func RunContainerInitProcess(command string, args []string) error {
        logrus.Infof("command %s", command)

        defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
        syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), "")
        argv := []string{command}
        if err := syscall.Exec(command, argv, os.Environ()); err != nil {
                logrus.Errorf(err.Error())
        }
        return nil
}

这里 MountFlag 意思如下:

  • MS_NOEXEC 在本文件系统中不允许运行其他程序;
  • MS_NOSUID 在本系统中运行程序 的时候,不允许set-user-ID或者set-group-ID;
  • MS_NODEV 这个参数是自Linux 2.4,所有的mount都会默认设定的参数。

本函数最后syscall.Exec实现系统调用,并将用户进程运行起来的操作:
首先,使用 Docker 建起来一个容器之后会发现容器内的第一个程序,也就是 PID为1的那个进程,是指定的前台进程。
容器创建之后,执行的第1个进程并不是用户的进程,而是 init 初始化的进程。
这时,如通过ps -ef命令查看就会发现,容器内第一个进程变成了自己 init,和预想的是不一 样的。你可能会想大不了,把这个进程给kill掉。但这里又有一个头疼的问题PID为1的进程是不能kill的,如果该进程被 kill 的容器也就不存在了。 那么这里 execve系统调用就可以大显神威了。
sysCall.Exec这个方法其实最终调用了 Kernel int execve(const char *filename, char *const argv[] , char *const envp[]) ;这个系统函数的作用是执行档期filename对应的程序。它会覆盖当前进程的镜像、数据、堆栈等信息,包括PID这些会被将要运行的进程覆盖掉。
也就是说,调用这个方法,将用户指定的进程运行起来,把最初的init进程给替换掉,这样当进入到容器内部的时候, 会发现容器内的第一个程序就是我们指定的进程了。这其实也是目Docker 使用的容器引擎 rune 实现方式之一。

测试

下面来将其编译运行 一下。

go build .
./mydocker run -ti /bin/sh
{"level":"info","msg":"init come on","time":"2020-07-21T16:07:25+08:00"}
{"level":"info","msg":"command /bin/sh","time":"2020-07-21T16:07:25+08:00"}
{"level":"info","msg":"command /bin/sh","time":"2020-07-21T16:07:25+08:00"}

ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 16:07 pts/0    00:00:00 /bin/sh
root         5     1  0 16:07 pts/0    00:00:00 ps -ef

ls
container  Godeps  main_command.go  main.go  mydocker  network  README.md  run.go  vendor

由于没有 chroot ,所以目前的系统文件系统是继承自父进程的。运行了一下 ls 命令,发现容器启动起来以后,打印出了当前目录的内容。

举报

相关推荐

0 条评论