这是《自己动手写Docker》的读书笔记,本篇为第四章的笔记,主要的内容为如何使用GO语言、busybox以及AUFS构建一个可以运行的容器,并实现Docker中的Volume功能。
实验环境参数如下
- 虚拟机 Parallel14
- Ubuntu 16.04
- Linux内核 4.10.0-28-generic
- Golang v1.12.4
本笔记的代码也是在Github仓库schwarzeni/write-your-own-docker-learning上,cpt4
文件下的内容。
使用busybox
首先使用Docker安装busybox镜像,并使其运行起来,同时将其打包成tar。
busybox中有非常多的UNIX中的空间,它提供了一个非常完整且小巧的文件系统,这个可以为我们所用。
首先,为了确保Docker的速度,最好使用阿里云的Docker镜像加速,免费且上面有详细的使用教程。之后就输几行命令即可
1 2 3 4 5 6 7 8
| docker pull busybox
docker run -d busybox top -b
docker ps
|
获取其容器ID后,就可以导出了
1 2 3 4 5
| docker export -o busybox.tar 0b7d
tar -xvf busybox.tar -C busybox/
|
pivot_root
pivot_root
为Linux的系统调用,主要功能是改变当前的root文件系统。pivot_root
可以将当前进程的root文件系统移动到 put_old
文件夹中,然后使 new_root
成为新的root文件系统。
chroot
是针对某个进程,系统的其他部分依旧运行在老的root目录下。而pivot_root
是把整个系统切换到一个新的root目录中,移除对之前root文件系统的依赖,这样你就可以umount原先的文件系统。
以下代码对应 cpt4/cpt4-1-pivote-root
关键函数如下
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
| func pivotRoot(root string) (err error) { if err = syscall.Mount(root, root, "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil { return fmt.Errorf("mount rootfs to itself error: %v", err) }
pivotDir := filepath.Join(root, ".pivot_root") if err = os.Mkdir(pivotDir, 0777); err != nil { return }
if err = syscall.PivotRoot(root, pivotDir); err != nil { return fmt.Errorf("pivot_root %v", err) }
if err = syscall.Chdir("/"); err != nil { return fmt.Errorf("chdir / %v", err) }
pivotDir = filepath.Join("/", ".pivot_root")
if err = syscall.Unmount(pivotDir, syscall.MNT_DETACH); err != nil { return fmt.Errorf("unmount pivot_root dir %v", err) }
return os.Remove(pivotDir) }
|
这样就可以调用此函数进程mount操作了
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
|
func setUpMount() { pwd, err := os.Getwd() if err != nil { log.Errorf("Get current location error %v", err) return } log.Infof("Current location is %s", pwd)
if err = syscall.Mount("", "/", "", syscall.MS_PRIVATE | syscall.MS_REC, ""); err != nil { return }
if err = pivotRoot(pwd); err != nil { return }
defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV if err = syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), ""); err != nil { return } if err = syscall.Mount("tmpfs", "/dev", "tmpfs", syscall.MS_NOSUID|syscall.MS_STRICTATIME, "mode=755"); err != nil { return } }
|
使用AUFS
AUFS
可以构建一个拥有二层模式的镜像,对于最上层 可写层 的修改不会影响到 基础层
mount
Docker在使用镜像启动一个容器时,会新建2个layer:write layer
和 container-init layer
。前者为容器唯一的可读写层;后者为容器新建的只读层,用来存储容器启动时传入的系统信息。最后把 write layer
和 container-init layer
以及相关镜像的layers都mount到一个mnt目录下,然后将mnt目录作为容器启动的根目录。
应用AUFS的主要目的是不让在容器中进程的操作对镜像产生任何的影响。主要实现如下几个函数
CreateReadOnlyLayer
新建 busybox
文件夹,将busybox.tar解压到 busybox
文件夹下,作为容器的 只读层
CreateWriteLayer
创建名为 writeLayer
的文件夹,作为容器唯一的 可写层
CreateMountPoint
创建 mnt
文件夹作为挂载点,然后将 writeLayer
目录和 busybox
目录mount到mnt目录下
在 NewParentProcess
函数中调用这几个函数,将容器使用的宿主机目录换成 mnt
目录。相关代码对应于目录 cpt4/cpt4-2-3-aufs-volume
下
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
|
func NewWorkspace(rootURL string, mntURL string, volume string) { CreateReadOnlyLayer(rootURL) CreateWriteLayer(rootURL) CreateMountPoint(rootURL, mntURL) }
func CreateReadOnlyLayer(rootURL string) { busyboxURL := path.Join(rootURL, "busybox/") busyboxTarURL := path.Join(rootURL, "busybox.tar")
exist, err := PathExists(busyboxURL) if err != nil { log.Infof("Fail to judge whether dir %s exists. %v", err) }
if !exist { if err := os.Mkdir(busyboxURL, 0777); err != nil { log.Errorf("Mkdir dir %s for busybox error, %v", busyboxURL, err) }
if _, err := exec.Command("tar", "-xvf", busyboxTarURL, "-C", busyboxURL).CombinedOutput(); err != nil { log.Errorf("Untar dir %s error. %v", busyboxTarURL, err) } } }
func CreateWriteLayer(rootURL string) { writeURL := path.Join(rootURL, "writeLayer/") if err := os.Mkdir(writeURL, 0777); err != nil { log.Errorf("Mkdir dir %s for writelayer error. %v", writeURL, err) } }
func CreateMountPoint(rootURL string, mntURL string) { if err := os.Mkdir(mntURL, 0777); err != nil { log.Errorf("Mkdir dir %s for mnt error. %v", mntURL, err) } dirs := "dirs=" + path.Join(rootURL, "writeLayer") + ":" + path.Join(rootURL, "busybox") cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", mntURL) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Errorf("error when mount aufs. %v", err) } }
func PathExists(sourceURL string) (exist bool, err error) { _, err = os.Stat(sourceURL) if err == nil { return true, nil } if os.IsNotExist(err) { return false, nil } return false, err }
|
umount
Docker会在删除容器的时候,将容器对应的 Write Layer
和 Container-init Layer
删除,而保留镜像的所有内容。Go语言主要实现 DeleteWorkspace
函数,内部包括:
DeleteMountPount
umount掉 mnt
目录,随后删除
DeleteWriteLayer
删除掉 writeLayer
文件夹,因为从之前的代码(如下)中可以看出这个文件夹是AUFS中用于记录变化的
1 2 3 4
| dirs := "dirs=" + path.Join(rootURL, "writeLayer") + ":" + path.Join(rootURL, "busybox") cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", mntURL)
|
具体的函数实现如下
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
|
func DeleteWorkspace(rootURL string, mntURL string, volume string) { DeleteMountPoint(mntURL) DeleteWriteLayer(rootURL) }
func DeleteMountPoint(mntURL string) { cmd := exec.Command("umount", mntURL) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Errorf("error when umount mnt %s. %v", mntURL, err) } if err := os.RemoveAll(mntURL); err != nil { log.Errorf("error when remove mnt folder %s. %v", mntURL, err) } }
func DeleteWriteLayer(rootURL string) { writeURL := path.Join(rootURL, "writeLayer") if err := os.RemoveAll(writeURL); err != nil { log.Errorf("error when remove writeLayer folder %s. %v", writeURL, err) } }
|
从书中偷一个时序图
![](image1.png)
当启动程序时,对容器内部进行修改,发生改变的只是 writeLayer
中的内容,busybox
中的内容没有改变
实现Volume
此功能将外部系统的文件和容器内部的文件实现了映射,相当于是挂载到了同一设备上。主要使用了如下的mount系统调用
1
| mount -t aufs -o dirs=<parentURL> none <mnt>/<containerURL>
|
本节对应的代码位置同上
首先,添加命令行flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| var runCommand = cli.Command{ Name: "run", Usage: "mydocker run -ti [command]", Flags: []cli.Flag{ cli.StringFlag{ Name: "v", Usage: "volume", }, }, Action: func(ctx *cli.Context) error{ volumn := ctx.String("v") Run(tty, cmdArr, resConf, volumn) return nil }, }
|
其次,添加 MountVolume
和 DeleteMountPointWithVolume
,同时要对 NewWorkspace
和 DeleteWorkspace
做出修改
1 2 3 4 5 6
| func volumeUrlExtract(volume string) ([]string) { var volumeURLS []string volumeURLS = strings.Split(volume, ":") return volumeURLS }
|
MountVolume
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
|
func MountVolume(rootURL string, mntURL string, volumesURLs []string) {
parentURL := volumesURLs[0] if err := os.Mkdir(parentURL, 0777); err != nil { log.Infof("Mkdir parent dir %s error. %v", parentURL, err) }
containerURL := volumesURLs[1] containerVolumeURL := path.Join(mntURL, containerURL) if err := os.Mkdir(containerVolumeURL, 0777); err != nil { log.Infof("Mkdir container dir %s error. %v", containerVolumeURL, err) }
dirs := "dirs=" + parentURL cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", containerVolumeURL) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Errorf("Mount volume failed %v", err) } }
|
DeleteMountPointWithVolume
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
func DeleteMountPointWithVolume(rootURL string, mntURL string, volumeURLs []string) { containerURL := path.Join(mntURL, volumeURLs[1]) cmd := exec.Command("umount", containerURL) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Errorf("Unmount volumn failed. %v", err) }
DeleteMountPoint(mntURL) }
|
NewWorkspace
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| func NewWorkspace(rootURL string, mntURL string, volume string) { CreateReadOnlyLayer(rootURL) CreateWriteLayer(rootURL) CreateMountPoint(rootURL, mntURL)
if volume != "" { volumeURLs := volumeUrlExtract(volume) if len(volumeURLs) == 2 && volumeURLs[0] != "" && volumeURLs[1] != "" { MountVolume(rootURL, mntURL, volumeURLs) log.Infof("mount volume: %q", volumeURLs) } else { log.Infof("Volume parameter input is not correct") } }
}
|
DeleteWorkspace
1 2 3 4 5 6 7 8 9 10 11
| func DeleteWorkspace(rootURL string, mntURL string, volume string) { volumeURLs := volumeUrlExtract(volume) if len(volumeURLs) == 2 && volumeURLs[0] != "" && volumeURLs[1] != "" { DeleteMountPointWithVolume(rootURL, mntURL, volumeURLs) } else { DeleteMountPoint(mntURL) }
DeleteWriteLayer(rootURL) }
|
再偷一个时序图
![](image2.png)
镜像打包
这个思路比较简单,就不在这里写了,就是把 mnt
打个包