自己动手写Docker







一.容器简介

1.容器与虚拟机

  • 虚拟机:包含用户程序、必要的函数库、整个客户操作系统,所有这些差不多需要占用几个GB的空间
  • 容器::包含用户程序和所有的依赖,但是容器之间共享内核。各个容器在宿主机上相互隔离,在用户态下运行

2.容器的用途

  1. 加速开发效率
    • 不需要耗费大量时间设置开发环境
    • 隔离性使得开发者可以为自己的应用选择最好的语言和工具,不要担心产生内部工具的冲突
    • 将应用程序的配置和所有依赖打包成镜像,可以保证应用在任何环境中都能按预期来运行
  2. 合作开发
    • 使用Docker Hub管理分享镜像
  3. 快速扩容
    • Docker容器可以秒级启动和停止,因此,可以在需要的时候快速扩容出大量的应用程序,抗住并发的压力

3.Docker版本

Docker从1.13.x版本开始,版本分为企业版EE和社区版CE,版本号也改为按照时间线来发布。比如17.03就是2017年3月,有点类似于ubuntu的版本发布方式



二.基础技术

1.Namespace

  • kernel的一个功能
  • 可以隔离一系列系统资源:比如PID(Process ID),User ID, Network等等

当前Linux一共实现了6种不同类型的Namespace:

Namespace类型 系统调用参数 内核版本
Mount namespaces CLONE_NEWNS 2.4.19
UTS namespaces CLONE_NEWUTS 2.6.19
IPC namespaces CLONE_NEWIPC 2.6.19
PID namespaces CLONE_NEWPID 2.6.24
Network namespaces CLONE_NEWNET 2.6.29
User namespaces CLONE_NEWUSER 3.8

Namesapce 的API主要使用三个系统调用

  • clone():创建新进程。根据系统调用参数来判断哪种类型的namespace被创建,而且它们的子进程也会被包含到namespace中
  • unshare():将进程移出某个namespace
  • setns():将进程加入到namespace中

查看进程的namespace信息

 1#1.ls -l
 2[root@303f9007851f mnt]# ls -l /proc/1/ns/
 3total 0
 4lrwxrwxrwx 1 root root 0 Jul 16 08:05 cgroup -> cgroup:[4026531835]
 5lrwxrwxrwx 1 root root 0 Jul 16 08:05 ipc -> ipc:[4026532243]
 6lrwxrwxrwx 1 root root 0 Jul 16 08:05 mnt -> mnt:[4026532241]
 7lrwxrwxrwx 1 root root 0 Jul 16 08:05 net -> net:[4026532246]
 8lrwxrwxrwx 1 root root 0 Jul 16 08:05 pid -> pid:[4026532244]
 9lrwxrwxrwx 1 root root 0 Jul 16 08:05 user -> user:[4026531837]
10lrwxrwxrwx 1 root root 0 Jul 16 08:05 uts -> uts:[4026532242]
11
12#2.readlink
13[root@303f9007851f mnt]# readlink /proc/1/ns/cgroup
14cgroup:[4026531835]
15[root@303f9007851f mnt]# readlink /proc/1/ns/ipc
16ipc:[4026532243]
17[root@303f9007851f mnt]# readlink /proc/1/ns/mnt
18mnt:[4026532241]
19[root@303f9007851f mnt]# readlink /proc/1/ns/net
20net:[4026532246]
21[root@303f9007851f mnt]# readlink /proc/1/ns/pid
22pid:[4026532244]
23[root@303f9007851f mnt]# readlink /proc/1/ns/user
24user:[4026531837]
25[root@303f9007851f mnt]# readlink /proc/1/ns/uts
26uts:[4026532242]

1.1 UTS Namespace

UTS是UNIX Timesharing System的简称

主要隔离nodename和domainname两个系统标识。在UTS namespace里面,每个 namespace 允许有自己的主机名

以下程序执行的命令会创建一个新的UTS Namespace:

 1package main
 2
 3import (
 4    "os/exec"
 5    "syscall"
 6    "os"
 7    "log"
 8)
 9
10func main() {
11    cmd := exec.Command("sh") //指定执行的命令
12    //设置系统调用参数
13    cmd.SysProcAttr = &syscall.SysProcAttr{
14        Cloneflags: syscall.CLONE_NEWUTS,  //创建一个UTS Namespace
15    }
16    cmd.Stdin = os.Stdin
17    cmd.Stdout = os.Stdout
18    cmd.Stderr = os.Stderr
19
20    if err := cmd.Run(); err != nil {
21        log.Fatal(err)
22    }
23}

查看父子进程是否在同一个UTS Namespace中:

 1sh-4.1# pstree -lp
 2bash(1)───go(17)─┬─main(36)─┬─sh(40)───pstree(43)
 3                 │          ├─{main}(37)
 4                 │          ├─{main}(38)
 5                 │          └─{main}(39)
 6                 ├─{go}(18)
 7                 ├─{go}(19)
 8                 ├─{go}(20)
 9                 ├─{go}(21)
10                 └─{go}(22)
11sh-4.1# pstree -lp
12bash(1)───go(17)─┬─main(36)─┬─sh(40)───pstree(44)
13                 │          ├─{main}(37)
14                 │          ├─{main}(38)
15                 │          └─{main}(39)
16                 ├─{go}(18)
17                 ├─{go}(19)
18                 ├─{go}(20)
19                 ├─{go}(21)
20                 └─{go}(22)
21sh-4.1# echo $$
2240
23sh-4.1# readlink /proc/36/ns/uts
24uts:[4026532242]
25sh-4.1# readlink /proc/40/ns/uts
26uts:[4026532328]

测试在子进程中修改主机名是否会影响父进程:

1sh-4.1# hostname
2c74daf96aad0
3sh-4.1# hostname chenximing
4sh-4.1# hostname
5chenximing
6
7[root@c74daf96aad0 /]# hostname
8c74daf96aad0

1.2 IPC Namespace

IPC Namespace 是用来隔离 System V IPC 和POSIX message queues.每一个IPC Namespace都有他们自己的System V IPC 和POSIX message queue

以下程序执行的命令会创建一个新的IPC Namespace:

 1package main
 2
 3import (
 4    "log"
 5    "os"
 6    "os/exec"
 7    "syscall"
 8)
 9
