linux run c,探索 runC (上)

前言

容器运行时(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,

"user": {

"uid": 0,

"gid": 0

},

"args": [

"sh"

],

"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

bVblHSQ?w=599&h=517

runc create 实现原理 (上)

以上面的例子为例,以'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中的几个重要数据结构的关系

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() (

NotifyMemoryPressure(level PressureLevel) (

}

有了抽象接口,那么一定有具体的实现,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, 数据类型是 Config.config

加载 Factory,实际返回 LinuxFactory

调用 Factory 的Create()方法

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() 可分为两部分

调用 newProcess() 方法, 用 spec.Process 创建 libcontainer.Process,注意第二个参数是 true ,表示新创建的 process 会作为新创建容器的第一个 process。

根据 r.action 的值决定如何操作得到的 libcontainer.Process

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() 方法主要完成两件事

创建 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() 方法

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

创建一对 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 中,同时将_LIBCONTAINER_FIFOFD=%d记录到 cmd.Env。

最后就是创建 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, /* 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 */

......

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值