目录
总结:目前业内比较知名的有。 至少我的主人是这样的。 第一个加载返回顶部的结构。 方法实现如下: 上面的部分可以分为两部分。 在创建时调用该方法。 注意,第二个参数表示新创建的容器将是新创建的容器的第一个参数。
简介
容器运行时(容器运行时)是指管理容器和容器镜像的软件。 当今业界比较知名的一些包括 docker 和 rkt。 如果不同的运行时只能支持自己的容器,显然不利于容器技术整体的发展。 因此,2015 年 6 月,Docker 和容器领域的其他领导者共同创建了容器格式和运行时的开放行业标准,即开放容器计划(OCI)。 。 OCI具体包括两个标准:运行时标准(runtime-spec)和容器镜像标准(image-spec)。 简单来说,容器镜像标准定义了容器镜像的打包方式(打包格式),运行时标准定义了容器的运行方式。
本文包含:
runC概念及使用
runC运行容器原理分析
本文包含不:
Docker 引擎使用 runC
runC 概念
runC 是一个命令行工具,它: 用于运行容器的 OCI 标准(CLI 工具)。 这也是运行时的实现。 这个概念可能是新概念,但您计算机上的底层 Docker 基础设施可能实际上正在使用它。 至少在我的主机上是这样。
root@node-1:~# 安装 docker info....运行时:runcDefault 运行时:runc....
运行C
runC 可以与 Docker Engine 一起使用,也可以跨多个传送带使用(它本身就是一个命令行工具)。 完成以下步骤来自runC的README,
依赖项
Go版本1.6或更高版本
libseccomp库
yum install libseccomp-devel for CentOS apt-get install libseccomp-dev for Ubuntu
下载并编译
# 创建“github”GOPATH/src 目录.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包
OCI包 指一组符合标准的文件,这些文件包含所有数据。运行容器所需的并存储在包含以下两项的公共目录中:config.json:包含容器操作的配置数据
容器根文件系统
如果主机上安装了docker,docker export命令。 我有一个导出为OCI捆绑包
格式的图像
#创建顶级捆绑包目录> mkdir /mycontainer> cd /mycontainer#创建rootfs目录> mkdir rootfs#通过 Docker 将 Busybox 导出到 rootfs 目录 > docker export $(docker createzybox) | tar -C rootfs -xvf -> ls rootfs bin dev etc home proc root sys tmp usr var
根文件系统还需要 config.json、runc spec 生成一个基本模板,您可以根据该模板进行修改。
> runc spec> lsconfig.json rootfs
生成的config.json模板比较长。 这里 arg< 改为 process /strong> 和 terminal
{ "process": { "terminal":false , <[ k4]- 此处更改为 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 容器运行时的定制。 每个值都可以在运行时规范中找到。这意味着,由于OCI容器运行时支持多个平台,因此它的规范也有通用部分(写在config.md中)和平台相关部分(Linux平台、 ETC。)。 配置-linux)。
进程:指定容器启动后运行的进程执行环境。 最重要的子项是参数。 指定要运行的可执行程序。 在上面修改后的模板中,我将其更改为“sleep 5”。
root:指定容器的根文件系统。 其中 path 子项是您之前导出的根文件系统的路径。
linux:此项与平台相关。 其中,命名空间表示新创建的容器创建或使用的命名空间类型
运行容器
这里我们使用。 create 创建容器的命令
# run as root> cd /mycontainer> runc create mycontainerid
list 使用我将使用的命令进行检查。 容器状态 Created
# 显示容器已创建,且处于“已创建”状态> runc listID PID STATUS BUNDLE CREATED OWNERmycontainerid 12068created /mycontainer 2018-12-25T19:45:37.346925609Z root
start 使用命令显示容器状态
# 启动里面的进程容器> runc start mycontainerid
5秒内,使用list命令显示容器的状态为running
#显示容器正在运行在 5 秒内运行的是 runningrunc listID PID STATUS BUNDLE CREATED OWNERmycontainerid 12068 running /mycontainer 2018-12-25T19:45:37.346925609Z root
5 秒后,使用 list 检查 命令容器的状态已停止。
# 5 秒后,检查容器是否已停止,它将显示它已完成并且当前处于停止状态。 runc listID PID 状态捆绑包已创建 OWNERmycontainerid 0使用 stop /mycontainer 2018-12-25T19:45:37.346925609Z root
delete 命令删除容器
# 现在删除containerrunc删除mycontainerid
runC实现
runC可以启动和管理符合OCI标准的容器。 简单来说,runC必须使用OCI包创建一个独立的执行环境来运行指定的程序。 在 Linux 平台上,此环境指的是各种类型的命名空间和功能配置。
代码结构
runC是用Go语言实现的。 当前最新版本(2018.12)是v1.0.0-rc6。 代码的结构可以分为两大块。 一种是移动到根目录。 > 每个runC命令对应一个文件,第二个文件是libcontainer,负责创建/启动/管理容器。 runC的本质可以说在 >libcontainer
Runc create实现原理(第1部分) 以上面的例子为例,我们来看看“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. ) |- c.start(process) |- c.newParentProcess(process) |-响应parent.start()
create命令入口为它位于 create.go 中,直接关注注册操作的实现。 输入runc create mycontainerid执行注册的action并将参数保存在context中
/*将会完成。 run.go */Action: func(context *cli.Context) error { ...... spec, err := setupSpec (context) /* (sleep 5 here) */ status, err := startContainer(context, spec, CT_ACT_CREATE, nil) .....}
setupSpec:从命令行输入中搜索 -b 指定的 OCI 包目录。 。 not 该参数默认为当前目录。 读取 config.json 文件并将其内容转换为 Go 数据结构 specs.Spec。 该结构在文件 github.com/opencontainers/runtime-spec/specs-go /config 中定义。 .go的内容是按照OCI标准编写的。
sleep 5达到变量规格
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() 容器, err := createContainer(context, id, spec) r := &runner{ 容器: 容器, 操作: 操作, init: true, ... } return r.run(spec. Process)}
我们首先解释一下runC中一些重要数据结构之间的差异
容器接口
In你需要理解其中的关系。 runC,Container用于表示一个容器对象。 这是一个包含 BaseContainer 接口的抽象接口。 正如内部方法的名称所示,这些都是管理容器的基本操作。
/* libcontainer/container.go */type BaseContainer 接口 { 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 容器接口 { BaseContainer Checkpoint(criuOpts *CriuOpts) 错误恢复(process *Process, criuOpts *CriuOpts) 错误暂停() 错误恢复() 错误 NotifyOOM() (<- chan struct{}, error) NotifyMemoryPressure(levelPressureLevel) (<-chan struct{}, error)}
抽象接口需要 linuxContainer 是一个实现。 也就是说,它是当前版本的 runC 在 Linux 平台上的唯一实现。 下面是它的定义。 initPath非常重要。
type linuxContainer struct { id string config *configs.Config initPath string initArgs []string initProcessparentProcess ..... }
工厂接口
在 runC 中,所有容器都是由容器工厂创建的。 Factory也是一个抽象接口。 它定义为:仅包含四种方法。
type Factory Interface { Create(id string, config *configs.Config) (Container, error) Load(id string) (Container, error) StartInitialization() error Type () string}
Linux 平台 ---LinuxFactory 还具有 Factory 接口的标准实现。 InitPath也非常重要。 稍后会详细介绍这一点。
// LinuxFactory实现了基于Linux系统的默认工厂接口。 type LinuxFactory struct { // InitPath 是调用的路径。 // init 负责生成容器。 InitPath string ...... // InitArgs 是调用 init 职责 // 来生成容器的参数。 InitArgs []string}
换句话说,对于Linux平台,工厂创建一个容器,它实际上是一个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[ k4]keyring"), Spec: spec, RootlessEUID: os.Geteuid() != 0, RootlessCgroups: rootlessCg, }) /* 2. 加载工厂 * /Factory, err :=loadFactory(context) 如果 err != nil { return nil, err } /* 3. 调用Factory的Create()方法 */ return Factory.Create(id, config)}
可以看到,上面的代码就是It是分离的。 into
将配置保存到config,数据类型为Config.config。
加载工厂并实际返回。 >LinuxFactory
Factory
调用sleep的Create()方法。 5 你到达变量config
第1步:没有配置保存说起来容易做起来难,但是现有的规范和其他用户命令行选项设置只需将其转换为数据结构并保存。 第 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}
获得特定工厂实现后的下一步是调用: 它的Create()方法,对于Linux平台,就是下面的方法。 可以看到LinuxFactory中记录的InitPath和InitArgs被转换,分配给linuxContainer,并以 . 结果
func (l *LinuxFactory) Create(id string, config *configs).Config) (容器,错误) { .... 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() 容器, err := createContainer(context, id, spec) r := &runner{ 容器: 容器, 操作: 操作, init: true, .. . } 返回 r.run(spec.Process)}
runner的run()的输入参数是一个spec.Process结构。 注意spec.Process的定义,它的内容来自config.json文件,而spec.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 stop */ case CT_ACT_RUN: err = r.container.Run(process) /* runc run */ Default: panic("Unknown action") } ......返回状态 err }
上面的run()可以分为两部分。
调用newProcess()方法并使用spec.Process。 要创建libcontainer.Process,请注意第二个参数为true。 这意味着新创建的进程将是新创建的容器的第一个进程。
根据变量r.action的值确定如何操作获取到的libcontainer.Process
sleep 5将要。 process
libcontainer.Process结构体定义在/libcontainer/process.go中,其中大部分是spec.Process
/* 父进程 */// Process 在 //container.type 中指定进程的配置和 IO。 Process struct { Args []string Env []string User string AnotherGroups []string Cwd string Stdin io.Reader Stdout io.Writer Stderr io .Writer ExtraFiles []*os.File ConsoleWidth uint16 ConsoleHeight uint16 功能 *configs.Capability AppArmorProfile 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); /* 2. 调用 start() */ if process.Init { c.deleteExecFifo() } return err } return nil }
Start() 这个方法主要做两件事。
创建fifo:创建一个名为exec .fifopipeline的文件。 我们稍后会用到这个管子。
调用start()方法如下
func (c *linuxContainer) start(process *Process) error {parent, err := c.newParentProcess(process) /* 1.创建parentProcess */ err :=parent.start(); / * 2.启动这个parentProcess */...
start()还完成了两件事:
ParentProcess。 >
调用此ParentProcess的start()方法
sleep 5到达变量parent
那么什么是父进程? 顾名思义,父进程类似于Linux中可以派生子进程的父进程。 在runC b>中,parentProcess是一个抽象接口,如下所示:
typeparentProcessinterface { // pid 返回正在运行的进程的pid。 pid() int // start 开始运行进程。 start() error // 向进程发送 SIGKILL 并等待其完成。出口。 Terminate() error // wait 等待进程返回进程状态。 wait() (*os.ProcessState, error) // startTime 返回进程的开始时间。 startTime() (uint64, error) signal(os.Signal) error externalDescriptors() []string setExternalDescriptors(fds []string)}
这包括两个实现。 和setnsProcess。 前者用于在容器内创建第一个进程,后者用于在现有容器内创建新进程。 在创建容器的例子中,p.Init = true,所以 initProcess
func (c *linuxContainer) newParentProcess(p *Process) (parentProcess, error) {parentPipe, childPipe , err := utils.NewSockPair("init") /* 1. 创建套接字对 */ cmd, err := c.commandTemplate(p, childPipe) /* 2. 创建 *exec.Cmd */ if ! .Init { return c.newSetnsProcess(p, cmd,parentPipe, childPipe) } 如果出现错误:= c.includeExecFifo(cmd); err != nil { /* 3. 打开之前创建的 FIFO */ return nil, newSystemErrorWithCause(err, "include execfifo in cmd.Exec setup") } return c.newInitProcess( p, cmd ,parentPipe, childPipe) /* 4 . 创建一个 initProcess */}
newParentProcess() 该方法有四个步骤。 前三步都是为第四步做准备。 也就是说,它生成一个 initProcess
并创建一个 SocketPair 对。 没什么可说的。 生成的结果被放置在initProcess
中并创建*exec。 cmd的代码是: 这里,cmd运行的可执行程序和参数是从c.initPath设置的。 也就是说,设置了 c.initPath 中的“/”。 >LinuxFactoryproc/self/exe”和“init”。 这意味着新执行的程序将是runC本身,但参数将是init。 然后将外部创建的SocketPairchildPipe的一端放入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 = 附加(cmd.ExtraFiles, p.ExtraFiles...) cmd.ExtraFiles = 附加(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 中,并 _LIBCONTAINER_FIFOFD= Set %d记录到cmd.Env。
最后一步是创建一个InitProcess。 在这里,我们首先需要将 _LIBCONTAINER_INITTYPE="standard" 添加到 cmd.Env 中,然后从 configs 读取名称创建一个新容器。 。指定节奏类型并将其打包成变量数据供以后使用。 最后,创建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 }返回&initProcess{ cmd:cmd,childPipe:childPipe,parentPipe:parentPipe,经理:c.cgroupManager, intelRdtManager: c.intelRdtManager, config: c.newInitConfig(p), 容器: c, process: p, /* 在这里休眠 5*/ bootstrapData: data, sharePidns: sharePidns, }, nil}
sleep 5 initProcess.process
返回linuxContainer的start()方法并创建parent 我会的。 >strong>,下一步是调用它的start()方法。
func (c *linuxContainer) start(process *Process) error {parent, err := c.newParentProcess (process) /* 1.创建parentProcess(已完成) */ err :=parent.start ( ); /* 2. 启动这个父进程 */ ......
评论前必须登录!
注册