10func main() {
11    cmd := exec.Command("sh") //指定执行的命令
12    //设置系统调用参数
13    cmd.SysProcAttr = &syscall.SysProcAttr{
14        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC,
15    }
16    cmd.Stdin = os.Stdin
17    cmd.Stdout = os.Stdout
18    cmd.Stderr = os.Stderr
19
20    if err := cmd.Run(); err != nil {
21        log.Fatal(err)
22    }
23}

测试父子进程是否拥有独立的IPC:

 1# 以下查看、然后在父进程中创建一个message queue
 2[root@c74daf96aad0 mnt]# ipcs -q
 3
 4------ Message Queues --------
 5key        msqid      owner      perms      used-bytes   messages
 6
 7[root@c74daf96aad0 mnt]# ipcmk -Q
 8Message queue id: 0
 9[root@c74daf96aad0 mnt]# ipcs -q
10
11------ Message Queues --------
12key        msqid      owner      perms      used-bytes   messages
130x58f3f100 0          root       644        0            0
14
15# 以下运行前面的go程序,子进程会创建新的IPC Namespace
16[root@c74daf96aad0 mnt]# go run main.go
17sh-4.1# ipcs -q
18
19------ Message Queues --------
20key        msqid      owner      perms      used-bytes   messages

1.3 PID Namespace

PID namespace是用来隔离进程 id。同样的一个进程在不同的 PID Namespace 里面可以拥有不同的 PID。这样就可以理解,在 docker container 里面,我们使用ps -ef 经常能发现,容器内在前台跑着的那个进程的 PID 是1,但是我们在容器外,使用ps -ef会发现同样的进程却有不同的 PID,这就是PID namespace 干的事情

以下程序执行的命令会创建一个新的PID Namespace:

 1package main
 2
 3import (
 4    "log"
 5    "os"
 6    "os/exec"
 7    "syscall"
 8)
 9
10func main() {
11    cmd := exec.Command("sh") //指定执行的命令
12    //设置系统调用参数
13    cmd.SysProcAttr = &syscall.SysProcAttr{
14        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,
15    }
16    cmd.Stdin = os.Stdin
17    cmd.Stdout = os.Stdout
18    cmd.Stderr = os.Stderr
19
20    if err := cmd.Run(); err != nil {
21        log.Fatal(err)
22    }
23}

查看相同子进程在不同PID Namespace中pid:

 1# 子进程sh在父进程PID Namespace中的pid为163
 2[root@c74daf96aad0 /]# pstree -pl
 3bash(1)───go(140)─┬─pid(159)─┬─sh(163)
 4                  │          ├─{pid}(160)
 5                  │          ├─{pid}(161)
 6                  │          └─{pid}(162)
 7                  ├─{go}(141)
 8                  ├─{go}(142)
 9                  ├─{go}(143)
10                  ├─{go}(144)
11                  └─{go}(145)
12
13# 子进程在新建的PID Namespace中的pid为1,如果没有创建新PID Namespace,这里会显示163
14sh-4.1# echo $$
151

1.4 Mount Namespace

mount namespace是Linux 第一个实现的namesapce 类型,因此它的系统调用参数是CLONE_NEWNS(new namespace 的缩写)

mount namespace 是用来隔离各个进程看到的挂载点视图。在不同namespace中的进程看到的文件系统层次是不一样的。在mount namespace 中调用mount()和umount()仅仅只会影响当前namespace内的文件系统,而对全局的文件系统是没有影响的

1.5 User Namesapce

User namespace 主要是隔离用户的用户组ID。也就是说,一个进程的User ID 和Group ID 在User namespace 内外可以是不同的。比较常用的是,在宿主机上以一个非root用户运行创建一个User namespace,然后在User namespace里面却映射成root 用户。这样意味着,这个进程在User namespace里面有root权限,但是在User namespace外面却没有root的权限。从Linux kernel 3.8开始,非root进程也可以创建User namespace ,并且此进程在namespace里面可以被映射成 root并且在 namespace内有root权限

以下程序执行的命令会创建一个新的User Namespace:

 1package main
 2
 3import (
 4    "log"
 5    "os"
 6    "os/exec"
 7    "syscall"
 8)
 9
10func main() {
11    cmd := exec.Command("sh") //指定执行的命令
12    //设置系统调用参数
13    cmd.SysProcAttr = &syscall.SysProcAttr{
14        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER,
15    }
16    cmd.Stdin = os.Stdin
17    cmd.Stdout = os.Stdout
18    cmd.Stderr = os.Stderr
19
20    if err := cmd.Run(); err != nil {
21        log.Fatal(err)
22    }
23}

查看当前用户在不同User Namespace下的id:

1[root@c74daf96aad0 mnt]# id
2uid=0(root) gid=0(root) groups=0(root)
3
4# 如果没有创建新的User Namespace,以下仍会是root
5[root@c74daf96aad0 mnt]# go run namespace/user.go
6sh-4.1$ id
7uid=65534 gid=65534 groups=65534

1.6 Network Namespace

Network namespace 是用来隔离网络设备,IP地址端口等网络栈的namespace。Network namespace 可以让每个容器拥有自己独立的网络设备(虚拟的),而且容器内的应用可以绑定到自己的端口,每个 namesapce 内的端口都不会互相冲突。在宿主机上搭建网桥后,就能很方便的实现容器之间的通信,而且每个容器内的应用都可以使用相同的端口

以下程序执行的命令会创建一个新的User Namespace:

 1import (
 2    "log"
 3    "os"
 4    "os/exec"
 5    "syscall"
 6)
 7
 8func main() {
 9    cmd := exec.Command("sh") //指定执行的命令
10    //设置系统调用参数
11    cmd.SysProcAttr = &syscall.SysProcAttr{
12        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET,
13    }
14    cmd.Stdin = os.Stdin
15    cmd.Stdout = os.Stdout
16    cmd.Stderr = os.Stderr
17
18    if err := cmd.Run(); err != nil {
19        log.Fatal(err)
20    }
21}

使用ifconfig可以查看到新的User Namespace中没有网络设备


2.Cgroups

Linux Cgroups(Control Groups) 提供了对一组进程及将来的子进程的资源的限制,控制和统计的能力,这些资源包括CPU,内存,存储,网络等。通过Cgroups,可以方便的限制某个进程的资源占用,并且可以实时的监控进程的监控和统计信息

