容器運行時(Container Runtime
)是指管理容器和容器鏡像的軟件。當前業內比較有名的有docker,rkt等。若是不一樣的運行時只能支持各自的容器,那麼顯然不利於整個容器技術的發展。因而在2015年6月,由Docker以及其餘容器領域的領導者共同創建了圍繞容器格式和運行時的開放的工業化標準,即Open Container Initiative
(OCI
),OCI
具體包含兩個標準:運行時標準(runtime-spec)和容器鏡像標準(image-spec)。簡單來講,容器鏡像標準定義了容器鏡像的打包形式(pack format
),而運行時標準定義瞭如何去運行一個容器。node
本文包含如下內容:linux
本文不包含如下內容:git
runC是一個遵循OCI標準的用來運行容器的命令行工具(CLI Tool),它也是一個Runtime的實現。儘管你可能對這個概念很陌生,但實際上,你的電腦上的docker底層可能正在使用它。至少在筆者的主機上是這樣。github
root@node-1:~# docker info ..... Runtimes: runc Default Runtime: runc .....
runC
不只能夠被docker engine
使用,它也能夠單獨使用(它自己就是命令行工具),如下使用步驟徹底來自runC's README,若是docker
libseccomp庫shell
yum install libseccomp-devel for CentOS apt-get install libseccomp-dev for Ubuntu
# 在GOPATH/src目錄建立'github.com/opencontainers'目錄 > cd github.com/opencontainers > git clone https://github.com/opencontainers/runc > cd runc > make > sudo make install
或者使用go get
安裝json
# 在GOPATH/src目錄建立github.com目錄 > go get github.com/opencontainers/runc > cd $GOPATH/src/github.com/opencontainers/runc > make > sudo make install
以上步驟完成後,runC
將安裝在/usr/local/sbin/runc
目錄bootstrap
OCI Bundle
是指知足OCI標準的一系列文件,這些文件包含了運行容器所須要的全部數據,它們存放在一個共同的目錄,該目錄包含如下兩項:bash
若是主機上安裝了docker,那麼可使用docker export
命令將已有鏡像導出爲OCI Bundle
的格式數據結構
# create the top most bundle directory > mkdir /mycontainer > cd /mycontainer # create the rootfs directory > mkdir rootfs # export busybox via Docker into the rootfs directory > docker export $(docker create busybox) | tar -C rootfs -xvf - > ls rootfs bin dev etc home proc root sys tmp usr var
有了root filesystem,還須要config.json,runc spec
能夠生成一個基礎模板,以後咱們能夠在模板基礎上進行修改。
> runc spec > ls config.json rootfs
生成的config.json模板比較長,這裏我將它process中的arg 和 terminal進行修改
{ "process": { "terminal":false, <-- 這裏改成 true "user": { "uid": 0, "gid": 0 }, "args": [ "sh" <-- 這裏改成 "sleep","5" ], "env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "TERM=xterm" ], "cwd": "/", }, "root": { "path": "rootfs", "readonly": true }, "linux": { "namespaces": [ { "type": "pid" }, { "type": "network" }, { "type": "ipc" }, { "type": "uts" }, { "type": "mount" } ], } }
config.json 文件的內容都是 OCI Container Runtime 的訂製,其中每一項值均可以在Runtime Spec找到具體含義,OCI Container Runtime 支持多種平臺,所以其 Spec 也分爲通用部分(在config.md中描述)以及平臺相關的部分(如linux平臺上就是config-linux)
process
:指定容器啓動後運行的進程運行環境,其中最重要的的子項就是args,它指定要運行的可執行程序, 在上面的修改後的模板中,咱們將其改爲了"sleep 5"root
:指定容器的根文件系統,其中path子項是指向前面導出的中root filesystem的路徑linux
: 這一項是平臺相關的。其中namespaces表示新建立的容器會額外建立或使用的namespace的類型如今咱們使用create
命令建立容器
# run as root > cd /mycontainer > runc create mycontainerid
使用list
命令查看容器狀態爲created
# view the container is created and in the "created" state > runc list ID PID STATUS BUNDLE CREATED OWNER mycontainerid 12068 created /mycontainer 2018-12-25T19:45:37.346925609Z root
使用start
命令查看容器狀態
# start the process inside the container > runc start mycontainerid
在5s內 使用list
命令查看容器狀態爲running
# within 5 seconds view that the container is running runc list ID PID STATUS BUNDLE CREATED OWNER mycontainerid 12068 running /mycontainer 2018-12-25T19:45:37.346925609Z root
在5s後 使用list
命令查看容器狀態爲stopped
# after 5 seconds view that the container has exited and is now in the stopped state runc list ID PID STATUS BUNDLE CREATED OWNER mycontainerid 0 stopped /mycontainer 2018-12-25T19:45:37.346925609Z root
使用delete
命令能夠刪除容器
# now delete the container runc delete mycontainerid
runC
能夠啓動並管理符合OCI標準的容器。簡單地說,runC
須要利用OCI bundle
建立一個獨立的運行環境,並執行指定的程序。在Linux平臺上,這個環境就是指各類類型的Namespace
以及Capability
等等配置
runC
由Go語言實現,當前(2018.12)最新版本是v1.0.0-rc6,代碼的結構可分爲兩大塊,一是根目錄下的go文件,對應各個runC
命令,二是負責建立/啓動/管理容器的libcontainer
,能夠說runC
的本質都在libcontainer
以上面的例子爲例,以'runc create'這條命令來看runC
是如何完成從無到有建立容器,並運行用戶指定的 'sleep 5' 這個進程的。
建立容器,運行 sleep 5 就是咱們的目標,請牢記
本文涉及的調用關係以下,可隨時翻閱
setupSpec(context) startContainer(context, spec, CT_ACT_CREATE, nil) |- createContainer |- specconv.CreateLibcontainerConfig |- loadFactory(context) |- libcontainer.New(......) |- factory.Create(id, config) |- runner.run(spec.Process) |- newProcess(*config, r.init) |- r.container.Start(process) |- c.createExecFifo() |- c.start(process) |- c.newParentProcess(process) |- parent.start()
create
命令的響應入口在 create.go, 咱們直接關注其註冊的Action的實現,當輸入runc create mycontainerid
時會執行註冊的Action,而且參數存放在Context中
/* run.go */ Action: func(context *cli.Context) error { ...... spec, err := setupSpec(context) /* (sleep 5 在這裏) */ status, err := startContainer(context, spec, CT_ACT_CREATE, nil) ..... }
setupSpec
:從命令行輸入中找到-b 指定的 OCI bundle 目錄,若沒有此參數,則默認是當前目錄。讀取config.json文件,將其中的內容轉換爲Go的數據結構specs.Spec,該結構定義在文件 github.com/opencontainers/runtime-spec/specs-go/config.go,裏面的內容都是OCI標準描述的。sleep 5 到了變量 spec
startContainer
:嘗試建立啓動容器,注意這裏的第三個參數是 CT_ACT_CREATE, 表示僅建立容器。本文使用linux平臺,所以實際調用的是 utils_linux.go 中的startContainer()。startContainer()根據用戶將用戶輸入的 id 和剛纔的獲得的 spec 做爲輸入,調用 createContainer() 方法建立容器,再經過一個runner.run()方法啓動它/× utils_linux.go ×/ func startContainer(context *cli.Context, spec *specs.Spec, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) { id := context.Args().First() container, err := createContainer(context, id, spec) r := &runner{ container: container, action: action, init: true, ...... } return r.run(spec.Process) }
這裏須要先了解下runC
中的幾個重要數據結構的關係
在runC
中,Container用來表示一個容器對象,它是一個抽象接口,它內部包含了BaseContainer接口。從其內部的方法的名字就能夠看出,都是管理容器的基本操做
/* libcontainer/container.go */ type BaseContainer interface { ID() string Status() (Status, error) State() (*State, error) Config() configs.Config Processes() ([]int, error) Stats() (*Stats, error) Set(config configs.Config) error Start(process *Process) (err error) Run(process *Process) (err error) Destroy() error Signal(s os.Signal, all bool) error Exec() error } /* libcontainer/container_linux.go */ type Container interface { BaseContainer Checkpoint(criuOpts *CriuOpts) error Restore(process *Process, criuOpts *CriuOpts) error Pause() error Resume() error NotifyOOM() (<-chan struct{}, error) NotifyMemoryPressure(level PressureLevel) (<-chan struct{}, error) }
有了抽象接口,那麼必定有具體的實現,linuxContainer 就是一個實現,或者說,它是當前版本runC
在linux平臺上的惟一一種實現。下面是其定義,其中的 initPath 很是關鍵
type linuxContainer struct { id string config *configs.Config initPath string initArgs []string initProcess parentProcess ..... }
在runC
中,全部的容器都是由容器工廠(Factory)建立的, Factory 也是一個抽象接口,定義以下,它只包含了4個方法
type Factory interface { Create(id string, config *configs.Config) (Container, error) Load(id string) (Container, error) StartInitialization() error Type() string }
linux平臺上的對 Factory 接口也有一個標準實現---LinuxFactory,其中的 InitPath 也很是關鍵,稍後咱們會看到
// LinuxFactory implements the default factory interface for linux based systems. type LinuxFactory struct { // InitPath is the path for calling the init responsibilities for spawning // a container. InitPath string ...... // InitArgs are arguments for calling the init responsibilities for spawning // a container. InitArgs []string }
因此,對於linux平臺,Factory 建立 Container 實際上就是 LinuxFactory 建立 linuxContainer
回到createContainer(),下面是其實現
func createContainer(context *cli.Context, id string, spec *specs.Spec) (libcontainer.Container, error) { /* 1. 將配置存放到config */ rootlessCg, err := shouldUseRootlessCgroupManager(context) config, err := specconv.CreateLibcontainerConfig(&specconv.CreateOpts{ CgroupName: id, UseSystemdCgroup: context.GlobalBool("systemd-cgroup"), NoPivotRoot: context.Bool("no-pivot"), NoNewKeyring: context.Bool("no-new-keyring"), Spec: spec, RootlessEUID: os.Geteuid() != 0, RootlessCgroups: rootlessCg, }) /* 2. 加載Factory */ factory, err := loadFactory(context) if err != nil { return nil, err } /* 3. 調用Factory的Create()方法 */ return factory.Create(id, config) }
能夠看到,上面的代碼大致上分爲
sleep 5 到了變量 config
第1步存放配置沒什麼好說的,無非是將已有的 spec 和其餘一些用戶命令行選項配置換成一個數據結構存下來。而第2部加載Factory,在linux上,就是返回一個 LinuxFactory 結構。而這是經過在其內部調用 libcontainer.New()方法實現的
/* utils/utils_linux.go */ func loadFactory(context *cli.Context) (libcontainer.Factory, error) { ..... return libcontainer.New(abs, cgroupManager, intelRdtManager, libcontainer.CriuPath(context.GlobalString("criu")), libcontainer.NewuidmapPath(newuidmap), libcontainer.NewgidmapPath(newgidmap)) }
libcontainer.New() 方法在linux平臺的實現以下,能夠看到,它的確會返回一個LinuxFactory,而且InitPath設置爲"/proc/self/exe",InitArgs設置爲"init"
/* libcontainer/factory_linux.go */ func New(root string, options ...func(*LinuxFactory) error) (Factory, error) { ..... l := &LinuxFactory{ ..... InitPath: "/proc/self/exe", InitArgs: []string{os.Args[0], "init"}, } ...... return l, nil }
獲得了具體的 Factory 實現,下一步就是調用其Create()方法,對 linux 平臺而言,就是下面這個方法,能夠看到,它會將 LinuxFactory 上記錄的 InitPath 和 InitArgs 賦給 linuxContainer 並做爲結果返回
func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, error) { .... c := &linuxContainer{ id: id, config: config, initPath: l.InitPath, initArgs: l.InitArgs, } ..... return c, nil }
回到 startContainer() 方法,再獲得 linuxContainer 後,將建立一個 runner 結構,並調用其run()方法
/* utils_linux.go */ func startContainer(context *cli.Context, spec *specs.Spec, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) { id := context.Args().First() container, err := createContainer(context, id, spec) r := &runner{ container: container, action: action, init: true, ...... } return r.run(spec.Process) }
runner 的 run() 的入參是 spec.Process 結構,咱們並不須要關注它的定義,由於它的內容都來源於 config.json 文件,spec.Process 不過是其中 Process 部分的 Go 語言數據的表示。run() 方法的實現以下:
func (r *runner) run(config *specs.Process) (int, error) { ...... process, err := newProcess(*config, r.init) /* 第1部分 */ ...... switch r.action { case CT_ACT_CREATE: err = r.container.Start(process) /* runc start */ /* 第2部分 */ case CT_ACT_RESTORE: err = r.container.Restore(process, r.criuOpts) /* runc restore */ case CT_ACT_RUN: err = r.container.Run(process) /* runc run */ default: panic("Unknown action") } ...... return status, err }
上面的 run() 可分爲兩部分
sleep 5 到了變量 process
libcontainer.Process 結構定義在 /libcontainer/process.go, 其中大部份內容都來自 spec.Process
/* parent process */ // Process specifies the configuration and IO for a process inside // a container. type Process struct { Args []string Env []string User string AdditionalGroups []string Cwd string Stdin io.Reader Stdout io.Writer Stderr io.Writer ExtraFiles []*os.File ConsoleWidth uint16 ConsoleHeight uint16 Capabilities *configs.Capabilities AppArmorProfile string Label string NoNewPrivileges *bool Rlimits []configs.Rlimit ConsoleSocket *os.File Init bool ops processOperations }
接下來就是要使用 Start() 方法了
func (c *linuxContainer) Start(process *Process) error { if process.Init { if err := c.createExecFifo(); err != nil { /* 1.建立fifo */ return err } } if err := c.start(process); err != nil { /* 2. 調用start() */ if process.Init { c.deleteExecFifo() } return err } return nil }
Start() 方法主要完成兩件事
exec.fifo
的管道,這個管道後面會用到func (c *linuxContainer) start(process *Process) error { parent, err := c.newParentProcess(process) /* 1. 建立parentProcess */ err := parent.start(); /* 2. 啓動這個parentProcess */ ......
start() 也完成兩件事:
sleep 5 到了變量 parent
那麼什麼是 parentProcess ? 正如其名,parentProcess 相似於 linux 中能夠派生出子進程的父進程,在runC
中,parentProcess 是一個抽象接口,以下:
type parentProcess interface { // pid returns the pid for the running process. pid() int // start starts the process execution. start() error // send a SIGKILL to the process and wait for the exit. terminate() error // wait waits on the process returning the process state. wait() (*os.ProcessState, error) // startTime returns the process start time. startTime() (uint64, error) signal(os.Signal) error externalDescriptors() []string setExternalDescriptors(fds []string) }
它有兩個實現,分別爲 initProcess
和 setnsProcess
,前者用於建立容器內的第一個進程,後者用於在已有容器內建立新的進程。在咱們的建立容器例子中,p.Init = true ,因此會建立 initProcess
func (c *linuxContainer) newParentProcess(p *Process) (parentProcess, error) { parentPipe, childPipe, err := utils.NewSockPair("init") /* 1.建立 Socket Pair */ cmd, err := c.commandTemplate(p, childPipe) /* 2. 建立 *exec.Cmd */ if !p.Init { return c.newSetnsProcess(p, cmd, parentPipe, childPipe) } if err := c.includeExecFifo(cmd); err != nil { /* 3.打開以前建立的fifo */ return nil, newSystemErrorWithCause(err, "including execfifo in cmd.Exec setup") } return c.newInitProcess(p, cmd, parentPipe, childPipe) /* 4.建立 initProcess */ }
newParentProcess() 方法動做有 4 步,前 3 步都是在爲第 4 步作準備,即生成 initProcess
runC
自己,只是參數變成了 init
,以後又將外面建立的 SocketPair 的一端 childPipe放到了cmd.ExtraFiles ,同時將_LIBCONTAINER_INITPIPE=%d
加入cmd.Env,其中 %d
爲文件描述符的數字func (c *linuxContainer) commandTemplate(p *Process, childPipe *os.File) (*exec.Cmd, error) { cmd := exec.Command(c.initPath, c.initArgs[1:]...) cmd.Args[0] = c.initArgs[0] cmd.ExtraFiles = append(cmd.ExtraFiles, p.ExtraFiles...) cmd.ExtraFiles = append(cmd.ExtraFiles, childPipe) cmd.Env = append(cmd.Env, fmt.Sprintf("_LIBCONTAINER_INITPIPE=%d", stdioFdCount+len(cmd.ExtraFiles)-1), ) ...... return cmd, nil }
_LIBCONTAINER_FIFOFD=%d
記錄到 cmd.Env。_LIBCONTAINER_INITTYPE="standard"
加入cmd.Env,而後從 configs 讀取須要新的容器建立的 Namespace 的類型,並將其打包到變量 data 中備用,最後再建立 InitProcess 本身,能夠看到,這裏將以前的一些資源和變量都聯繫了起來func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, parentPipe, childPipe *os.File) (*initProcess, error) { cmd.Env = append(cmd.Env, "_LIBCONTAINER_INITTYPE="+string(initStandard)) nsMaps := make(map[configs.NamespaceType]string) for _, ns := range c.config.Namespaces { if ns.Path != "" { nsMaps[ns.Type] = ns.Path } } _, sharePidns := nsMaps[configs.NEWPID] data, err := c.bootstrapData(c.config.Namespaces.CloneFlags(), nsMaps) if err != nil { return nil, err } return &initProcess{ cmd: cmd, childPipe: childPipe, parentPipe: parentPipe, manager: c.cgroupManager, intelRdtManager: c.intelRdtManager, config: c.newInitConfig(p), container: c, process: p, /* sleep 5 在這裏 */ bootstrapData: data, sharePidns: sharePidns, }, nil }
sleep 5 在 initProcess.process 中
回到 linuxContainer 的 start() 方法,建立好了 parent ,下一步就是調用它的 start() 方法了
func (c *linuxContainer) start(process *Process) error { parent, err := c.newParentProcess(process) /* 1. 建立parentProcess (已完成) */ err := parent.start(); /* 2. 啓動這個parentProcess */ ......