后浪笔记一零二四

docker引擎的架构图:

                          DockerEngine
                                |
                           containerd
             ___________________|____________________
             |                  |                   |
      containerd-shim   containerd-shim            ...
             |                  |                   |
           runC               runC                 ...
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ ps -ef | grep dockerd
root       971     1  2 22:23 ?        00:00:13 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root     17561  3669  0 22:33 pts/0    00:00:00 grep --color=auto dockerd
$ ps -ef | grep ' 1 .*/usr/bin/containerd'
root       968     1  0 22:23 ?        00:00:01 /usr/bin/containerd
root     17432  3669  0 22:33 pts/0    00:00:00 grep --color=auto  1 .*/usr/bin/containerd
$ docker run -d busybox sleep 1000
$ pstree -l -a -A 968
containerd
  |-containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/92ab2ef6eed5fb92ef2e2fdc17143691301bd6661044db9e7df53495384eab2c -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc -systemd-cgroup
  |   |-weed volume -mserver=master:9333 -port=8080 -ip 172.18.0.2 -dir /data
  |   |   `-8*[{weed}]
  |   `-9*[{containerd-shim}]
  ...
  |-containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/0ae7d921d48567b095a449bd7bcb69d75103cc25ea3a5370304a1426d27e1c25 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc -systemd-cgroup
  |   |-sleep 1000
  |   `-9*[{containerd-shim}]
  `-41*[{containerd}]

注意,容器的"单进程模型",并不是指容器里只能运行"一个"进程,而是指容器没有管理多个进程的能力。 这是因为容器里PID=1的进程就是应用本身,其他的进程都是这个PID=1进程的子进程。 可是,用户编写的应用(容器内PID=1的进程),并不能够像正常操作系统里的init进程或者systemd那样拥有进程管理的功能。

1. 实现容器的后台运行

容器,在操作系统看来,其实就是一个进程。当前运行命令的mydocker是主进程,容器是被当做mydocker进程fork出来的子进程。 子进程的结束和父进程的运行是一个异步的过程,即父进程永远不知道子进程到底什么时候结束。如果创建子进程的父进程退出, 那么这个子进程就成了没人管的孩子,俗称孤儿进程。为了避免孤儿进程退出时无法释放所占用的资源而僵死,进程号为1的进程 init就会接受这些孤儿进程。这就是父进程退出而容器进程依然运行的原理。

首先,需要给run子命令添加一个-d标签(bool类型),表示这个容器启动的时候以daemon的模式运行。注意,-d和-it不能共存的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 这也是d这个参数唯一被用到的地方
if tty && d {
        fmt.Fatal("it and d parameter can not both provided")
}

...

// 0test.go第297行, 从直接执行cmd.Wait()改为只有tty为true时才执行
// 注意之前在cmd.Wait()之后的代码,都必须在tty为true后,同步执行
// cmd.Wait()
if tty {
        cmd.Wait()

        // 3. 改动:
        // docker会在删除容器的时候,把容器对应的Write Layer和Container-init Layer删除,而保留镜像所有的内容
        // 在这里,我们简化为只删除Write Layer (Container-init的处理,非常复杂)
        DeleteWorkSpace(rootURL, mntURL, v)

        // 在容器退出的时候,同步删除容器信息文件
        deleteContainerInfo(containerName)

}
...
// os.Exit(-1)
os.Exit(0)

cmd.Wait()主要是用于父进程等待子进程结束,这在交互式创建容器的步骤里面是没有问题的,但是在这里,如果detach创建了容器, 就不能再去等待,创建容器之后,父进程就已经退出了。因此,这里只是将容器内的init进程启动起来,就已经完成工作,紧接着就 可以退出,然后由操作系统进程ID为1的init进程去接管容器进程。

实现分离的模式运行容器之后,会出现一个问题: 使用一个镜像启动的多个容器会使用同一个overlayFS文件系统,容器的可写层会互相影响。所以,这里要实现如下两个目的:

  • 为每个容器分配单独的隔离文件系统
  • 修改mydocker commit命令,实现对不同容器进行打包镜像的功能