2.1 Cgroups中的3个组件

  1. cgroup(控制组):控制组就是一组按照某种标准划分的进程。Cgroups 中的资源控制都是以控制族群为单位实现。一个进程可以加入到某个控制组,也从一个进程组迁 移到另一个控制组。一个进程组的进程可以使用 cgroups 以控制族群为单位分配的资源, 同时受到 cgroups 以控制组为单位设定的限制

  2. subsystem(子系统):一个子系统就是一个资源控制器。比如 cpu 子系统就是控制 cpu 时 间分配的一个控制器。子系统必须附加(attach)到一个hierarchy上才能起作用,一个子系统附 加到某个hierarchy以后,这个hierarchy上的所有控制族群都受到这个子系统的控制,一般包含有:

    • blkio 设置对块设备(比如硬盘)的输入输出的访问控制
    • cpu 设置cgroup中的进程的CPU被调度的策略
    • cpuacct 可以统计cgroup中的进程的CPU占用
    • cpuset 在多核机器上设置cgroup中的进程可以使用的CPU和内存(此处内存仅使用于NUMA架构)
    • devices 控制cgroup中进程对设备的访问
    • freezer 用于挂起(suspends)和恢复(resumes) cgroup中的进程
    • memory 用于控制cgroup中进程的内存占用
    • net_cls 用于将cgroup中进程产生的网络包分类(classify),以便Linux的tc(traffic controller) 可以根据分类(classid)区分出来自某个cgroup的包并做限流或监控。
    • net_prio 设置cgroup中进程产生的网络流量的优先级
    • ns 这个subsystem比较特殊,它的作用是cgroup中进程在新的namespace fork新进程(NEWNS)时,创建出一个新的cgroup,这个cgroup包含新的namespace中进程

    这些subsystem是逐步合并到内核中的,可以安装cgroup的命令行工具(apt-get install cgroup-bin),然后通过lssubsys看到kernel支持的subsystem

     1# -m同时列出子系统的挂载点
     2root@068ca8da6d06:/# lssubsys -m
     3cpuset /sys/fs/cgroup/cpuset
     4cpu /sys/fs/cgroup/cpu
     5cpuacct /sys/fs/cgroup/cpuacct
     6blkio /sys/fs/cgroup/blkio
     7memory /sys/fs/cgroup/memory
     8devices /sys/fs/cgroup/devices
     9freezer /sys/fs/cgroup/freezer
    10net_cls /sys/fs/cgroup/net_cls
    11perf_event /sys/fs/cgroup/perf_event
    12net_prio /sys/fs/cgroup/net_prio
    13hugetlb /sys/fs/cgroup/hugetlb
    14pids /sys/fs/cgroup/pids
    
  3. hierarchy(层级):控制族群可以组织成 hierarchical 的形式,既一颗控制族群树。控制族群树上的子节点控制族群是父节点控制族群的孩子,继承父控制族群的特定的属性

2.2 Cgroup文件系统

把cgroup文件系统挂载(mount)上以后,就可以像操作文件一样对cgroups的hierarchy进行浏览和操作管理(包括权限管理、子文件管理等等)

/sys/fs/cgroup是cgroup文件系统的挂载点

 1root@068ca8da6d06:/# df -a | grep cgroup
 2tmpfs            1023468       0   1023468   0% /sys/fs/cgroup
 3cpuset                 0       0         0    - /sys/fs/cgroup/cpuset
 4cpu                    0       0         0    - /sys/fs/cgroup/cpu
 5cpuacct                0       0         0    - /sys/fs/cgroup/cpuacct
 6blkio                  0       0         0    - /sys/fs/cgroup/blkio
 7memory                 0       0         0    - /sys/fs/cgroup/memory
 8devices                0       0         0    - /sys/fs/cgroup/devices
 9freezer                0       0         0    - /sys/fs/cgroup/freezer
10net_cls                0       0         0    - /sys/fs/cgroup/net_cls
11perf_event             0       0         0    - /sys/fs/cgroup/perf_event
12net_prio               0       0         0    - /sys/fs/cgroup/net_prio
13hugetlb                0       0         0    - /sys/fs/cgroup/hugetlb
14pids                   0       0         0    - /sys/fs/cgroup/pids
15cgroup                 0       0         0    - /sys/fs/cgroup/systemd
  • /sys/fs/cgroup下的每个目录及其子目录构成了一个hierarchy(层级)
  • 在每个root cgroup(根控制组)下可以创建目录,比如在blkio目录下新建一个docker,每个创建的目录就对应一个cgroup(控制组)
  • 每个root cgroup(根控制组)下的tasks文件包含了创建root cgroup(根控制组)时,系统中的所有进程。新建一个cgroup(控制组)时,新cgroup(控制组)中tasks文件中不包含任务


三.构造容器

1.proc文件系统

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

/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/N/self 当前正在运行的进程

2.mydocker文件分布结构

 1mydocker
 2   |------- container
 3   |            |-------- container_process.go  //包含了Run函数需要执行的NewParentProcess函数
 4   |            |-------- init.go  //包含了容器的init进程需要执行的RunContainerInitProcess函数
 5   |
 6   |------- Godeps
 7   |           |--------- Godeps.json
 8   |           |--------- Readme
 9   |
10   |------- cgroups
11   |           |-------- cgroup_manager.go      //包含了cgroup管理类的定义
12   |           |---------subsystems
13   |                         |
14   |                         |---- cpu.go       //包含了cpu子系统结构体的定义
15   |                         |---- cpuset.go    //包含了cpuset子系统结构体的定义
16   |                         |---- memory.go    //包含了memory子系统结构体的定义
17   |                         |---- subsystem.go //定义了用于cgroups的结构体和接口
18   |                         |---- utils.go     //包含了一些工具函数
19   |
20   |------- main_command.go   //包含了mydocker各种命令(run、list、exec等)的定义
21   |------- main.go           //程序入口
22   |------- run.go            //包含了run命令执行的Run函数的定义
23   |------- vendor

3.实现run命令

run命令的执行流程:

  1. mydocker run
  2. 执行runCommand命令(run命令)、解析命令行参数,调用Run函数
  3. Run函数调用NewParentProcess函数,创建一个执行自身的命令,传递给该命令一个字符串"init"参数(因此,该命令执行时,也就是执行mydocker init),同时,该命令会创建隔离的Namespace
  4. 完成3中的部分工作
  5. Run函数中,使用3和4返回的命令parent,通过parent.Start()函数来执行命令,也就是执行mydocker init,得到容器init进程,它会调用initCommand命令
  6. initCommand命令中会调用RunContainerInitProcess函数
  7. RunContainerInitProcess函数调用setUpMount函数,该函数会调用pivotRoot函数完成类型chroot的功能,然后挂载proc和tmpfs文件系统、RunContainerInitProcess函数最后调用syscall.Exec将容器init进程替换成用户指定的程序
  8. 创建完成,容器开始运行
  9. 5中调用parent.Start()函数后,进程会创建容器的cgroup,并使用CgroupManager将容器的cgroup限制信息写入配置文件,然后将容器的进程id添加到tasks文件中

