原書代碼
https://github.com/xianlubird/mydocker.git
#code-3.1
Linux Proc
Linux 下的/proc 文件系統(tǒng)是由內(nèi)核提供,它其實不是一個真正的文件系統(tǒng),只包含了系統(tǒng)運行時信息(比如系統(tǒng)內(nèi)存,mount 設備信息,一些硬件配置等等,它只存在于內(nèi)存中,而不占用外存空間。它是以文件系統(tǒng)的形式為訪問內(nèi)核數(shù)據(jù)的操作提供接口。
比如說lsmod就和cat /proc/modules是等效的
root@taroballs-PC:~# ls /proc/
1 1284 1524 1802 2103 31 430 514 cpuinfo modules
10 1294 1525 1815 2144 312 4332 515 crypto mounts
1005 13 1529 1818 2148 318 435 516 devices mtrr
1030 1312 153 1826 22 32 436 517 diskstats net
1031 1320 1530 1832 221 3237 437 518 dma pagetypeinfo
1032 1346 154 1837 225 3241 438 525 driver partitions
當你去遍歷這個目錄的時候會發(fā)現(xiàn)很多數(shù)字,這些都是為每個進程創(chuàng)建的空間,數(shù)字就是他們的 PID。
| 重要術語 | 相關說明 |
|---|---|
| /proc/N | pid為N的進程信息 |
| /proc/N/cmdline | 進程啟動命令 |
| /proc/N/cwd | 鏈接到進程當前工作目錄 |
| /proc/N/environ | 進程環(huán)境變量列表 |
| /proc/N/exe | 鏈接到進程的執(zhí)行命令文件 |
| /proc/N/fd | 包含進程相關的所有的文件描述符 |
| /proc/N/maps | 與進程相關的內(nèi)存映射信息 |
| /proc/N/mem | 指代進程持有的內(nèi)存,不可讀 |
| /proc/N/root | 鏈接到進程的根目錄 |
| /proc/N/stat | 進程的狀態(tài) |
| /proc/N/statm | 進程使用的內(nèi)存的狀態(tài) |
| /proc/N/status | 進程狀態(tài)信息,比stat/statm更具可讀性 |
| /proc/self | 鏈接到當前正在運行的進程 |
實現(xiàn) run 命令
實現(xiàn)一個簡單版本的run命令,類似docker run -ti [command]
代碼目錄結構如下:
root@taroballs-PC:~# tree mydocker/ -L 2
mydocker/
├── container
│ ├── container_process.go
│ └── init.go
├── Godeps
│ ├── Godeps.json
│ └── Readme
├── main_command.go
├── main.go
├── run.go
└── vendor
├── github.com
└── golang.org
首先分析下main.go函數(shù)寫了些什么:
package main
import (
log "github.com/Sirupsen/logrus"
"github.com/urfave/cli"http://這個包提供了命令行工具
"os"
)
const usage = `mydocker is a simple container runtime implementation.
The purpose of this project is to learn how docker works and how to write a docker by ourselves
Enjoy it, just for fun.`
func main() {
app := cli.NewApp()
app.Name = "mydocker"
app.Usage = usage
//暫時定義兩個命令init、run
app.Commands = []cli.Command{
initCommand,
runCommand,
}
//`app.Before` 內(nèi)初始化了一下`logrus`的日志配置。
app.Before = func(context *cli.Context) error {
// Log as JSON instead of the default ASCII formatter.
log.SetFormatter(&log.JSONFormatter{})
log.SetOutput(os.Stdout)
return nil
}
//運行出錯時 記錄日志
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
分別看下兩條命令的具體定義,查看main_command.go文件
runcommand命令實現(xiàn)
//main_command.go
var runCommand = cli.Command{
Name: "run",
Usage: `Create a container with namespace and cgroups limit
mydocker run -ti [command]`,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ti",
Usage: "enable tty",
},
},
//這里是run命令執(zhí)行的真正函數(shù)
Action: func(context *cli.Context) error {
if len(context.Args()) < 1 {//判斷是否包含參數(shù)
return fmt.Errorf("Missing container command")
}
cmd := context.Args().Get(0)//獲取參數(shù)
tty := context.Bool("ti")
Run(tty, cmd)//調(diào)用Run方法去準備啟動容器
return nil
},
}

