目标
这篇博客的目标是实现如下命令.
root@nicktming:~/go/src/github.com/nicktming/mydocker# go build .
root@nicktming:~/go/src/github.com/nicktming/mydocker# ./mydocker run -it /bin/sh
# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 23:55 pts/4 00:00:00 /bin/sh
root 4 1 0 23:56 pts/4 00:00:00 ps -ef
源码: 代码下载
git checkout
实现
实现run方法
根据前面博客对
urfave/cli
的介绍,所以在main
方法中直接使用. 该run
方法很简单,就是调用/bin/sh
命令.
.
|-- command
| |-- command.go
| `-- run.go
|-- main.go
|-- README.md
`-- urfave-cli-examples
|-- test01.go
|-- test02.go
`-- test03.go
main
方法
package main
import (
"github.com/nicktming/mydocker/command"
"github.com/urfave/cli"
"log"
"os"
)
func main() {
app := cli.NewApp()
app.Name = "mydocker"
app.Usage = "implementation of mydocker"
app.Commands = []cli.Command{
command.RunCommand,
}
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
}
可以看一下在
command/command.go
文件中RunCommand
var RunCommand = cli.Command{
Name: "run",
Flags: []cli.Flag {
cli.BoolFlag{
Name: "it",
Usage: "enable tty",
},
},
Action: func(c *cli.Context) error {
tty := c.Bool("it")
command := c.Args().Get(0)
Run(command, tty)
return nil
},
}
可以看到
run
命令中有一个flag
为it
用于执行命令时是否需要tty
,之后会调用command/run.go
中的Run
方法.
func Run(command string, tty bool) {
cmd := exec.Command(command)
// for kinds of namespace
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
}
if tty {
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
}
/**
* Start() will not block, so it needs to use Wait()
* Run() will block
*/
if err := cmd.Start(); err != nil {
log.Printf("Run Start err: %v.\n", err)
log.Fatal(err)
}
cmd.Wait()
}
执行命令
go build .
./mydocker run -it /bin/sh
但是存在一个问题是当执行ps -ef
时还是可以看到整个宿主机的进程. 但是先执行mount -t proc proc /proc
后在执行ps -ef
的时候就只显示当前namespace
内进程的状态了, 这就是mount namespace
的作用. 因此接下来的一个任务就是要在此容器起来前先执行该mount
命令.
实现init命令
需要做以下修改
1. 在command/command.go
中增加InitCommand
命令
var InitCommand = cli.Command{
Name: "init",
Flags: []cli.Flag {
cli.BoolFlag{
Name: "it",
Usage: "enable tty",
},
},
Action: func(c *cli.Context) error {
command := c.Args().Get(0)
Init(command)
return nil
},
}
2. 在
command
增加init.go
以下内容:
package command
import (
"log"
"os"
"os/exec"
"syscall"
)
func Init(command string) {
defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), "")
cmd := exec.Command(command)
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
log.Printf("Init Run() function err : %v\n", err)
log.Fatal(err)
}
}
3. 在
command/run.go
中的Run
方法中修改, 将先执行当前进程(/proc/self/exe
)的init
命令, 参数为command
. 表示在执行用户的command
命令以前,在已经做好namespace
隔离的进程中先执行init
命令(其实就是执行mount -t proc proc /proc
操作), 然后再执行用户command
.
//cmd := exec.Command(command)
cmd := exec.Command("/proc/self/exe", "init", command)
4. 在
main.go
的main
方法中加入InitCommand
命令.
app.Commands = []cli.Command{
command.RunCommand,
command.InitCommand,
}
运行
go build .
./mydocker run -it /bin/sh
# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 23:51 pts/3 00:00:00 /proc/self/exe init /bin/sh
root 4 1 0 23:51 pts/3 00:00:00 /bin/sh
root 5 4 0 23:51 pts/3 00:00:00 ps -ef
可以看到容器内
1
号进程是进入容器内第一个执行的进程, 这个就是PID namespace
的作用. 但是这个依然没有达到预期的目标, 因为这个1
号是程序自己因为需要去做mount proc proc /proc
操作而使用的一个进程, 并不是我们预期的用户进程在这里是4
. (是由init
进程fork
出来执行command(/bin/sh)
的一个子进程.)
因此接下来的任务是将这个
init
隐藏起来, 让用户初始容器的命令进程成为真正的1
号进程.
实现用户进程成为1
号进程
由于
command/init.go
中的实现是用cmd.Run
形式,所以会出现这样的情况.
1. 修改
command/init.go
中的Init
方法代码如下:
func Init(command string) {
defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), "")
/*
cmd := exec.Command(command)
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
log.Printf("Init Run() function err : %v\n", err)
log.Fatal(err)
}
*/
if err := syscall.Exec(command, []string{command}, os.Environ()); err != nil {
log.Printf("syscall.Exec err: %v\n", err)
log.Fatal(err)
}
}
执行结果:
root@nicktming:~/go/src/github.com/nicktming/mydocker# go build .
root@nicktming:~/go/src/github.com/nicktming/mydocker# ./mydocker run -it /bin/sh
# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 21:55 pts/6 00:00:00 /bin/sh
root 4 1 0 21:55 pts/6 00:00:00 ps -ef
关于syscall.Exec
syscall.exec
会执行参数指定的命令,但是并不创建新的进程,只在当前进程空间内执行,即替换当前进程的执行内容,会重用同一个进程号PID
.
使用exec.Command
方式
func main() {
log.Printf("pid:%d\n", os.Getpid())
cmd := exec.Command("sh")
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
log.Printf("Init Run() function err : %v\n", err)
log.Fatal(err)
}
}
运行:
root@nicktming:~/go/src/github.com/nicktming/mydocker/test/syscall# go run TestExec.go
2019/03/25 23:47:29 pid:20255
# echo $$
20258
使用
syscall.Exec
func main() {
log.Printf("pid:%d\n", os.Getpid())
// cmd := exec.Command("sh")
//
// cmd.Stdin = os.Stdin
// cmd.Stderr = os.Stderr
// cmd.Stdout = os.Stdout
//
// if err := cmd.Run(); err != nil {
// log.Printf("Init Run() function err : %v\n", err)
// log.Fatal(err)
// }
command := "/bin/sh"
if err := syscall.Exec(command, []string{command}, os.Environ()); err != nil {
log.Printf("syscall.Exec err: %v\n", err)
log.Fatal(err)
}
}
执行命令:
root@nicktming:~/go/src/github.com/nicktming/mydocker/test/syscall# go run TestExec.go
2019/03/25 23:53:52 pid:20872
# echo $$
20872
总结
本节实现了容器的基本命令
run
, 一个最基本的功能.
参考
1. 自己动手写docker.(基本参考此书,加入一些自己的理解,加深对
docker
的理解)
全部内容
1. [mydocker]---环境说明
2. [mydocker]---urfave cli 理解
3. [mydocker]---Linux Namespace
4. [mydocker]---Linux Cgroup
5. [mydocker]---构造容器01-实现run命令
6. [mydocker]---构造容器02-实现资源限制01
7. [mydocker]---构造容器02-实现资源限制02
8. [mydocker]---构造容器03-实现增加管道
9. [mydocker]---通过例子理解存储驱动AUFS
10. [mydocker]---通过例子理解chroot 和 pivot_root
11. [mydocker]---一步步实现使用busybox创建容器
12. [mydocker]---一步步实现使用AUFS包装busybox
13. [mydocker]---一步步实现volume操作
14. [mydocker]---实现保存镜像
15. [mydocker]---实现容器的后台运行
16. [mydocker]---实现查看运行中容器
17. [mydocker]---实现查看容器日志
18. [mydocker]---实现进入容器Namespace
19. [mydocker]---实现停止容器
20. [mydocker]---实现删除容器
21. [mydocker]---实现容器层隔离
22. [mydocker]---实现通过容器制作镜像
23. [mydocker]---实现cp操作
24. [mydocker]---实现容器指定环境变量
25. [mydocker]---网际协议IP
26. [mydocker]---网络虚拟设备veth bridge iptables
27. [mydocker]---docker的四种网络模型与原理实现(1)
28. [mydocker]---docker的四种网络模型与原理实现(2)
29. [mydocker]---容器地址分配
30. [mydocker]---网络net/netlink api 使用解析
31. [mydocker]---网络实现
32. [mydocker]---网络实现测试