出于对各类平台开发经验的迫切需要, 和本着只有亲自创造的才是真正理解的费曼学习精神, 我准备写一个系列的从无到有的各类项目开发blogs, 一是记录编程过程, 遇到的问题, 个人领悟, 二是自我督促去做基础开发, 积累开发经验, 理解各个常用软件的实现原理, 三是水blogs, 三是为了最终研究高级安全技术夯实基本功
这里用golang完成一个简易docker, 直接从源码层面去理解容器的原理
要真正理解软件世界中的容器是什么, 需要了解制作容器的过程. 在这个过程中, 将讨论容器vs容器化、linux容器(包括名称空间、cgroup和分层文件系统), 然后我们将遍历一些从头构建简单容器的代码, 最后讨论这一切的真正含义
要在低层次上讨论容器,我们必须讨论三个方面: 名称空间、cgroup和分层文件系统.
名称空间提供了在一台机器上运行多个容器所需的隔离, 同时为每个容器提供了类似于它自己的环境. demo中有六个名称空间, 每一个都可以被独立请求, 相当于给一个进程(及其子进程)一个机器资源子集的视图.
其中的6个名称空间是
cgroups是一种提供容器间资源共享的机制.
cgroups收集一组进程或任务id, 并对它们进行限制. 在命名空间隔离进程的地方, cgroups在进程之间执行资源共享. 内核将cgroup公开为一个可以挂载的特殊文件系统. 只需将进程id添加到任务文件中, 就可以将进程或线程添加到cgroup中, 然后通过编辑该目录中的文件来读取和配置各种值.
Layered Filesystems负责实现images的整体移动.
在基本层面上, 分层文件系统相当于优化调用, 为每个容器创建根文件系统的一个副本. 有很多方法可以做到这一点. Btrfs在文件系统层使用copy on write. Aufs使用“union mounts”. 这里只使用非常简单的方法: 真正地进行复制. (很慢但有效
main.go
程序读入第一个参数. 如果是’ run ‘则运行parent()方法, 如果是child()则运行子方法. 父方法运行’ /proc/self/exe ', 这是一个包含当前可执行文件的内存映像的特殊文件.
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
switch os.Args[1] {
case "run":
parent()
case "child":
child()
default:
panic("invalid operation")
}
}
func parent() {
cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Println("Error", err)
os.Exit(1)
}
}
func child() {
cmd := exec.Command(os.Args[2], os.Args[3:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Println("Error", err)
os.Exit(1)
}
}
func must(err error) {
if err != nil {
panic(err)
}
}
在parent()函数添加名称空间, 告诉go在运行子进程时传递一些额外的标志.
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
}
至此进程处于一组独立的名称空间中, 不过文件系统看起来和主机一样. 这是因为进程处于挂载的名称空间中,而初始挂载是从创建的名称空间继承的. 为了改变挂载的文件系统, 在child()
中添加
must(syscall.Mount("rootfs", "rootfs", "", syscall.MS_BIND, ""))
must(os.MKdirAll("rootfs/oldrootfs", 0700))
must(syscall.PivotRoot("rootfs", "rootfs/oldrootfs"))
must(os.Chdir("/"))
最后两行是最重要的, 将当前目录/
移动到rootfs/oldrootfs
,并将新的rootfs
目录切换到/
. 当pivoroot
调用完成后, 容器中的/
目录将指向rootfs
一个省略了初始化操作, cgroups资源共享的简易docker就完成了
package main
import (
"fmt"
"os"
"os/exec"
"syscall"
)
func main() {
switch os.Args[1] {
case "run":
parent()
case "child":
child()
default:
panic("invalid operation")
}
}
func parent() {
cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
}
if err := cmd.Run(); err != nil {
fmt.Println("Error", err)
os.Exit(1)
}
}
func child() {
must(syscall.Mount("rootfs", "rootfs", "", syscall.MS_BIND, ""))
must(os.MKdirAll("rootfs/oldrootfs", 0700))
must(syscall.PivotRoot("rootfs", "rootfs/oldrootfs"))
must(os.Chdir("/"))
cmd := exec.Command(os.Args[2], os.Args[3:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Println("Error", err)
os.Exit(1)
}
}
func must(err error) {
if err != nil {
panic(err)
}
}
后面会完善demo-docker的细节, 这里只是一个开始
docker主要包括名称空间, cgroups, 分层文件系统, 分别实现隔离, 资源共享, 镜像功能
https://www.infoq.com/articles/build-a-container-golang/
Liz Rize大佬的教学
https://www.youtube.com/watch?v=8fi7uSYlOdc