1)main

 1func main() {
 2    app := cli.NewApp()
 3    app.Name = "mydocker"
 4    app.Usage = usage
 5
 6    // 定义mydocker的一些基本命令
 7    app.Commands = []cli.Command{
 8        initCommand,
 9        runCommand, //run命令
10        listCommand,
11        logCommand,
12        execCommand,
13        stopCommand,
14        removeCommand,
15        commitCommand,
16        networkCommand,
17    }
18
19    app.Before = func(context *cli.Context) error {
20        // Log as JSON instead of the default ASCII formatter.
21        log.SetFormatter(&log.JSONFormatter{})
22
23        log.SetOutput(os.Stdout)
24        return nil
25    }
26
27    if err := app.Run(os.Args); err != nil {
28        log.Fatal(err)
29    }
30}

2)runCommand

 1//run命令的定义
 2var runCommand = cli.Command{
 3    Name:  "run",
 4    Usage: `Create a container with namespace and cgroups limit ie: mydocker run -ti [image] [command]`,
 5    Flags: []cli.Flag{
 6        cli.BoolFlag{
 7            Name:  "ti",
 8            Usage: "enable tty",
 9        },
10        cli.BoolFlag{
11            Name:  "d",
12            Usage: "detach container",
13        },
14        cli.StringFlag{
15            Name:  "m",
16            Usage: "memory limit",
17        },
18        cli.StringFlag{
19            Name:  "cpushare",
20            Usage: "cpushare limit",
21        },
22        cli.StringFlag{
23            Name:  "cpuset",
24            Usage: "cpuset limit",
25        },
26        cli.StringFlag{
27            Name:  "name",
28            Usage: "container name",
29        },
30        cli.StringFlag{
31            Name:  "v",
32            Usage: "volume",
33        },
34        cli.StringSliceFlag{
35            Name:  "e",
36            Usage: "set environment",
37        },
38        cli.StringFlag{
39            Name:  "net",
40            Usage: "container network",
41        },
42        cli.StringSliceFlag{
43            Name:  "p",
44            Usage: "port mapping",
45        },
46    },
47    /*
48     * 以下为run命令执行的真正函数
49     */
50    Action: func(context *cli.Context) error {
51        if len(context.Args()) < 1 {
52            return fmt.Errorf("Missing container command")
53        }
54        var cmdArray []string
55        for _, arg := range context.Args() {
56            cmdArray = append(cmdArray, arg)
57        }
58
59        /*
60         * 解析参数
61         */
62
63        imageName := cmdArray[0]
64        cmdArray = cmdArray[1:]
65
66        createTty := context.Bool("ti")
67        detach := context.Bool("d")
68
69        if createTty && detach {
70            return fmt.Errorf("ti and d paramter can not both provided")
71        }
72        //cgroup配置参数
73        resConf := &subsystems.ResourceConfig{
74            MemoryLimit: context.String("m"),
75            CpuSet:      context.String("cpuset"),
76            CpuShare:    context.String("cpushare"),
77        }
78        log.Infof("createTty %v", createTty)
79        containerName := context.String("name")
80        volume := context.String("v")
81        network := context.String("net")
82
83        envSlice := context.StringSlice("e")
84        portmapping := context.StringSlice("p")
85
86        //调用Run函数
87        Run(createTty, cmdArray, resConf, containerName, volume, imageName, envSlice, network, portmapping)
88        return nil
89    },
90}

3)Run

 1func Run(tty bool, comArray []string, res *subsystems.ResourceConfig, containerName, volume, imageName string,
 2    envSlice []string, nw string, portmapping []string) {
 3    containerID := randStringBytes(10)
 4    if containerName == "" {
 5        containerName = containerID
 6    }
 7
 8    parent, writePipe := container.NewParentProcess(tty, containerName, volume, imageName, envSlice)
 9    if parent == nil {
10        log.Errorf("New parent process error")
11        return
12    }
13    //执行NewParentProcess返回的命令它首先会clone 出来一个Namespace 隔离的
14    //进程,然后在子进程中,调用 /proc/self/exe ,也就是调用自己,发送init 参数,
15    //调用我们写的init 方法,去初始化容器的一些资源。
16    if err := parent.Start(); err != nil {
17        log.Error(err)
18    }
19
20    //record container info
21    containerName, err := recordContainerInfo(parent.Process.Pid, comArray, containerName, containerID, volume)
22    if err != nil {
23        log.Errorf("Record container info error %v", err)
24        return
25    }
26
27    /*
28     * 下面由当前进程(容器的外部进程)设置容器的cgroup限制
29     */
30    
31    // CgroupManager对象管理容器的cgroup
32    cgroupManager := cgroups.NewCgroupManager(containerID)
33    defer cgroupManager.Destroy()
34    //调用set方法将容器的资源限制写入到3个子系统中容器cgroup下的配置文件中
35    cgroupManager.Set(res)
36    //将容器的进程id添加到3个子系统中容器cgroup下的tasks文件中
37    cgroupManager.Apply(parent.Process.Pid)
38
39    if nw != "" {
40        // config container network
41        network.Init()
42        containerInfo := &container.ContainerInfo{
43            Id:          containerID,
44            Pid:         strconv.Itoa(parent.Process.Pid),
45            Name:        containerName,
46            PortMapping: portmapping,
47        }
48        if err := network.Connect(nw, containerInfo); err != nil {
49            log.Errorf("Error Connect Network %v", err)
50            return
51        }
52    }
53
54    //对容器设置完限制之后,通过该函数将用户命令通过管道的写端发送给容器进程
55    sendInitCommand(comArray, writePipe)
56
57    if tty {
58        parent.Wait() //等待容器从终端结束
59        deleteContainerInfo(containerName)
60        container.DeleteWorkSpace(volume, containerName)
61    }
62}
63
64func sendInitCommand(comArray []string, writePipe *os.File) {
65    command := strings.Join(comArray, " ")
66    log.Infof("command all is %s", command)
67    writePipe.WriteString(command)
68    writePipe.Close()
69}

