上一篇我们从linux 容器的诞生,与架构对docker 有了初步的了解,这个篇章我们将透过现象看本质,深入的探索Linux容器化与Docker 技术 的原理与本质。
Docker 是一种流行的容器化平台,它利用 Linux 内核中的 cgroups 和 namespaces 特性实现了轻量级的容器隔离。下面将详细介绍 Docker 的底层实现原理,并深入的看看探索其中使用到的三个系统调用与容器隔离的关系。
Docker 的底层实现原理主要涉及以下三个方面
cgroups
cgroups(Control Groups)是 Linux 内核中的一种特性,它可以将进程分组并限制它们对系统资源(如 CPU、内存、磁盘和网络)的使用。Docker 使用 cgroups 来实现容器的资源隔离和限制,例如限制容器可以使用的 CPU 核心数量和内存大小。
在 Linux 系统中,cgroups 的配置存放在 /sys/fs/cgroup
目录下。其中,每个子目录都对应一个 cgroup,可以对其进行资源控制。以下是一些重要的配置文件:
cpu.cfs_quota_us:CPU 时间配额,以微秒为单位。如果值为 -1,则表示没有限制。cpu.cfs_period_us:CPU 时间周期,以微秒为单位。该值确定了 CPU 时间的单位。memory.limit_in_bytes:内存限制,以字节为单位。如果值为 -1,则表示没有限制。blkio.weight:块设备 IO 权重,范围为 10-1000。
namespaces
namespaces 是 Linux 内核中的一种特性(进程隔离技术),它可以将全局系统资源抽象为一组本地资源。在 Docker 中,通过使用不同的 namespaces,可以实现容器的隔离。例如,PID 命名空间可以使容器只能看到自己内部的进程,网络命名空间可以使容器拥有自己的网络接口和 IP 地址,与主机网络隔离。
在 Linux 系统中,namespaces 的配置存放在 /proc/[pid]/ns
目录下,其中 [pid]
为进程 ID。以下是一些重要的配置文件:
ipc:进程间通信隔离,包括信号量、消息队列和共享内存等。uts:主机名和域名隔离。net:网络隔离,包括 IP 地址、网络接口、路由表等。pid:进程隔离,每个容器内的进程只能看到该容器内的进程。mnt:文件系统隔离,每个容器有自己的文件系统视图。
联合文件系统
Docker 使用联合文件系统(UnionFS)实现镜像的分层存储。UnionFS 是一种文件系统堆叠技术,它允许多个文件系统层透明地合并为一个虚拟文件系统。Docker 的联合文件系统由多个镜像层组成,每个镜像层都可以看作一个只读的文件系统,只有最上层的可读写文件系统可以进行写操作。
联合文件系统的配置信息存放在容器的元数据中,包括镜像层的 ID、镜像层的挂载路径等。Docker 会在运行容器时,将不同镜像层的文件系统堆叠在一起,形成一个完整的文件系统。
容器的文件系统路径为 /var/lib/docker/overlay2/<container-id>/merged。
clone()
clone() 系统调用可以创建一个新的进程,并可以通过设计参数来达到隔离的效果。在 Docker 中,每个容器都是一个独立的进程,可以拥有自己的文件系统、网络接口、进程空间和用户空间等。因此,在 Docker 中,clone() 系统调用被用于创建新的容器进程。
使用 clone() 系统调用时,可以通过指定不同的参数来实现不同程度的隔离,例如:
CLONE_NEWUTS:创建一个新的 UTS 命名空间,用于隔离主机的主机名和域名;CLONE_NEWIPC:创建一个新的 IPC 命名空间,用于隔离进程间通信(IPC)机制,例如信号量、共享内存和消息队列;CLONE_NEWPID:创建一个新的 PID 命名空间,用于隔离进程 ID;CLONE_NEWNET:创建一个新的网络命名空间,用于隔离容器的网络接口和 IP 地址;CLONE_NEWUSER:创建一个新的用户命名空间,用于隔离用户和用户组的 ID。
unshare()
unshare() 系统调用可以将进程从主机命名空间中分离出来,并创建一个新的命名空间,使得容器拥有自己独立的命名空间。在 Docker 中,unshare() 系统调用被用于创建新的命名空间,并将容器进程与主机进程分离开来,以实现容器的隔离。这个命名空间将成为容器进程的根命名空间,容器进程只能访问该命名空间下的资源。在这个命名空间中,容器进程将看到自己的文件系统、进程空间和网络接口等,而不是主机上的资源。
setns()
setns() 系统调用可以将进程加入到特定的命名空间中。在 Docker 中,当容器需要访问主机网络时,可以使用 setns() 系统调用将进程切换到主机网络命名空间中,并访问主机网络资源。因此,setns() 系统调用是 Docker 实现容器与主机网络之间通信的的关键系统调用。
在 Docker 中,容器的创建和运行可以分为以下四个步骤:
镜像获取
Docker 的镜像是一个文件系统层的集合,包含了应用程序运行所需的所有组件。在创建容器之前,需要先获取容器的基础镜像,这可以通过从 Docker Hub 中下载镜像,或者使用本地构建的镜像。
容器创建
使用 Docker 命令行工具(CLI)创建容器时,Docker 首先会调用 clone() 系统调用来创建一个新的进程。该进程将拥有自己的文件系统、进程空间、网络接口等,与主机和其他容器隔离。
示例代码
package main
import ( "fmt" "os" "syscall")
func main() { cmd := "/bin/bash" args := []string{"bash"}
cloneFlags := syscall.CLONE_NEWPID | syscall.CLONE_NEWUTS | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC // 使用 clone() 系统调用创建子进程 pid, err := syscall.Clone(func() { // 子进程中运行指定命令 err := syscall.Exec(cmd, args, os.Environ()) if err != nil { panic(err) } }, nil, cloneFlags) if err != nil { panic(err) }
fmt.Printf("child process ID: %d\n", pid)
// 等待子进程结束 var status syscall.WaitStatus _, err = syscall.Wait4(pid, &status, 0, nil) if err != nil { panic(err) } fmt.Printf("child process exit status: %d\n", status.ExitStatus())}
容器启动
在容器创建完成后,Docker 会使用 unshare() 系统调用创建新的命名空间,并将容器进程与主机进程隔离开来。然后,Docker 会将容器进程切换到容器的网络命名空间中,以获得独立的网络接口和 IP 地址。
示例代码
package main
import ( "fmt" "syscall")
func main() { // 创建新的 UTS 命名空间 err := syscall.Unshare(syscall.CLONE_NEWUTS) if err != nil { panic(err) }
// 获取当前主机的 hostname hostname, err := syscall.Gethostname() if err != nil { panic(err) } fmt.Printf("hostname in current namespace: %s\n", hostname)
// 修改当前命名空间中的 hostname err = syscall.Sethostname([]byte("newhostname")) if err != nil { panic(err) }
// 再次获取当前主机的 hostname hostname, err = syscall.Gethostname() if err != nil { panic(err) } fmt.Printf("hostname in new namespace: %s\n", hostname)}
容器运行
在容器启动后,Docker 会使用 setns() 系统调用将容器进程切换到相应的命名空间中,并将容器进程加入到相应的 cgroups 中,以限制容器使用系统资源。此外,Docker 还会挂载容器的文件系统层,并启动容器中定义的应用程序。
示例代码
package main
import ( "fmt" "os" "syscall")
func main() { pid := os.Getpid()
// 打开指定进程的 PID namespace fd, err := syscall.Open(fmt.Sprintf("/proc/%d/ns/pid", pid), syscall.O_RDONLY, 0) if err != nil { panic(err) } defer syscall.Close(fd)
// 加入指定进程的 PID namespace err = syscall.Setns(fd, syscall.CLONE_NEWPID) if err != nil { panic(err) }
// 获取当前进程的 PID pid = os.Getpid() fmt.Printf("PID in new namespace: %d\n", pid)}
syscall 相关的知识可以到五分钟学GO的连载学习。
Docker 利用 Linux 内核中的 cgroups 和 namespaces 特性实现了轻量级的容器隔离。在 Docker 中,clone() 系统调用被用于创建新的容器进程,unshare() 系统调用被用于创建新的命名空间,以实现容器的隔离,而 setns() 系统调用被用于将容器进程切换到相应的命名空间中。通过这些系统调用,Docker 可以在容器和主机之间实现高度的隔离和安全性,从而提供可移植和可重复的应用程序环境。
在实际应用中,容器化技术可以大大简化应用程序的部署和管理。例如,你可以使用Docker来打包你的应用程序及其所有依赖项,然后使用Kubernetes来管理这些容器化的应用程序。这种方式可以让你的应用程序在不同环境中无缝运行,并且可以轻松地进行扩展和维护。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。