修改test/0test.go

  1. 修改MntURL和WriteLayerUrl和WorkLayerUrl
1
2
3
MntURL = RootURL + "%s/mnt/"
WriteLayerUrl= RootURL +"%s/diff/"
WorkLayerUrl= RootURL +"%s/work/"
  1. 给DeleteWorkSpace函数添加imageName stringcontainerName string这两个参数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
func DeleteWorkSpace(volume, containerName string) {
        if volume != "" {
                volumeURLs := strings.Split(volume, ":")
                length := len(volumeURLs)
                if length == 2 && volumeURLs[0] != "" && volumeURLs[1] != "" {
                        DeleteMountPointWithVolume(volumeURLs, containerName)
                } else {
                        DeleteMountPoint(containerName)
                }
        } else {
                DeleteMountPoint(containerName)
        }
        DeleteWriteLayer(containerName)
}
func DeleteMountPoint(containerName string) error {
        mntURL := fmt.Sprintf(MntUrl, containerName)
        _, err := exec.Command("umount", mntURL).CombinedOutput()
        if err != nil {
                fmt.Printf("Unmount %s error %v\n", mntURL, err)
                return err
        }
        if err := os.RemoveAll(mntURL); err != nil {
                fmt.Printf("Remove mountpoint dir %s error %v\n", mntURL, err)
                return err
        }
        return nil
}
func DeleteMountPointWithVolume(volumeURLs []string, containerName string) error {
        mntURL := fmt.Sprintf(MntUrl, containerName)
        containerUrl := mntURL + "/" +  volumeURLs[1]
        if _, err := exec.Command("umount", containerUrl).CombinedOutput(); err != nil {
                fmt.Printf("Umount volume %s failed. %v\n", containerUrl, err)
                return err
        }

        if _, err := exec.Command("umount", mntURL).CombinedOutput(); err != nil {
                fmt.Printf("Umount mountpoint %s failed. %v\n", mntURL, err)
                return err
        }

        if err := os.RemoveAll(mntURL); err != nil {
                fmt.Printf("Remove mountpoint dir %s error %v\n", mntURL, err)
                return err
        }

        return nil
}
func DeleteWriteLayer(containerName string) {
        writeURL := fmt.Sprintf(WriteLayerUrl, containerName)
        writeURL := strings.TrimRight(writeURL, "diff/")
        if err := os.RemoveAll(writeURL); err != nil {
                log.Fatal("Remove writeLayer dir %s error %v", writeURL, err)
        }
}

至此,便完成了为每个容器分配单独的隔离文件系统的工作,下面介绍对commit子命令的改造:

if len(os.Args) < 4 {
        println("Missing container name and image name")  
        os.Exit(-1)
}
commitContainer(os.Args[2], os.Args[3])

接下来修改commitContainer函数,实际根据传入的containerName制作imageName.tar镜像:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func commitContainer(containerName, imageName string){
        mntURL := fmt.Sprintf(MntUrl, containerName)
        mntURL += "/"

        imageTar := RootUrl + "/" + imageName + ".tar"

        if _, err := exec.Command("tar", "-czf", imageTar, "-C", mntURL, ".").CombinedOutput(); err != nil {
                log.Errorf("Tar folder %s error %v", mntURL, err)
        }
}
  1. 给NewWorkSpace函数添加imageName stringcontainerName string这两个参数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