4)NewParentProcess

 1func NewParentProcess(tty bool, containerName, volume, imageName string, envSlice []string) (*exec.Cmd, *os.File) {
 2    readPipe, writePipe, err := NewPipe()
 3    if err != nil {
 4        log.Errorf("New pipe error %v", err)
 5        return nil, nil
 6    }
 7    //这里的 /proc/self/exe 调用中,/proc/self/ 指的是当前运行进程自己的环境, exec 其实就是自己
 8    //调用了自己,使用这种方式对创建出来的进程进行初始化
 9    initCmd, err := os.Readlink("/proc/self/exe")
10    fmt.Println(initCmd)
11    if err != nil {
12        log.Errorf("get init process error %v", err)
13        return nil, nil
14    }
15    //init 是传递给本进程的第一个参数,在本例中,其实就是会去调用initCommand
16    //去初始化进程的一些环境和资源
17    cmd := exec.Command(initCmd, "init")
18    cmd.SysProcAttr = &syscall.SysProcAttr{
19        //clone 参数指定了新进程需要创建哪些新的Namespace,即哪些环境需要隔离
20        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
21            syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
22    }
23    //如果用户指定了-ti 参数,就需要把当前进程的输入输出导入到标准输入输出上
24    if tty {
25        cmd.Stdin = os.Stdin
26        cmd.Stdout = os.Stdout
27        cmd.Stderr = os.Stderr
28    } else {
29        dirURL := fmt.Sprintf(DefaultInfoLocation, containerName)
30        if err := os.MkdirAll(dirURL, 0622); err != nil {
31            log.Errorf("NewParentProcess mkdir %s error %v", dirURL, err)
32            return nil, nil
33        }
34        stdLogFilePath := dirURL + ContainerLogFile
35        stdLogFile, err := os.Create(stdLogFilePath)
36        if err != nil {
37            log.Errorf("NewParentProcess create file %s error %v", stdLogFilePath, err)
38            return nil, nil
39        }
40        cmd.Stdout = stdLogFile
41    }
42
43    //在调用cmd命令时,会外带将管道的读句柄传递给子进程,因此容器内可以通过管道的读端从父进程读取信息
44    //外带是因为标准输入、标准输出、标准错误3个文件句柄是子进程一创建就有的
45    cmd.ExtraFiles = []*os.File{readPipe}
46
47    cmd.Env = append(os.Environ(), envSlice...)
48    NewWorkSpace(volume, imageName, containerName)
49    cmd.Dir = fmt.Sprintf(MntUrl, containerName)
50    return cmd, writePipe
51}

5)initCommand

 1//init命令的定义,此操作为内部方法,禁止外部调用
 2var initCommand = cli.Command{
 3    Name:  "init",
 4    Usage: "Init container process run user's process in container. Do not call it outside",
 5    Action: func(context *cli.Context) error {
 6        log.Infof("init come on")
 7        //调用container/init.go中的RunContainerInitProcess函数
 8        err := container.RunContainerInitProcess()
 9        return err
10    },
11}

6)RunContainerInitProcess

 1//这里的init 函数是在容器内部执行的,也就是说, 代码执行到这里后,容器所在的进程其实就已经创建出来了,
 2//这是本容器执行的第一个进程。使用mount 先去挂载proc 文件系统,以便后面通过ps 等系统命令去查看当前进
 3//程资源的情况。
 4func RunContainerInitProcess() error {
 5    cmdArray := readUserCommand() //容器进程会在此等待父进程传递来的用户命令
 6    if cmdArray == nil || len(cmdArray) == 0 {
 7        return fmt.Errorf("Run container get user command error, cmdArray is nil")
 8    }
 9
10    setUpMount()
11    //调用exec.LookPath ,可以在系统的PATH 里面寻找命令的绝对路径
12    //因此可以不必麻烦的将命令写完整。比如“docker run ... /bin/ls” 
13    //可以改成“docker run ... ls” 
14    path, err := exec.LookPath(cmdArray[0])
15    if err != nil {
16        log.Errorf("Exec loop path error %v", err)
17        return err
18    }
19    log.Infof("Find path %s", path)
20    if err := syscall.Exec(path, cmdArray[0:], os.Environ()); err != nil {
21        log.Errorf(err.Error())
22    }
23    return nil
24}
25
26func readUserCommand() []string {
27    //uintptr(3)就是指index为3 的文件描述符,也就是传递进来的管道的一端
28    pipe := os.NewFile(uintptr(3), "pipe")
29    defer pipe.Close()
30    //通过管道从父进程读取传递来的参数
31    msg, err := ioutil.ReadAll(pipe)
32    if err != nil {
33        log.Errorf("init read pipe error %v", err)
34        return nil
35    }
36    msgStr := string(msg)
37    return strings.Split(msgStr, " ")
38}

7)setUpMount

 1func setUpMount() {
 2    pwd, err := os.Getwd()
 3    if err != nil {
 4        log.Errorf("Get current location error %v", err)
 5        return
 6    }
 7    log.Infof("Current location is %s", pwd)
 8    pivotRoot(pwd)  //相当于chroot的功能
 9
10    //mount proc
11    //MS_NOEXEC在本文件系统中不允许允许其他程序
12    //MS_NOSUID在本系统中运行程序的时候,不允许ser-user-ID或set-group-ID
13    //MS_NODEV是自从Linux2.4以来,所有mount的系统都会都会默认设定的参数
14    defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
15    syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), "")
16
17    //mount tmpfs
18    syscall.Mount("tmpfs", "/dev", "tmpfs", syscall.MS_NOSUID|syscall.MS_STRICTATIME, "mode=755")
19}

8)pivotRoot

 1func pivotRoot(root string) error {
 2    /**
 3      为了使当前root的老 root 和新 root 不在同一个文件系统下,我们把root重新mount了一次
 4      bind mount是把相同的内容换了一个挂载点的挂载方法
 5    */
 6    if err := syscall.Mount(root, root, "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
 7        return fmt.Errorf("Mount rootfs to itself error: %v", err)
 8    }
 9    // 创建 rootfs/.pivot_root 存储 old_root
10    pivotDir := filepath.Join(root, ".pivot_root")
11    if err := os.Mkdir(pivotDir, 0777); err != nil {
12        return err
13    }
14    // pivot_root 到新的rootfs, 现在老的 old_root 是挂载在rootfs/.pivot_root
15    // 挂载点现在依然可以在mount命令中看到
16    if err := syscall.PivotRoot(root, pivotDir); err != nil {
17        return fmt.Errorf("pivot_root %v", err)
18    }
19    // 修改当前的工作目录到根目录
20    if err := syscall.Chdir("/"); err != nil {
21        return fmt.Errorf("chdir / %v", err)
22    }
23
24    pivotDir = filepath.Join("/", ".pivot_root")
25    // umount rootfs/.pivot_root
26    if err := syscall.Unmount(pivotDir, syscall.MNT_DETACH); err != nil {
27        return fmt.Errorf("unmount pivot_root dir %v", err)
28    }
29    // 删除临时文件夹
30    return os.Remove(pivotDir)
31}

