自己动手写docker笔记(4)构造简单实现run命令版本的容器

原书代码

https://github.com/xianlubird/mydocker.git
#code-3.1

Linux Proc

Linux 下的/proc 文件系统是由内核提供,它其实不是一个真正的文件系统,只包含了系统运行时信息(比如系统内存,mount 设备信息,一些硬件配置等等,它只存在于内存中,而不占用外存空间。它是以文件系统的形式为访问内核数据的操作提供接口。
比如说lsmod就和cat /proc/modules是等效的

root@taroballs-PC:~# ls /proc/
1     1284  1524  1802  2103  31    430   514        cpuinfo      modules
10    1294  1525  1815  2144  312   4332  515        crypto       mounts
1005  13    1529  1818  2148  318   435   516        devices      mtrr
1030  1312  153   1826  22    32    436   517        diskstats    net
1031  1320  1530  1832  221   3237  437   518        dma          pagetypeinfo
1032  1346  154   1837  225   3241  438   525        driver       partitions

当你去遍历这个目录的时候会发现很多数字,这些都是为每个进程创建的空间,数字就是他们的 PID。

重要术语 相关说明
/proc/N pid为N的进程信息
/proc/N/cmdline 进程启动命令
/proc/N/cwd 链接到进程当前工作目录
/proc/N/environ 进程环境变量列表
/proc/N/exe 链接到进程的执行命令文件
/proc/N/fd 包含进程相关的所有的文件描述符
/proc/N/maps 与进程相关的内存映射信息
/proc/N/mem 指代进程持有的内存,不可读
/proc/N/root 链接到进程的根目录
/proc/N/stat 进程的状态
/proc/N/statm 进程使用的内存的状态
/proc/N/status 进程状态信息,比stat/statm更具可读性
/proc/self 链接到当前正在运行的进程

实现 run 命令

实现一个简单版本的run命令,类似docker run -ti [command]

代码目录结构如下:

root@taroballs-PC:~# tree  mydocker/ -L 2
mydocker/
├── container
│   ├── container_process.go
│   └── init.go
├── Godeps
│   ├── Godeps.json
│   └── Readme
├── main_command.go
├── main.go
├── run.go
└── vendor
    ├── github.com
    └── golang.org

首先分析下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
    //暂时定义两个命令init、run
    app.Commands = []cli.Command{
        initCommand,
        runCommand,
    }
    //`app.Before` 内初始化了一下`logrus`的日志配置。
    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)
    }
}

分别看下两条命令的具体定义,查看main_command.go文件

runcommand命令实现

//main_command.go
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",
        },
    },
    //这里是run命令执行的真正函数
    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)//调用Run方法去准备启动容器
        return nil
    },
}

先来看看Run函数做了些什么:

//run函数在run.go中

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)
}

解释一下:

让我们看一下NewParentProcess函数都写了些什么

//container/container_process.go 
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
}

解释一下:

接着看看返回cmd之后的调用:


查看InitCommand命令的具体实现

//main_command.go
//此方法为内部操作,禁止外部调用
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
    },
}

那么这里看看RunContainerInitProcess函数做了些什么

//container/init.go 
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 的系统都会默认设定的参数

解释一下

——————

运行一下:

#记得先在GOPATH准备两个包
git clone https://github.com/Sirupsen/logrus.git
git clone https://github.com/urfave/cli.git
#记得移动到GOPATH跑程序

Result

root@taroballs-PC:/home/taroballs/go/src/github.com/xianlubird/mydocker# ./mydocker run -ti /bin/sh
{"level":"info","msg":"init come on","time":"2018-02-01T00:47:22+08:00"}
{"level":"info","msg":"command /bin/sh","time":"2018-02-01T00:47:22+08:00"}
{"level":"info","msg":"command /bin/sh","time":"2018-02-01T00:47:22+08:00"}
# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 00:47 pts/0    00:00:00 /bin/sh
root         6     1  0 00:47 pts/0    00:00:00 ps -ef
# 

对比一下运行docker镜像容器

root@taroballs-PC:~# docker run -ti ubuntu /bin/sh
# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 16:51 pts/0    00:00:00 /bin/sh
root         5     1  0 16:51 pts/0    00:00:00 ps -ef
# 

是不是相类似呢?在运行个/bin/ls试试看

root@taroballs-PC:/home/taroballs/go/src/github.com/xianlubird/mydocker# ./mydocker run -ti /bin/ls
{"level":"info","msg":"init come on","time":"2018-02-01T00:54:45+08:00"}
{"level":"info","msg":"command /bin/ls","time":"2018-02-01T00:54:45+08:00"}
{"level":"info","msg":"command /bin/ls","time":"2018-02-01T00:54:45+08:00"}
container  main_command.go  mydocker  README.md  vendor
Godeps     main.go      network   run.go
root@taroballs-PC:/home/taroballs/go/src/github.com/xianlubird/mydocker# 
#由于我们没有`chroot`,所以目前我们的系统文件系统是继承自我们的父进程的,这里我们运行了一下`ls`命令,发现容器启动起来以后,打印出来了当前目录的内容,然后退出了.

推荐阅读更多精彩内容

  • linux资料总章2.1 1.0写的不好抱歉 但是2.0已经改了很多 但是错误还是无法避免 以后资料会慢慢更新 大...
    O感悟人生O阅读 10,344评论 2 33
  • 转载自:http://blog.csdn.net/hguisu/article/details/6122513原作...
    miaoiao阅读 1,159评论 0 7
  • 线性表 一 线性表特点 除第一个元素外,其他元素的前面都只有一个元素 除最后一个元素外,其他位置的数据后面都只有一...
    Carrism阅读 115评论 0 0
  • 我以为我是个心底藏着很多故事的人,但奈何无太多表达能力,所以只能掩藏于心,但我还是想把这样的我讲给你听……好了,先...
    遇见yj阅读 714评论 28 6