(4)构造容器之实现run命令版的容器
linux proc
linux 下的/proc 文件系统是又内核提供的,包含了系统运行时信息(系统内存、mount 设备信息,硬件配置等),为访问内核数据提供接口。
实现runC
项目结构
项目结构.pnghttps://github.com/justinmjc/mymoby
tag:3.1节
代码解析
main.go
package main
import (
"github.com/urfave/cli"
"github.com/Sirupsen/logrus"
"log"
"os"
)
const usage = `my moby is a simple contaniner runtime implementation.`
func main() {
app :=cli.NewApp()
app.Name = "mymoby"
app.Usage=usage
app.Commands = []cli.Command{
initCommand,
runCommand,
}
app.Before = func(context *cli.Context) error {
logrus.SetFormatter(&logrus.JSONFormatter{})
log.SetOutput(os.Stdout)
return nil
}
if err := app.Run(os.Args); err!=nil{
log.Fatal(err)
}
}
使用github.com/urfave/cli提供的命令行工具,定义了mymoby的基本命令initCommand和runCommand,然后在app.Before 初始化日志配置。
接下来开下initCommand和runCommand的定义,在main_command.go文件中
main_command.go
package main
import (
"github.com/urfave/cli"
"fmt"
log "github.com/Sirupsen/logrus"
"./container"
)
var runCommand = cli.Command{
Name:"run",
Usage:`Create a container with namespace and cgroups limit mymoby run -ti [command]`,
Flags:[]cli.Flag{
cli.BoolFlag{
Name:"ti",
Usage:"enable tty",
},
},
/*
这里是run命令执行的真正函数。
1判断参数是否包含command
2获取用户指定的command
3调用Run function去准备启动容器
*/
Action: func(context *cli.Context) error{
if len(context.Args())<1{
return fmt.Errorf("Missing container command")
}
cmd := context.Args().Get(0)
tty := context.Bool("ti")
Run(tty,cmd)
return nil
},
}
var initCommand = cli.Command{
Name: "init",
Usage: "Init container process run user's process in container.Do not call it outside",
/*
1获取传递过来的command参数
2执行容器初始化操作
*/
Action: func(context *cli.Context) error {
log.Infof("init come on")
cmd := context.Args().Get(0)
err :=container.RunContainerInitProcess(cmd, nil)
return err
},
}
runCommand中的Run在run.go中定义
package main
import (
log "github.com/Sirupsen/logrus"
"./container"
"os"
)
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)
}
Run 调用NewParentProcess(),在container_process.go中定义
package container
import (
"os/exec"
"syscall"
"os"
)
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
}
在NewParentProcess()中“/proc/self”指的是当前运行进程自己的环境,exec其实是自己调用自己,通过这种方式对创建出来的进程进行初始化。
args的第一个参数"init",实际上就是会去调用initCommand进行初始化操作
下面的clone参数就是去fork出来一个新进程,使用namespace隔离环境
Run执行完在NewParentProcess()后执行parent.Start(),Start方法是真正前面创建好的command的调用,它首先clone出一个Namespace隔离的进程,然后在子进程中调用/proc/self/exe,发送init,初始化容器的一些资源。
最后,运行
maojiancai@bogon:~/mygo/mymoby$ go build .
maojiancai@bogon:~/mygo/mymoby$ ls
container main_command.go main.go mymoby README.md run.go src
maojiancai@bogon:~/mygo/mymoby$ ./mymoby run -ti /bin/sh
{"level":"error","msg":"fork/exec /proc/self/exe: operation not permitted","time":"2018-01-02T06:56:12-08:00"}
maojiancai@bogon:~/mygo/mymoby$ sudo ./mymoby run -ti /bin/sh
[sudo] password for maojiancai:
{"level":"info","msg":"init come on","time":"2018-01-02T06:56:44-08:00"}
{"level":"info","msg":"command %s/bin/sh","time":"2018-01-02T06:56:44-08:00"}
# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 06:56 pts/0 00:00:00 /bin/sh
root 4 1 0 06:56 pts/0 00:00:00 ps -ef
#
在init.go 中调用的syscall.Exec方法,最终调用了Kernel的execve 这个系统函数。它的作用是执行当前filename对应的程序。他会覆盖当前进行的镜像、数据和堆栈信息,PID,这些都会被将要运行的程序覆盖。