4.增加容器资源限制

4.1 子系统限制资源

总的来说就是通过在子系统的层级下,使用文件操作来创建删除cgroup目录,读写配置文件进行控制

目前主要包括cpucpusetmemory三种限制,即实现了3个子系统,每个子系统实现了Subsystem接口,它要求子系统具有4个操作

 1type Subsystem interface {
 2    //返回子系统的名字
 3    Name() string
 4    //设置某个cgroup 在这个Subsystem 中的资源限制
 5    Set(path string, res *ResourceConfig) error
 6    //将迸程添加到某个cgroup 中
 7    Apply(path string, pid int) error
 8    //移除某个cgroup
 9    Remove(path string) error
10}
  1. Name:返回子系统的名字
  2. Set:在path表示的cgroup中,将res资源限制写入配置文件。资源限制使用结构体ResourceConfig记录
    1//用于传递资源限制配置的结构体
    2type ResourceConfig struct {
    3    MemoryLimit string
    4    CpuShare    string
    5    CpuSet      string
    6}
    
  3. Apply:将pid进程添加到path表示的cgroup
  4. Remove:将path表示的cgroup删除

每个子系统定义在mydocker/cgroups/subsystems目录下,如mydocker/cgroups/subsystems/memory.go中定义的memory子系统:

 1//子系统结构体并没有多余的成员,仅仅通过实现接口Subsystem的4个函数来实现继承
 2type MemorySubSystem struct {
 3}
 4
 5//设置cgroupPath对应的cgroup的内存资源限制
 6func (s *MemorySubSystem) Set(cgroupPath string, res *ResourceConfig) error {
 7    //GetCgroupPath获取相对于s.Name()子系统的cgroupPath cgroup的路径
 8    //如果cgroup不存在(即没有相应目录),GetCgroupPath可能创建cgroup,
 9    //GetCgroupPath函数的最后一个bool参数就是用于指示是否需要创建
10    if subsysCgroupPath, err := GetCgroupPath(s.Name(), cgroupPath, true); err == nil {
11        if res.MemoryLimit != "" {
12            //设置这个cgroup的内存限制,即将限制写入到cgroup对应目录的memory.limit_in_bytes文件中
13            if err := ioutil.WriteFile(path.Join(subsysCgroupPath, "memory.limit_in_bytes"), []byte(res.MemoryLimit), 0644); err != nil {
14                return fmt.Errorf("set cgroup memory fail %v", err)
15            }
16        }
17        return nil
18    } else {
19        return err
20    }
21
22}
23
24//删除cgroupPath对应的cgroup
25func (s *MemorySubSystem) Remove(cgroupPath string) error {
26    //GetCgroupPath获取相对于s.Name()子系统的cgroupPath cgroup的路径
27    //如果cgroup不存在(即没有相应目录),GetCgroupPath可能创建cgroup,
28    //GetCgroupPath函数的最后一个bool参数就是用于指示是否需要创建
29    if subsysCgroupPath, err := GetCgroupPath(s.Name(), cgroupPath, false); err == nil {
30        //调用RemoveAll删除目录
31        return os.RemoveAll(subsysCgroupPath)
32    } else {
33        return err
34    }
35}
36
37//将一个迸程加入到cgroupPath 对应的cgroup 中
38func (s *MemorySubSystem) Apply(cgroupPath string, pid int) error {
39    //GetCgroupPath获取相对于s.Name()子系统的cgroupPath cgroup的路径
40    //如果cgroup不存在(即没有相应目录),GetCgroupPath可能创建cgroup,
41    //GetCgroupPath函数的最后一个bool参数就是用于指示是否需要创建
42    if subsysCgroupPath, err := GetCgroupPath(s.Name(), cgroupPath, false); err == nil {
43        //将进程号写入到tasks文件中
44        if err := ioutil.WriteFile(path.Join(subsysCgroupPath, "tasks"), []byte(strconv.Itoa(pid)), 0644); err != nil {
45            return fmt.Errorf("set cgroup proc fail %v", err)
46        }
47        return nil
48    } else {
49        return fmt.Errorf("get cgroup %s error: %v", cgroupPath, err)
50    }
51}
52
53func (s *MemorySubSystem) Name() string {
54    return "memory"
55}

整个系统创建一个数组SubsystemsIns,该数组包含了cpucpusetmemory3个子系统:

1var (
2    SubsystemsIns = []Subsystem{
3        &CpusetSubSystem{},
4        &MemorySubSystem{},
5        &CpuSubSystem{},
6    }
7)

4.2 查找cgroup路径

在子系统实现设置cgroup的资源控制、将进程添加到cgroup、创建cgroup等操作中,都要先获取cgroup的路径,通过GetCgroupPath函数来完成

 1//GetCgroupPath获取相对于subsystem子系统的cgroupPath cgroup的路径
 2func GetCgroupPath(subsystem string, cgroupPath string, autoCreate bool) (string, error) {
 3    //找到子系统的根路径
 4    cgroupRoot := FindCgroupMountpoint(subsystem)
 5    //判断相对于子系统根路径的cgroupPath cgroup是否已经存在
 6    if _, err := os.Stat(path.Join(cgroupRoot, cgroupPath)); err == nil || (autoCreate && os.IsNotExist(err)) {
 7        if os.IsNotExist(err) {
 8            //cgroup不存在则通过创建一个目录来创建cgroup
 9            if err := os.Mkdir(path.Join(cgroupRoot, cgroupPath), 0755); err == nil {
10            } else {
11                return "", fmt.Errorf("error create cgroup %v", err)
12            }
13        }
14        //返回cgroup的路径
15        return path.Join(cgroupRoot, cgroupPath), nil
16    } else {
17        return "", fmt.Errorf("cgroup path error %v", err)
18    }
19}
20
21//该函数查看/proc/self/mountinfo文件,找到并返回子系统的根路径,即整个层级的根
22//对于memory来说,就是/sys/fs/cgroup/memory
23func FindCgroupMountpoint(subsystem string) string {
24    f, err := os.Open("/proc/self/mountinfo")
25    if err != nil {
26        return ""
27    }
28    defer f.Close()
29
30    scanner := bufio.NewScanner(f)
31    for scanner.Scan() {
32        txt := scanner.Text()
33        fields := strings.Split(txt, " ")
34        for _, opt := range strings.Split(fields[len(fields)-1], ",") {
35            if opt == subsystem {
36                return fields[4]
37            }
38        }
39    }
40    if err := scanner.Err(); err != nil {
41        return ""
42    }
43
44    return ""
45}