func NewWorkSpace(volume, imageName, containerName string) {
        CreateReadOnlyLayer(imageName)
        CreateWriteLayer(containerName)
        CreateMountPoint(containerName, imageName)
        if volume != "" {
                volumeURLs := strings.Split(volume, ":")
                length := len(volumeURLs)
                if length == 2 && volumeURLs[0] != "" && volumeURLs[1] != "" {
                        MountVolume(volumeURLs, containerName)
                        log.Infof("NewWorkSpace volume urls %q", volumeURLs)
                } else {
                        log.Infof("Volume parameter input is not correct.")
                }
        }
}
// 解压tar格式的镜像文件作为只读层
// CreateReadOnlyLayer函数的作用是根据用户输入的镜像为每个容器创建只读层
// 将CreateReadOnlyLayer函数的参数修改为imageName,镜像解压出来的只读层以RootUrl + imageName命名
func CreateReadOnlyLayer(imageName string) error {
        unTarFolderUrl := RootUrl + "/" + imageName + "/"
        imageUrl := RootUrl + "/" + imageName + ".tar"
        exist, err := PathExists(unTarFolderUrl)
        if err != nil {
                fmt.Printf("Fail to judge whether dir %s exists. %v\n", unTarFolderUrl, err)
                return err
        }
        if !exist {
                if err := os.MkdirAll(unTarFolderUrl, 0777); err != nil {
                        fmt.Printf("Mkdir %s error %v\n", unTarFolderUrl, err)
                        return err
                }

                if _, err := exec.Command("tar", "-xvf", imageUrl, "-C", unTarFolderUrl).CombinedOutput(); err != nil {
                        fmt.Printf("Untar dir %s error %v", unTarFolderUrl, err)
                        return err
                }
        }
        return nil
}
// CreateWriteLayer函数的作用是为每个容器创建一个读写层。将CreateWriteLayer函数的参数修改成containerName,
// 容器的读写层修改成以WriteLayerUrl + containerName命名
func CreateWriteLayer(containerName string) {
        writeURL := fmt.Sprintf(WriteLayerUrl, containerName)
        if err := os.MkdirAll(writeURL, 0777); err != nil {
                log.Infof("Mkdir write layer dir %s error. %v", writeURL, err)
        }
}
// CreateMountPoint函数的作用是创建容器的根目录,然后把镜像只读层和容器读写层挂载到容器根目录,成为容器的文件系统。
// CreateMountPoint函数的参数列表改为containerName和imageName。把通过镜像压缩出来的只读写和容器的可读写层用
// overlayFS联合挂载成为容器的文件系统
func CreateMountPoint(containerName , imageName string) error {
        mntUrl := fmt.Sprintf(MntUrl, containerName)
        if err := os.MkdirAll(mntUrl, 0777); err != nil {
                log.Errorf("Mkdir mountpoint dir %s error. %v", mntUrl, err)
                return err
        }
        // lowerdir
        tmpImageLocation := RootUrl + "/" + imageName
        // upperdir
        tmpWriteLayer := fmt.Sprintf(WriteLayerUrl, containerName)
        // workdir
        workLayer := fmt.Sprintf(WorkLayerUrl, containerName)
        // 联合挂载的最终结果目录
        mntURL := fmt.Sprintf(MntUrl, containerName)
        _, err := exec.Command("mount", "-t", "overlay", "-o", fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", tmpImageLocation, tmpWriteLayer, workLayer), "none", mntURL).CombinedOutput()
        if err != nil {
                log.Errorf("Run command for creating mount point failed %v", err)
                return err
        }
        return nil
}
// MountVolume函数的作用是根据用户输入的volume参数获取相应要挂载的宿主机数据卷URL和容器中的挂载点URL,
// 并挂载数据卷。MountVolume函数的参数列表改为volumeURLs和containerName,容器的挂载点改为以MntUrl + containerName + containerUrl命名
func MountVolume(volumeURLs []string, containerName string) error {
        // 创建宿主机文件目录
        parentUrl := volumeURLs[0]
        if err := os.Mkdir(parentUrl, 0777); err != nil {
                fmt.Printf("Mkdir parent dir %s error. %v\n", parentUrl, err)
        }
        // 在容器文件系统里创建挂载点
        containerUrl := volumeURLs[1]
        mntURL := fmt.Sprintf(MntUrl, containerName)
        containerVolumeURL := mntURL + "/" +  containerUrl
        if err := os.Mkdir(containerVolumeURL, 0777); err != nil {
                fmt.Printf("Mkdir container dir %s error. %v\n", containerVolumeURL, err)
        }
        workLayer := fmt.Sprintf(WorkLayerUrl, containerName)
        _, err := exec.Command("mount", "-t", "overlay", "-o", fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s",parentUrl, parentUrl, workLayer), "none", containerVolumeURL).CombinedOutput()
        if err != nil {
                fmt.Pritnf("Mount volume failed. %v\n", err)
                return err
        }
        return nil
}

2. 实现mydocker ps的功能

首先,添加一个名叫"ps"的子命令,并且,当子命令为"ps"时,就直接执行下面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import (
        ...
        "text/tabwriter"
        ...
)

func ListContainers() {
        dirURL := fmt.Sprintf(container.DefaultInfoLocation, "")
        dirURL = dirURL[:len(dirURL)-1]
        files, err := ioutil.ReadDir(dirURL)
        if err != nil {
                log.Errorf("Read dir %s error %v", dirURL, err)
                return
        }

        var containers []*container.ContainerInfo
        for _, file := range files {
                if file.Name() == "network" {
                        continue
                }
                tmpContainer, err := getContainerInfo(file)
                if err != nil {
                        log.Errorf("Get container info error %v", err)
                        continue
                }
                containers = append(containers, tmpContainer)
        }

        w := tabwriter.NewWriter(os.Stdout, 12, 1, 3, ' ', 0)
        fmt.Fprint(w, "ID\tNAME\tPID\tSTATUS\tCOMMAND\tCREATED\n")
        for _, item := range containers {
                fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
                        item.Id,
                        item.Name,
                        item.Pid,
                        item.Status,
                        item.Command,
                        item.CreatedTime)
        }
        if err := w.Flush(); err != nil {
                log.Errorf("Flush error %v", err)
                return
        }
}
func getContainerInfo(file os.FileInfo) (*container.ContainerInfo, error) {
        containerName := file.Name()
        configFileDir := fmt.Sprintf(container.DefaultInfoLocation, containerName)
        configFileDir = configFileDir + container.ConfigName
        content, err := ioutil.ReadFile(configFileDir)
        if err != nil {
                log.Errorf("Read file %s error %v", configFileDir, err)
                return nil, err
        }
        var containerInfo container.ContainerInfo
        if err := json.Unmarshal(content, &containerInfo); err != nil {
                log.Errorf("Json unmarshal error %v", err)
                return nil, err
        }

        return &containerInfo, nil
}

3. 实现查看容器日志

一般来说,对于容器中运行的进程,使日志达到标准输出是一个非常好的实现方案,因此需要将容器中的标准输出保存下来,以便需要的时候访问。这就是mydocker log命令的实现原理。

我们会将容器进程的标准输出挂载到"/var/run/mydocker/容器名/container.log"文件中,这样就可以在调用mydocker logs的时候去读取这个文件,并将进程内的标准输出打印出来。

首先需要修改0test.go文件,将容器进程的标准输出重定向一下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 添加全局变量
const (
        ...
        ContainerLogFile    string = "container.log"
        ...
)

// 0test.go 第240行, 添加else语句,当tty为false的时候,就执行重定向操作
else {
        // 生成容器对应目录的container.log文件
        dirURL := fmt.Sprintf(DefaultInfoLocation, containerName)
        if err := os.MkdirAll(dirURL, 0622); err != nil {
                log.Fatal("NewParentProcess mkdir %s error %v", dirURL, err)
        }
        stdLogFilePath := dirURL + ContainerLogFile
        stdLogFile, err := os.Create(stdLogFilePath)
        if err != nil {
                log.Fatal("NewParentProcess create file %s error %v", stdLogFilePath, err)
        }
        cmd.Stdout = stdLogFile
}

然后添加一个名为"logs"的子命令,并添加一个参数(会有参数校验),并直接执行查询日志的函数:

1
2
3
4
if len(cl.Args()) < 1 {
        log.Fatal("Please input your container name")
}
logContainer(cl.Args()[1])

logContainer的实现非常简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func logContainer(containerName string) {
        dirURL := fmt.Sprintf(DefaultInfoLocation, containerName)
        logFileLocation := dirURL + ContainerLogFile
        file, err := os.Open(logFileLocation)
        defer file.Close()
        if err != nil {
                log.Fatal("Log container open file %s error %v", logFileLocation, err)
        }
        content, err := ioutil.ReadAll(file)
        if err != nil {
                log.Fatal("Log container read file %s error %v", logFileLocation, err)
        }
        fmt.Fprint(os.Stdout, string(content))
}

4. 实现进入容器Namespace(mydocker exec命令)

setns是一个系统调用,可以根据提供的PID再次进入到指定的Namespace中。它需要先打开/proc/[pid]/ns/文件夹下对应的文件, 然后使当前进程进入到指定的Namespace中。但是对于Mount Namespace来说,一个具有多线程的进程是无法使用setns调用进入到 对应的命令空间的。而Go每启动一个程序就会进入多线程状态,因此需要在启动Go运行时之前先执行setns系统调用,这就需要用到cgo了。 代码见test/nsenter.go。nsenter.go中使用了c中的__attribute__构造函数,所以,一旦这个包被引入,它就会在所有Go运行的 环境启动之前执行,这样就避免了Go多线程导致的无法进入mnt Namespace的问题。

  1. Cgo Cgo是一个很炫酷的功能,允许Go程序去调用C的函数与标准库。你只需要以一种特殊的方式在Go的源代码里写出需要调用的C的代码, Cgo就可以把你的C源码文件和Go文件整合在一起。下面举一个最简单的例子,在这个例子中有两个函数————Random和Seed,在它们里面 调用了C的random和srandom函数。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package rand
/*
#include <stdlib.h>
*/
import "C"

func Random() int {
        return int(C.random())
}

func Seed(i int) {
        c.srandom(C.uint(i))
}

这段代码导入了C,但是你会发现在Go标准库里面并没有这个包,那是因为这根本就不是一个真正的包,而只是Cgo创建的一个特殊命名空间, 用来与C的命名空间交流。这两个函数都分别调用了C里面的random和uint函数,然后对它们进行了类型转换。这就实现了Go代码里面调用C的功能。

  1. 实现exec命令 首先需要添加一个名叫"exec"的子命令,这个子命令至少要包含两个参数:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 这句代码下面会提及
//This is for callback
if os.Getenv(ENV_EXEC_PID) != "" {
        fmt.Printf("pid callback pid %s\n", os.Getgid())
        return nil
}
// 我们希望命令格式是`mydocker exec <容器名>`
if len(os.Args) < 3 {
        log.Fatal("Missing container name or command")
}
containerName := os.Args[2]
// 经除了容器之外的参数当做需要执行的命令处理
commandArray := os.Args[3:]
// 执行命令
ExecContainer(containerName, commandArray)

然后看下ExecContainer的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// test/nsenter.go中的runc代码中,含有mydocker_pid和mydocker_cmd这两个环境变量,用于控制是否执行c里面的setns
const ENV_EXEC_PID = "mydocker_pid"
const ENV_EXEC_CMD = "mydocker_cmd"

func ExecContainer(containerName string, comArray []string) {
        // 根据传递过来的容器名获取宿主机对应的PID
        pid, err := GetContainerPidByName(containerName)
        if err != nil {
                log.Fatal("Exec container getContainerPidByName %s error %v", containerName, err)
        }
        // 把命令以空格为分隔符拼接成一个字符串,便于传递
        cmdStr := strings.Join(comArray, " ")
        fmt.Printf("container pid %s\n", pid)
        fmt.Printf("command %s\n", cmdStr)

        // 这里是重点,下面会讲解
        cmd := exec.Command("/proc/self/exe", "exec")
        cmd.Stdin = os.Stdin
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr

        os.Setenv(ENV_EXEC_PID, pid)
        os.Setenv(ENV_EXEC_CMD, cmdStr)

        if err := cmd.Run(); err != nil {
                log.Fatal("Exec container %s error %v", containerName, err)
        }
}

// 根据提供的容器名获取对应容器的PID
func GetContainerPidByName(containerName string) (string, error) {
        // 先拼接出存储容器信息的路径
        dirURL := fmt.Sprintf(DefaultInfoLocation, containerName)
        configFilePath := dirURL + ConfigName
        // 读取该对应路径下的文件内容
        contentBytes, err := ioutil.ReadFile(configFilePath)
        if err != nil {
                return "", err
        }
        var containerInfo ContainerInfo
        // 将文件内容反序列化成容器信息对象,然后返回对应的PID
        if err := json.Unmarshal(contentBytes, &containerInfo); err != nil {
                return "", err
        }
        return containerInfo.Pid, nil
}

这里又遇到了熟悉的/proc/self/exe, 只不过是换了后面的参数,由原来的init变成了现在的exec。 这么做的目的就是为了那段C代码的执行。因为一旦程序启动,那段C代码就会运行,那么对于我们使用exec来说, 当容器名和对应的命令传递过来以后,程序已经执行了,而且那段C代码也应该运行完毕。那么,怎么指定环境变量让它 再执行一遍呢?这里就用到了这个/proc/self/exe。 这里有创建了一个command,只不过这次只是简单地fork出来一个进程, 不需要这个进程拥有什么命名空间的隔离,然后把这个进程的标准输入输出都绑定到宿主机上。这样去run这里的进程时, 实际上就是又运行了一遍自己的程序,但是这时有一点不同的就是,再一次运行的时候已经指定了环境变量,所以C代码执行 的时候,就能拿到对应的环境变量,便可以进入到指定的Namespace中进行操作了。这时应该就可以明白前面一段代码的意义了:

1
2
3
4
5
//This is for callback
if os.Getenv(ENV_EXEC_PID) != "" {
        fmt.Printf("pid callback pid %s\n", os.Getgid())
        return nil
}

这里就是第二次进入本程序执行exec的时候,如果已经指定了环境变量,说明C代码已经运行,直接返回就可以了,以免重复调用。

  1. 实现给run子命令指定环境变量 修改run子命令,添加一个-e的标签:
1
2
var e []string = make([]string, 0)
cl.Var(newSliceValue([]string{}, &e), "e", "set environment")

cmd.ExtraFiles = []*os.File{readPipe}下面添加一行:

1
cmd.Env = append(os.Environ(), e...)

这个时候,对于不是detach模式的容器,是可以看到使用-e设置的环境变量的,但是detach模式下的容器却没有生效,原因如下: exec命令其实是mydocker发起的另外一个进程,这个进程的父进程其实是宿主机的,并不是容器内的。因为在Cgo里面使用了setns 系统调用,才使得这个进程进入到了容器内的命名空间,但是由于环境变量是继承自父进程的,因此这个exec进程的环境变量其实 是继承自宿主机的,所以在exec进程内看到的环境变量其实是宿主机的环境变量。所以需要根据容器的PID获取到它的环境变量, 并赋给exec进程上。

修改"2. 实现exec命令"中的ExecContainer函数,在os.Setenv(ENV_EXEC_CMD, cmdStr)下添加如下的代码:

1
cmd.Env = append(os.Environ(), getEnvsByPid(pid)...)

getEnvsByPid函数的实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 根据指定的PID来获取对应进程的环境变量
func getEnvsByPid(pid string) []string {
        // 进程环境变量存放的位置是/proc/PID/environ
        path := fmt.Sprintf("/proc/%s/environ", pid)
        contentBytes, err := ioutil.ReadFile(path)
        if err != nil {
                log.Errorf("Read file %s error %v", path, err)
                return nil
        }
        //env split by \u0000
        // 每个环境变量之前,是通过\u0000分隔的。
        envs := strings.Split(string(contentBytes), "\u0000")
        return envs
}

5. 实现停止容器

stop容器的原理很简单,主要就是查找到它的进程PID,然后发送SIGTERM信号,等待进程结束就好。

首先,添加一个名叫"stop"的子命令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 我们期望的调用方式是:"mydocker stop 容器名"
if len(os.Args) < 3 {
        log.Fatal("Missing container name")
}
containerName := os.Args[2]
stopContainer(containerName)

func stopContainer(containerName string) {
        // 根据容器名获取对应的主进程PID
        pid, err := GetContainerPidByName(containerName)
        if err != nil {
                log.Fatal("Get contaienr pid by name %s error %v", containerName, err)
        }
        // 将string类型的PID转换成int类型
        pidInt, err := strconv.Atoi(pid)
        if err != nil {
                log.Fatal("Conver pid from string to int error %v", err)
        }
        // 系统调用kill可以发送信号给进程,通过传递syscall.SIGTERM信号,去杀掉容器主进程
        if err := syscall.Kill(pidInt, syscall.SIGTERM); err != nil {
                log.Fatal("Stop container %s error %v", containerName, err)
        }
        // 根据容器名获取对应的信息对象
        containerInfo, err := getContainerInfoByName(containerName)
        if err != nil {
                log.Fatal("Get container %s info error %v", containerName, err)
        }
        // 至此,容器进程已经被kill,所以下面需要修改容器状态,PID可以置为空
        containerInfo.Status = STOP
        containerInfo.Pid = " "
        // 将修改后的信息序列化成json的字符串
        newContentBytes, err := json.Marshal(containerInfo)
        if err != nil {
                log.Fatal("Json marshal %s error %v", containerName, err)
        }
        dirURL := fmt.Sprintf(DefaultInfoLocation, containerName)
        configFilePath := dirURL + ConfigName
        // 重新写入新的数据覆盖原来的信息
        if err := ioutil.WriteFile(configFilePath, newContentBytes, 0622); err != nil {
                log.Fatal("Write file %s error", configFilePath, err)
        }
}

// 根据容器名获取对应的struct结构
func getContainerInfoByName(containerName string) (*ContainerInfo, error) {
        // 构造存放容器信息的路径
        dirURL := fmt.Sprintf(DefaultInfoLocation, containerName)
        configFilePath := dirURL + ConfigName
        contentBytes, err := ioutil.ReadFile(configFilePath)
        if err != nil {
                log.Fatal("Read file %s error %v", configFilePath, err)
        }
        var containerInfo ContainerInfo
        // 将容器信息字符串反序列化成对应的对象
        if err := json.Unmarshal(contentBytes, &containerInfo); err != nil {
                log.Fatal("GetContainerInfoByName unmarshal error %v", err)
        }
        return &containerInfo, nil
}

6. 实现删除容器

mydocker rm 实现起来非常简单,主要是文件操作,因为容器对应的进程已经被停止,所以只需要将对应记录文件信息的地址删除就可以了。

首先添加一个名为"rm"的子命令

1
2
3
4
5
if len(os.Args) < 3 {
        log.Fatal("Missing container name")
}
containerName := os.Args[2]
removeContainer(containerName)

removeContainer的实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func removeContainer(containerName string) {
        // 根据容器名获取容器对应的信息("5. 实现停止容器"中有介绍)
        containerInfo, err := getContainerInfoByName(containerName)
        if err != nil {
                log.Fatal("Get container %s info error %v", containerName, err)
        }
        // 只删除处于停止状态的容器
        if containerInfo.Status != STOP {
                log.Fatal("Couldn't remove running container")
        }
        // 找到对应存储容器信息的文件路径
        dirURL := fmt.Sprintf(DefaultInfoLocation, containerName)
        // 将所有信息包括子目录都移除
        if err := os.RemoveAll(dirURL); err != nil {
                log.Fatal("Remove file %s error %v", dirURL, err)
        }
        // 不能删除0test.go中的DeleteWorkSpace,因为那里的DeleteWorkSpace是为tty模式服务的
        DeleteWorkSpace(containerInfo.Volume, containerName)
}

本文发表于 0001-01-01,最后修改于 0001-01-01。

本站永久域名「 jiavvc.top 」,也可搜索「 后浪笔记一零二四 」找到我。


上一篇 « 下一篇 »

赞赏支持

请我吃鸡腿 =^_^=

i ysf

云闪付

i wechat

微信

推荐阅读

Big Image