先來看看Run函數(shù)做了些什么:

//run函數(shù)在run.go中
func Run(tty bool, command string) {
parent := container.NewParentProcess(tty, command)
if err := parent.Start(); err != nil {
log.Error(err)
}
parent.Wait()
os.Exit(-1)
}
解釋一下:

讓我們看一下NewParentProcess函數(shù)都寫了些什么
//container/container_process.go
func NewParentProcess(tty bool, command string) *exec.Cmd {
args := []string{"init", command}
cmd := exec.Command("/proc/self/exe", args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
}
if tty {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
return cmd
}
解釋一下:

接著看看返回cmd之后的調(diào)用:

查看InitCommand命令的具體實現(xiàn)
//main_command.go
//此方法為內(nèi)部操作,禁止外部調(diào)用
var initCommand = cli.Command{
Name: "init",
Usage: "Init container process run user's process in container. Do not call it outside",
Action: func(context *cli.Context) error {
log.Infof("init come on")
cmd := context.Args().Get(0)//獲取傳遞過來的參數(shù)
log.Infof("command %s", cmd)//寫入日志
err := container.RunContainerInitProcess(cmd, nil)//執(zhí)行容器初始化操作
return err
},
}

那么這里看看RunContainerInitProcess函數(shù)做了些什么
//container/init.go
func RunContainerInitProcess(command string, args []string) error {
logrus.Infof("command %s", command)
defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), "")
argv := []string{command}
if err := syscall.Exec(command, argv, os.Environ()); err != nil {
logrus.Errorf(err.Error())
}
return nil
}
解釋一下先:

這里的MountFlag的意思如下
- MS_NOEXEC 在本文件系統(tǒng)中不允許運行其他程序
- MS_NOSUID 在本系統(tǒng)中運行程序的時候不允許
set-user-ID或者set-group-ID - MS_NODEV 這個參數(shù)是自從Linux 2.4以來所有 mount 的系統(tǒng)都會默認設定的參數(shù)
解釋一下

——————
運行一下:
#記得先在GOPATH準備兩個包
git clone https://github.com/Sirupsen/logrus.git
git clone https://github.com/urfave/cli.git
#記得移動到GOPATH跑程序
Result
root@taroballs-PC:/home/taroballs/go/src/github.com/xianlubird/mydocker# ./mydocker run -ti /bin/sh
{"level":"info","msg":"init come on","time":"2018-02-01T00:47:22+08:00"}
{"level":"info","msg":"command /bin/sh","time":"2018-02-01T00:47:22+08:00"}
{"level":"info","msg":"command /bin/sh","time":"2018-02-01T00:47:22+08:00"}
# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 00:47 pts/0 00:00:00 /bin/sh
root 6 1 0 00:47 pts/0 00:00:00 ps -ef
#
對比一下運行docker鏡像容器
root@taroballs-PC:~# docker run -ti ubuntu /bin/sh
# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 16:51 pts/0 00:00:00 /bin/sh
root 5 1 0 16:51 pts/0 00:00:00 ps -ef
#
是不是相類似呢?在運行個/bin/ls試試看
root@taroballs-PC:/home/taroballs/go/src/github.com/xianlubird/mydocker# ./mydocker run -ti /bin/ls
{"level":"info","msg":"init come on","time":"2018-02-01T00:54:45+08:00"}
{"level":"info","msg":"command /bin/ls","time":"2018-02-01T00:54:45+08:00"}
{"level":"info","msg":"command /bin/ls","time":"2018-02-01T00:54:45+08:00"}
container main_command.go mydocker README.md vendor
Godeps main.go network run.go
root@taroballs-PC:/home/taroballs/go/src/github.com/xianlubird/mydocker#
#由于我們沒有`chroot`,所以目前我們的系統(tǒng)文件系統(tǒng)是繼承自我們的父進程的,這里我們運行了一下`ls`命令,發(fā)現(xiàn)容器啟動起來以后,打印出來了當前目錄的內(nèi)容,然后退出了.