4.3 管理每个cgroup

每个cgroup通过一个CgroupManager类型的对象来管理

1type CgroupManager struct {
2    Path     string                     // cgroup相对于子系统根cgroup的路径
3    Resource *subsystems.ResourceConfig // 资源配置
4}

可以使用NewCgroupManager函数,通过传入cgroup的路径来创建一个管理该cgroup的对象:

1//根据cgroup的路径,创建一个关联这个cgroup的对象
2func NewCgroupManager(path string) *CgroupManager {
3    return &CgroupManager{
4        Path: path,
5    }
6}

CgroupManager实现了3个管理cgroup的方法,它们都是通过遍历3个子系统组成的数组,然后调用每个子系统的相应方法,实现在相应子系统中设置限制、将进程添加到cgroup的tasks文件中、从子系统删除该cgroup

感觉这种实现是,每个cgroup同时关联到3个子系统,要么cgroup同时存在于3个子系统,要么同时不存在

 1//将进程pid加入到这个cgroup中
 2func (c *CgroupManager) Apply(pid int) error {
 3    for _, subSysIns := range subsystems.SubsystemsIns {
 4        subSysIns.Apply(c.Path, pid)
 5    }
 6    return nil
 7}
 8
 9//设置cgroup资源限制
10func (c *CgroupManager) Set(res *subsystems.ResourceConfig) error {
11    for _, subSysIns := range subsystems.SubsystemsIns {
12        subSysIns.Set(c.Path, res)
13    }
14    return nil
15}
16
17//释放cgroup
18func (c *CgroupManager) Destroy() error {
19    for _, subSysIns := range subsystems.SubsystemsIns {
20        if err := subSysIns.Remove(c.Path); err != nil {
21            logrus.Warnf("remove cgroup fail %v", err)
22        }
23    }
24    return nil
25}

4.4 带资源限制的容器

CgroupManager在配置容器资源限制时,首先会初始化子系统的实例(3个子系统组成的数组?),然后遍历调用子系统实例的set方法,创建和配置不同子系统挂载的层级中cgroup,最后再通过调用子系统实例的Apply方法将容器的进程分别加入到那些cgroup中,实现容器中的资源限制

具体步骤在run命令执行的Run函数



四.构造镜像

1.联合挂载与volume挂载

使用tar格式的镜像文件,挂载时解压镜像到一层镜像层目录,然后创建容器的读写层目录,最后创建容器的挂载目录,调用mount将镜像层和容器层进行联合挂载

整个联合挂载通过函数 NewWorkSpace 完成

 1//Create a AUFS filesystem as container root workspace
 2func NewWorkSpace(volume, imageName, containerName string) {
 3    CreateReadOnlyLayer(imageName)
 4    CreateWriteLayer(containerName)
 5    CreateMountPoint(containerName, imageName)
 6    if volume != "" {
 7        volumeURLs := strings.Split(volume, ":")
 8        length := len(volumeURLs)
 9        if length == 2 && volumeURLs[0] != "" && volumeURLs[1] != "" {
10            MountVolume(volumeURLs, containerName)
11            log.Infof("NewWorkSpace volume urls %q", volumeURLs)
12        } else {
13            log.Infof("Volume parameter input is not correct.")
14        }
15    }
16}

它又调用了下面4个函数:

  • 处理镜像层和容器层
    • CreateReadOnlyLayer:解压tar镜像到镜像层目录
    • CreateWriteLayer:创建容器的读写层目录
    • CreateMountPoint:创建容器的挂载目录,进行联合挂载
  • 处理volume卷
    • MountVolume

以tar格式的busybox镜像为例,容器文件系统的创建流程如下图:

下图是volume的挂载流程,volumeUrlExtract函数实现在了NewWorkSpace函数中:

1)处理镜像层和容器层

 1/root
 2  |
 3  |-------/镜像1      #镜像层
 4  |-------/镜像2      #镜像层
 5  |--------镜像3.tar  #原始镜像
 6  |--------...
 7  |
 8  |-------/writeLayer
 9  |           |
10  |           |--------/容器1  #读写层
11  |           |--------/容器2  #读写层
12  |           |--------...
13  |
14  |------/mnt    
15  |        |
16  |        |----------/容器1  #联合挂点点
17  |        |----------/容器2  #联合挂载点
 1//解压tar镜像到/root/imageName/目录,以该目录作为容器的只读层(镜像层)
 2func CreateReadOnlyLayer(imageName string) error {
 3    //RootUrl:/root
 4    unTarFolderUrl := RootUrl + "/" + imageName + "/" //解压目录
 5    imageUrl := RootUrl + "/" + imageName + ".tar"    //待解压的镜像压缩包
 6    exist, err := PathExists(unTarFolderUrl)
 7    if err != nil {
 8        log.Infof("Fail to judge whether dir %s exists. %v", unTarFolderUrl, err)
 9        return err
10    }
11    if !exist { //如果以前没有解压过(即使用过)该镜像
12        if err := os.MkdirAll(unTarFolderUrl, 0622); err != nil {
13            log.Errorf("Mkdir %s error %v", unTarFolderUrl, err)
14            return err
15        }
16        //调用tar解压
17        if _, err := exec.Command("tar", "-xvf", imageUrl, "-C", unTarFolderUrl).CombinedOutput(); err != nil {
18            log.Errorf("Untar dir %s error %v", unTarFolderUrl, err)
19            return err
20        }
21    }
22    return nil
23}
24
25//创建目录/root/writeLayer/containerName,作为容器的读写层(容器层)
26func CreateWriteLayer(containerName string) {
27    //WriteLayerUrl: /root/writeLayer/%s
28    writeURL := fmt.Sprintf(WriteLayerUrl, containerName)
29    if err := os.MkdirAll(writeURL, 0777); err != nil {
30        log.Infof("Mkdir write layer dir %s error. %v", writeURL, err)
31    }
32}
33
34//创建容器的挂载目录,调用mount进行挂载
35func CreateMountPoint(containerName, imageName string) error {
36    //MntUrl:/root/mnt/%s
37    mntUrl := fmt.Sprintf(MntUrl, containerName)
38    if err := os.MkdirAll(mntUrl, 0777); err != nil {
39        log.Errorf("Mkdir mountpoint dir %s error. %v", mntUrl, err)
40        return err
41    }
42    tmpWriteLayer := fmt.Sprintf(WriteLayerUrl, containerName) //读写层目录
43    tmpImageLocation := RootUrl + "/" + imageName              //镜像层目录
44    mntURL := fmt.Sprintf(MntUrl, containerName)               //挂载目录
45    dirs := "dirs=" + tmpWriteLayer + ":" + tmpImageLocation   //联合挂载目录字符串
46    //执行挂载,aufs
47    _, err := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", mntURL).CombinedOutput()
48    if err != nil {
49        log.Errorf("Run command for creating mount point failed %v", err)
50        return err
51    }
52    return nil
53}

