内容简介:容器运行时(本文包含以下内容:本文
前言
容器运行时( Container Runtime
)是指管理容器和容器镜像的软件。当前业内比较有名的有 docker
, rkt
等。如果不同的运行时只能支持各自的容器,那么显然不利于整个容器技术的发展。于是在2015年6月,由 Docker 以及其他容器领域的领导者共同建立了围绕容器格式和运行时的开放的工业化标准,即 Open Container Initiative
( OCI
), OCI
具体包含两个标准:运行时标准( runtime-spec
)和容器镜像标准( image-spec
)。简单来说,容器镜像标准定义了容器镜像的打包形式( pack format
),而运行时标准定义了如何去运行一个容器。
本文包含以下内容:
- runC的概念和使用
- runC运行容器的原理剖析
本文 不 包含以下内容:
- docker engine使用runC
runC概念
runC 是一个遵循OCI标准的用来运行容器的命令行工具(CLI Tool),它也是一个Runtime的实现。尽管你可能对这个概念很陌生,但实际上,你的电脑上的docker底层可能正在使用它。至少在笔者的主机上是这样。
root@node-1:~# docker info ..... Runtimes: runc Default Runtime: runc .....
安装runC
runC
不仅可以被 docker engine
使用,它也可以单独使用(它本身就是命令行工具),以下使用步骤完全来自 runC's README
,如果
依赖项
- Go version 1.6或更高版本
-
libseccomp库
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
安装
# 在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
目录
使用runC
创建一个OCI Bundle
OCI Bundle
是指满足OCI标准的一系列文件,这些文件包含了运行容器所需要的所有数据,它们存放在一个共同的目录,该目录包含以下两项:
- config.json:包含容器运行的配置数据
- container 的 root filesystem
如果主机上安装了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 的实现原理
runC
可以启动并管理符合OCI标准的容器。简单地说, runC
需要利用 OCI bundle
创建一个独立的运行环境,并执行指定的程序。在Linux平台上,这个环境就是指各种类型的 Namespace
以及 Capability
等等配置
代码结构
runC
由 Go 语言实现,当前(2018.12)最新版本是 v1.0.0-rc6
,代码的结构可分为两大块,一是根目录下的 go
文件,对应各个 runC
命令,二是负责创建/启动/管理容器的 libcontainer
,可以说 runC
的本质都在 libcontainer
runc create的过程
以上面的例子为例,以'runc create'这条命令来看 runC
是如何完成从无到有创建容器
create
命令的响应入口在 create.go, 我们直接关注其注册的 Action
的实现,当输入 runc create mycontainerid
时会执行注册的 Action
,并且参数存放在 Context
中
/* run.go */ Action: func(context *cli.Context) error { ...... spec, err := setupSpec(context) 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标准描述的 -
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
中的几个重要数据结构的关系
Container 接口
在 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 ..... }
Factory 接口
在 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) }
可以看到,上面的代码大体上分为
- 将配置存放到config
- 加载Factory
- 调用Factory的Create()方法
第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) ...... switch r.action { case CT_ACT_CREATE: err = r.container.Start(process) /* runc start */ 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() 可分为两部分
- 调用 newProcess() 方法, 用 spec.Process 创建 libcontainer.Process ,注意第二个参数是 true ,表示新创建的 process 会作为新创建容器的第一个 process
- 根据 r.action 的值决定如何操作得到的 libcontainer.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()方法主要完成两件事
-
创建 fifo: 创建一个名为
exec.fifo
的管道,这个管道后面会用到 - 调用 start() 方法,如下
func (c *linuxContainer) start(process *Process) error { parent, err := c.newParentProcess(process) /* 1. 创建parentProcess */ err := parent.start(); /* 2. 启动这个parentProcess */ ......
start()也完成两件事:
- 创建一个 ParentProcess
- 调用这个 ParentProcess 的 start() 方法
那么什么是 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
- 创建一对 SocketPair 没什么好说的,生成的结果会放到 initProcess
-
创建 *exec.Cmd,代码如下,这里设置了 cmd
要执行的可执行程序和参数来自 c.initPath
,即源自 LInuxFactory
的 "/proc/self/exe",和 "init" ,这表示新执行的程序就是
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 }
- includeExecFifo() 方法打开之前创建的 fifo,也将其 fd 放到 cmd.ExtraFiles 中。
-
最后就是创建 InitProcess
了,这里首先将
_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, bootstrapData: data, sharePidns: sharePidns, }, nil }
回到 linuxContainer 的 start() 方法,创建好了 parent ,下一步就是调用它的 start() 方法了
func (c *linuxContainer) start(process *Process) error { parent, err := c.newParentProcess(process) /* 1. 创建parentProcess (已完成) */ err := parent.start(); /* 2. 启动这个parentProcess */ ......
----- 待续
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
HTML 压缩/解压工具
在线压缩/解压 HTML 代码
HEX HSV 转换工具
HEX HSV 互换工具