前言
出于对各类平台开发经验的迫切需要, 和本着只有亲自创造的才是真正理解的费曼学习精神, 我准备写一个系列的从无到有的各类项目开发blogs, 一是记录编程过程, 遇到的问题, 个人领悟, 二是自我督促去做基础开发, 积累开发经验, 理解各个常用软件的实现原理, 三是水blogs, 三是为了最终研究高级安全技术夯实基本功
这里用golang完成一个简易docker, 直接从源码层面去理解容器的原理
开发
要真正理解软件世界中的容器是什么, 需要了解制作容器的过程. 在这个过程中, 将讨论容器vs容器化、linux容器(包括名称空间、cgroup和分层文件系统), 然后我们将遍历一些从头构建简单容器的代码, 最后讨论这一切的真正含义 要在低层次上讨论容器,我们必须讨论三个方面: 名称空间、cgroup和分层文件系统.
名称空间 - 隔离
名称空间提供了在一台机器上运行多个容器所需的隔离, 同时为每个容器提供了类似于它自己的环境. demo中有六个名称空间, 每一个都可以被独立请求, 相当于给一个进程(及其子进程)一个机器资源子集的视图. 其中的6个名称空间是
- PID: PID名称空间为进程及其子进程提供系统中进程子集的视图. 可以把它想象成一个映射表. 当pid名称空间中的进程向内核请求进程列表时, 内核会查看映射表. 如果该表中存在进程, 则使用映射的ID而不是真实的ID. 如果它在映射表中不存在, 内核就会假装它根本不存在. pid命名空间使在其内部创建的第一个进程pid 1(通过将其主机ID映射到1), 从而在容器中呈现出一个独立的进程树.
- MNT: 挂载名称空间为其中包含的进程提供了自己的挂载表. 这意味着它们可以挂载和卸载目录, 而不影响其他名称空间(包括主机名称空间). 更重要的是, 结合pivot_root系统调用, 它允许进程拥有自己的文件系统. 通过交换容器看到的文件系统,可以让一个进程认为它正在ubuntu、busybox或alpine上运行. 这一点让程序运行在不同操作系统成为可能.
- NET: 网络名称空间为使用它的进程提供自己的网络堆栈. 一般来说, 只有主网络名称空间(即启动计算机时启动的进程)才会附加任何实际的物理网卡. 但是可以创建虚拟以太网对连接的以太网卡,其中一端可以放在一个网络名称空间中, 另一端放在另一个网络名称空间中, 从而在网络名称空间之间创建虚拟链路. 这有点像在一台主机上有多个ip堆栈相互通信. 通过一些路由技术, 允许每个容器与真实的网络交互同时隔离每个独立的网络栈.
- UTS: UTS名称空间为它的进程提供它们自己的系统主机名和域名视图. 输入UTS命名空间后, 设置主机名或域名不会影响其他进程.
- IPC: IPC命名空间隔离各种进程间通信机制, 如消息队列等.
- USER:用户名称空间是最近添加的, 并且从安全性的角度来看可能是最强大的. 用户命名空间将进程看到的uid映射到主机上的另一组uid(和gid), 使用用户命名空间, 可以将容器的根用户ID(即0)映射到主机上的任意(且无特权)uid. 这意味着我们可以让一个容器认为它拥有根访问权限, 甚至可以在特定于容器的资源上给它根权限, 而不实际在根名称空间中给它任何特权. 容器可以自由地以uid 0的形式运行进程(通常与具有根权限同义), 但是内核实际上是将这个uid映射到一个没有特权的真正uid. 大多数容器系统不将容器中的任何uid映射到调用命名空间中的uid 0.
cgroups - 资源共享
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
|