2)处理volume卷

 1func MountVolume(volumeURLs []string, containerName string) error {
 2    //volume目录
 3    parentUrl := volumeURLs[0]
 4    //创建卷目录
 5    if err := os.Mkdir(parentUrl, 0777); err != nil {
 6        log.Infof("Mkdir parent dir %s error. %v", parentUrl, err)
 7    }
 8    //volume在容器内的挂载点
 9    containerUrl := volumeURLs[1]
10    //MntUrl:/root/mnt/%s
11    mntURL := fmt.Sprintf(MntUrl, containerName)
12    //挂载点的绝对路径
13    containerVolumeURL := mntURL + "/" + containerUrl
14    //创建挂载点的目录
15    if err := os.Mkdir(containerVolumeURL, 0777); err != nil {
16        log.Infof("Mkdir container dir %s error. %v", containerVolumeURL, err)
17    }
18    dirs := "dirs=" + parentUrl
19    //调用mount进行挂载,这里也是使用aufs,由于parentUrl只包含volume一个目录,所以是读写挂载
20    _, err := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", containerVolumeURL).CombinedOutput()
21    if err != nil {
22        log.Errorf("Mount volume failed. %v", err)
23        return err
24    }
25    return nil
26}

2.volume卸载与容器文件系统卸载

容器停止时会卸载容器文件系统,在mydocker的实现中,卸载完成后同时删除容器的挂载目录和读写层目录。主要通过 DeleteWorkSpace 函数完成:

 1//Delete the AUFS filesystem while container exit
 2func DeleteWorkSpace(volume, containerName string) {
 3    if volume != "" {
 4        volumeURLs := strings.Split(volume, ":")
 5        length := len(volumeURLs)
 6        if length == 2 && volumeURLs[0] != "" && volumeURLs[1] != "" {
 7            DeleteVolume(volumeURLs, containerName)
 8        }
 9    }
10    DeleteMountPoint(containerName)
11    DeleteWriteLayer(containerName)
12}

基本上和联合挂载是一个相反的过程。它又调用了下面3个函数:

  • 处理volume卷
    • DeleteVolume
  • 处理镜像层和容器层
    • DeleteMountPoint:调用umount卸载已挂载到容器挂载目录下的联合文件系统,然后删除容器的挂载目录
    • DeleteWriteLayer:删除容器的读写(层)目录

1)处理volume卷

1func DeleteVolume(volumeURLs []string, containerName string) error {
2    mntURL := fmt.Sprintf(MntUrl, containerName)
3    containerUrl := mntURL + "/" + volumeURLs[1]
4    if _, err := exec.Command("umount", containerUrl).CombinedOutput(); err != nil {
5        log.Errorf("Umount volume %s failed. %v", containerUrl, err)
6        return err
7    }
8    return nil
9}

2)处理镜像层和容器层

 1//调用umount卸载已挂载到容器挂载目录下的联合文件系统,然后删除容器的挂载目录
 2func DeleteMountPoint(containerName string) error {
 3    mntURL := fmt.Sprintf(MntUrl, containerName)
 4    //调用umount卸载已挂载到容器挂载目录下的联合文件系统
 5    _, err := exec.Command("umount", mntURL).CombinedOutput()
 6    if err != nil {
 7        log.Errorf("Unmount %s error %v", mntURL, err)
 8        return err
 9    }
10    //删除容器的挂载目录
11    if err := os.RemoveAll(mntURL); err != nil {
12        log.Errorf("Remove mountpoint dir %s error %v", mntURL, err)
13        return err
14    }
15    return nil
16}
17
18//删除容器的读写(层)目录
19func DeleteWriteLayer(containerName string) {
20    ////WriteLayerUrl: /root/writeLayer/%s
21    writeURL := fmt.Sprintf(WriteLayerUrl, containerName)
22    if err := os.RemoveAll(writeURL); err != nil {
23        log.Infof("Remove writeLayer dir %s error %v", writeURL, err)
24    }
25}

3.简单的镜像打包

镜像打包通过commit命令完成:

 1var commitCommand = cli.Command{
 2    Name:  "commit",
 3    Usage: "commit a container into image",
 4    Action: func(context *cli.Context) error {
 5        if len(context.Args()) < 2 {
 6            return fmt.Errorf("Missing container name and image name")
 7        }
 8        containerName := context.Args().Get(0)      //需要打包成镜像的容器名
 9        imageName := context.Args().Get(1)          //打包成的镜像名
10        commitContainer(containerName, imageName)   //调用该函数打包
11        return nil
12    },
13}

commit命令通过commitContainer函数实现打包:

 1func commitContainer(containerName, imageName string){
 2    //container.MntUrl:/root/mnt/%s
 3    mntURL := fmt.Sprintf(container.MntUrl, containerName)
 4    mntURL += "/"
 5
 6    //container.RootUrl:/root
 7    //打包成的镜像的路径
 8    imageTar := container.RootUrl + "/" + imageName + ".tar"
 9
10    //调用tar命令打包,由于是对容器的挂载目录进行打包,所以镜像包含了容器层和镜像层
11    if _, err := exec.Command("tar", "-czf", imageTar, "-C", mntURL, ".").CombinedOutput(); err != nil {
12        log.Errorf("Tar folder %s error %v", mntURL, err)
13    }
14}