大家好,继续深入容器系列,我们已经知道容器是从 Linux 命名空间和 Cgroups 构建的,为了更深入地了解它,我们将学习如何使用 Golang 构建自己的容器。
这篇文章我参考了 Julian Friedman 的Build Your Own Container Using Less than 100 Lines和 Liz Rice 的 Go从头开始构建容器。
这是深入容器系列的第四部分:
1.Linux 命名空间和 Cgroups:容器是由什么制成的?
2.深入容器运行时。
3.Kubernetes 如何与容器运行时一起工作。
4.深入Container - 用Golang构建自己的容器。
构建容器
container.go
package main
import (
"os"
)
func main() {
}
func must(err error) {
if err != nil {
panic(err)
}
}
进入全屏模式 退出全屏模式
docker run
docker run busybox echo "A"
进入全屏模式 退出全屏模式
您将看到容器运行并打印字母“A”,如果您运行以下命令:
docker run -it busybox sh
进入全屏模式 退出全屏模式
容器运行并且外壳将附加到它。
/ #
进入全屏模式 退出全屏模式
如果我们现在键入命令,则该命令正在容器中运行。
/ # hostname
d12ccc0e00a0
进入全屏模式 退出全屏模式
/ # ps
PID USER TIME COMMAND
1 root 0:00 sh
9 root 0:00 ps
进入全屏模式 退出全屏模式
hostname 命令不打印打印容器主机名的服务器的主机名,而 ps 命令只打印两个进程。
container.go
package main
import (
"os"
)
// docker run <image> <command>
// go run container.go run <command>
func main() {
switch os.Args[1] {
case "run":
run()
default:
panic("Error")
}
}
func run() {
}
func must(err error) {
if err != nil {
panic(err)
}
}
进入全屏模式 退出全屏模式
run()run()go run container.go rundocker run
run()
package main
import (
"os"
"os/exec"
)
// docker run <image> <command>
// go run container.go run <command>
func main() {
switch os.Args[1] {
case "run":
run()
default:
panic("Error")
}
}
func run() {
cmd := exec.Command(os.Args[2], os.Args[3:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
must(cmd.Run())
}
func must(err error) {
if err != nil {
panic(err)
}
}
进入全屏模式 退出全屏模式
os/execos.Argsgo run container.go run echo "A"os.Args
Args[0] = "container.go"
Args[1] = "run"
Args[2] = "echo"
Args[3] = "A"
进入全屏模式 退出全屏模式
exec.Command()os.ArgsCommand()
exec.Command(name string, arg ...string)
进入全屏模式 退出全屏模式
该函数采用第一个参数,即它将执行的命令,其余值是该命令的参数。
docker run -it busybox sh
go run container.go run sh
进入全屏模式 退出全屏模式
当你运行 docker 命令时,你会发现它几乎是一样的。
#
进入全屏模式 退出全屏模式
我们已经成功迈出了第一步😁,但是当你输入主机名命令时,它会打印我们服务器的主机名,而不是容器的主机名。
# hostname
LAPTOP-2COB82RG
进入全屏模式 退出全屏模式
如果您在我们的程序中键入更改主机名的命令,它也会影响服务器外部。
# hostnamectl set-hostname container
进入全屏模式 退出全屏模式
exit
我们的程序目前只是运行 sh 命令而不是容器,接下来,我们将通过每个步骤来构建容器。正如我们所知,容器是从 Linux 命名空间构建的。
命名空间
命名空间提供了隔离环境,帮助我们在同一台服务器上运行独立于其他进程的进程。在撰写本文时,有如下六个命名空间,
-
PID:PID 命名空间为进程提供一组来自其他命名空间的独立进程 ID (PID)。 PID 命名空间使在其中创建的第一个进程分配有 PID 1。
-
MNT:挂载命名空间控制挂载点,提供你挂载和卸载文件夹,不影响其他命名空间。
-
NET:网络命名空间为进程创建它们的网络堆栈。
-
UTS:UNIX 分时命名空间允许一个进程拥有单独的主机名和域名。
-
USER:用户命名空间为进程创建自己的一组 UIDS 和 GIDS。
-
IPC:IPC命名空间将进程与进程间通信隔离开来,这样可以防止不同IPC命名空间中的进程使用。
我们将在 Golang 程序中使用 PID、UTS 和 MNT 命名空间。
UTS 命名空间
container.go
package main
import (
"os"
"os/exec"
"syscall"
)
// docker run <image> <command>
// go run container.go run <command>
func main() {
switch os.Args[1] {
case "run":
run()
default:
panic("Error")
}
}
func run() {
cmd := exec.Command(os.Args[2], os.Args[3:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS,
}
must(cmd.Run())
}
func must(err error) {
if err != nil {
panic(err)
}
}
进入全屏模式 退出全屏模式
cmd.SysProcAttr
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS,
}
进入全屏模式 退出全屏模式
现在让我们再试一次。
go run container.go run sh
进入全屏模式 退出全屏模式
运行命令以更改主机名。
# hostnamectl set-hostname wsl
# hostname
wsl
进入全屏模式 退出全屏模式
exit
docker run -it busybox shcontainer.go
package main
import (
"os"
"os/exec"
"syscall"
)
// docker run <image> <command>
// ./container run <command>
func main() {
switch os.Args[1] {
case "run":
run()
case "child":
child()
default:
panic("Error")
}
}
func run() {
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,
}
must(cmd.Run())
}
func child() {
syscall.Sethostname([]byte("container"))
cmd := exec.Command(os.Args[2], os.Args[3:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
must(cmd.Run())
}
func must(err error) {
if err != nil {
panic(err)
}
}
进入全屏模式 退出全屏模式
child()exec.Command
exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
进入全屏模式 退出全屏模式
/proc/self/exesyscall.Sethostname([]byte("container"))
go run container.go run sh/proc/self/exe child shsyscall.Sethostname([]byte("container"))exec.Command("sh")
让我们再试一次。
go run container.go run sh
进入全屏模式 退出全屏模式
键入主机名,您将看到您的进程有自己的主机名。
# hostname
container
进入全屏模式 退出全屏模式
这样我们就完成了下一步😁。
接下来尝试输入ps命令列出进程,看看是不是和我们运行docker run时一样?
# ps
PID TTY TIME CMD
11254 pts/3 00:00:00 sudo
11255 pts/3 00:00:00 bash
17530 pts/3 00:00:00 go
17626 pts/3 00:00:00 container
17631 pts/3 00:00:00 exe
17636 pts/3 00:00:00 sh
17637 pts/3 00:00:00 ps
进入全屏模式 退出全屏模式
根本不像,您看到的进程是服务器外部的进程。
PID 命名空间
container.go
...
func run() {
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,
}
must(cmd.Run())
}
...
进入全屏模式 退出全屏模式
syscall.CLONE_NEWPID
go run container.go run sh
进入全屏模式 退出全屏模式
# ps
PID TTY TIME CMD
11254 pts/3 00:00:00 sudo
11255 pts/3 00:00:00 bash
17530 pts/3 00:00:00 go
17626 pts/3 00:00:00 container
17631 pts/3 00:00:00 exe
17636 pts/3 00:00:00 sh
17637 pts/3 00:00:00 ps
进入全屏模式 退出全屏模式
什么?它根本没有改变。为什么?
/proc
ls /proc
进入全屏模式 退出全屏模式
现在,您的进程的文件系统看起来与主机相同,因为它的文件系统是从当前服务器继承的,让我们改变它。
MNT 命名空间
container.go
package main
import (
"os"
"os/exec"
"syscall"
)
// docker run <image> <command>
// ./container run <command>
func main() {
switch os.Args[1] {
case "run":
run()
case "child":
child()
default:
panic("Error")
}
}
func run() {
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,
}
must(cmd.Run())
}
func child() {
syscall.Sethostname([]byte("container"))
must(syscall.Chdir("/"))
must(syscall.Mount("proc", "proc", "proc", 0, ""))
cmd := exec.Command(os.Args[2], os.Args[3:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
must(cmd.Run())
}
func must(err error) {
if err != nil {
panic(err)
}
}
进入全屏模式 退出全屏模式
syscall.CLONE_NEWNS
syscall.Chdir("/")
syscall.Mount("proc", "proc", "proc", 0, "")
进入全屏模式 退出全屏模式
现在,让我们再次运行。
go run container.go run sh
进入全屏模式 退出全屏模式
键入 ps 命令。
# ps
PID TTY TIME CMD
1 pts/3 00:00:00 exe
7 pts/3 00:00:00 sh
8 pts/3 00:00:00 ps
进入全屏模式 退出全屏模式
我们成功了😁。
结论
所以我们知道如何使用 Golang 构建一个简单的容器,但实际上,容器还有很多其他的东西,比如 Cgroups 来限制进程的资源,创建 USER 命名空间,以及从容器中挂载文件到服务器等等......
但基本上,容器创建隔离环境的主要功能是 Linux 命名空间。如果您有任何疑问或需要更多说明,可以在下面的评论部分提出。