OCI 和 runc:容器標準化和 docker

ROSJacob 7年前發布 | 33K 次閱讀 Docker

OCI 和容器標準

容器技術隨著 docker 的出現炙手可熱,所有的技術公司都積極擁抱容器,促進了 docker 容器的繁榮發展。 容器 一詞雖然口口相傳,但卻沒有統一的定義,這不僅是個技術概念的問題,也給整個社區帶來一個陰影:容器技術的標準到底是什么?由誰來決定?

很多人可能覺得 docker 已經成為了容器的事實標準,那我們以它作為標準問題就解決了。事情并沒有那么簡單,首先是否表示容器完全等同于 docker,不允許存在其他的容器運行時(比如 coreOS 推出的 rkt);其次容器上層抽象(容器集群調度,比如 kubernetes、mesos 等)和 docker 緊密耦合,docker 接口的變化將會導致它們無法使用。

總的來說,如果容器以 docker 作為標準,那么 docker 接口的變化將導致社區中所有相關工具都要更新,不然就無法使用;如果沒有標準,這將導致容器實現的碎片化,出現大量的沖突和冗余。這兩種情況都是社區不愿意看到的事情,OCI(Open Container Initiative) 就是在這個背景下出現的,它的使命就是推動容器標準化,容器能運行在任何的硬件和系統上,相關的組件也不必綁定在任何的容器運行時上。

官網上對 OCI 的說明如下:

An open governance structure for the express purpose of creating open industry standards around container formats and runtime. – Open Containers Official Site

OCI 由 docker、coreos 以及其他容器相關公司創建于 2015 年,目前主要有兩個標準文檔: 容器運行時標準 (runtime spec)和 容器鏡像標準 (image spec)。

這兩個協議通過 OCI runtime filesytem bundle 的標準格式連接在一起,OCI 鏡像可以通過工具轉換成 bundle,然后 OCI 容器引擎能夠識別這個 bundle 來運行容器。

下面,我們來介紹這兩個 OCI 標準。因為標準本身細節很多,而且還在不斷維護和更新,如果不是容器的實現者,沒有必須對每個細節都掌握。所以我以介紹概要為主,給大家有個主觀的認知。

image spec

OCI 容器鏡像主要包括幾塊內容:

  • 文件系統 :以 layer 保存的文件系統,每個 layer 保存了和上層之間變化的部分,layer 應該保存哪些文件,怎么表示增加、修改和刪除的文件等
  • config 文件 :保存了文件系統的層級信息(每個層級的 hash 值,以及歷史信息),以及容器運行時需要的一些信息(比如環境變量、工作目錄、命令參數、mount 列表),指定了鏡像在某個特定平臺和系統的配置。比較接近我們使用 docker inspect <image_id> 看到的內容
  • manifest 文件 :鏡像的 config 文件索引,有哪些 layer,額外的 annotation 信息,manifest 文件中保存了很多和當前平臺有關的信息
  • index 文件 :可選的文件,指向不同平臺的 manifest 文件,這個文件能保證一個鏡像可以跨平臺使用,每個平臺擁有不同的 manifest 文件,使用 index 作為索引

runtime spec

OCI 對容器 runtime 的標準主要是指定容器的運行狀態,和 runtime 需要提供的命令。下圖可以是容器狀態轉換圖:

  • init 狀態:這個是我自己添加的狀態,并不在標準中,表示沒有容器存在的初始狀態
  • creating:使用 create 命令創建容器,這個過程稱為創建中
  • created:容器創建出來,但是還沒有運行,表示鏡像和配置沒有錯誤,容器能夠運行在當前平臺
  • running:容器的運行狀態,里面的進程處于 up 狀態,正在執行用戶設定的任務
  • stopped:容器運行完成,或者運行出錯,或者 stop 命令之后,容器處于暫停狀態。這個狀態,容器還有很多信息保存在平臺中,并沒有完全被刪除

runc

runc 是 docker 捐贈給 OCI 的一個符合標準的 runtime 實現,目前 docker 引擎內部也是基于 runc 構建的。這部分我們就分析 runc 這個項目,加深對 OCI 的理解。

使用 runc 運行 busybox 容器

先來準備一個工作目錄,下面所有的操作都是在這個目錄下執行的,比如 mycontainer :

# mkdir mycontainer

接下來,準備容器鏡像的文件系統,我們選擇從 docker 鏡像中提取:

# mkdir rootfs

docker export $(docker create busybox) | tar -C rootfs -xvf -

ls rootfs

bin dev etc home proc root sys tmp usr var</code></pre>

有了 rootfs 之后,我們還要按照 OCI 標準有一個配置文件 config.json 說明如何運行容器,包括要運行的命令、權限、環境變量等等內容, runc 提供了一個命令可以自動幫我們生成:

# runc spec

ls

config.json rootfs</code></pre>

這樣就構成了一個 OCI runtime bundle 的內容,這個 bundle 非常簡單,就上面兩個內容:config.json 文件和 rootfs 文件系統。 config.json 里面的內容很長,這里就不貼出來了,我們也不會對其進行修改,直接使用這個默認生成的文件。有了這些信息,runc 就能知道怎么怎么運行容器了,我們先來看看簡單的方法 runc run (這個命令需要 root 權限),這個命令類似于 docker run ,它會創建并啟動一個容器:

?  runc run simplebusybox
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # hostname
runc
/ # whoami
root
/ # pwd
/
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
/ # ps aux
PID   USER     TIME   COMMAND
    1 root       0:00 sh
   11 root       0:00 ps aux

最后一個參數是容器的名字,需要在主機上保證唯一性。運行之后直接進入到了容器的 sh 交互界面,和通過 docker run 看到的效果非常類似。但是這個容器并沒有配置網絡方面的內容,只是有一個默認的 lo 接口,因此無法和外部通信,但其他功能都正常。

此時,另開一個終端,可以查看運行的容器信息:

? runc list
ID              PID         STATUS      BUNDLE                                    CREATED                          OWNER
simplebusybox   18073       running     /home/cizixs/Workspace/runc/mycontainer   2017-11-02T06:54:52.023379345Z   root

目前,在我的機器上,runc 會把容器的運行信息保存在 /run/runc 目錄下:

? tree /run/runc/
/run/runc/ └── simplebusybox └── state.json

1 directory, 1 file</code></pre>

除了 run 命令之外,我們也能通過create、start、stop、kill 等命令對容器狀態進行更精準的控制。繼續實驗,因為接下來要在后臺模式運行容器,所以需要對 config.json 進行修改。改動有兩處,把 terminal 的值改成 false ,修改 args 命令行參數為 sleep 20 :

"process": {
        "terminal": false,
        "user": {
            "uid": 0,
            "gid": 0
        },
        "args": [
            "sleep", "20"
        ],
        ...
}

接著,用 runc 子命令來控制容器的運行,實現各個容器狀態的轉換:

// 使用 create 創建出容器,此時容器并沒有運行,只是準備好了所有的運行環境
// 通過 list 命令可以查看此時容器的狀態為 created
?  runc create mycontainerid
?  runc list
ID              PID         STATUS      BUNDLE                                    CREATED                          OWNER
mycontainerid   15871       created     /home/cizixs/Workspace/runc/mycontainer   2017-11-02T08:05:50.658423519Z   root

// 運行容器,此時容器會在后臺運行,狀態變成了 running ? runc start mycontainerid ? runc list ID PID STATUS BUNDLE CREATED OWNER mycontainerid 15871 running /home/cizixs/Workspace/runc/mycontainer 2017-11-02T08:05:50.658423519Z root

// 等待一段時間(20s)容器退出后,可以看到容器狀態變成了 stopped ? runc list ID PID STATUS BUNDLE CREATED OWNER mycontainerid 0 stopped /home/cizixs/Workspace/runc/mycontainer 2017-11-02T08:05:50.658423519Z root

// 刪除容器,容器的信息就不存在了 ? runc delete mycontainerid ? runc list ID PID STATUS BUNDLE CREATED OWNER</code></pre>

把以上命令分開來雖然讓事情變得復雜了,但是也有很多好處。可以類比 unix 系統 fork-exec 模式,在兩者動作之間,用戶可以做很多工作。比如把 create 和 start 分開,在創建出來容器之后,可以使用插件為容器配置多主機網絡,或者準備存儲設置等。

runc 代碼實現

看完了 runc 命令演示,這部分來深入分析 runc 的代碼實現。要想理解 runc 是怎么創建 linux 容器的,需要熟悉namespace 和cgroup、 go 語言 、常見的系統調用。

分析的代碼對應的 commit id 如下,這個代碼是非常接近 v1.0.0 版本的:

?  runc git:(master) git rev-parse HEAD
0232e38342a8d230c2745b67c17050b2be70c6bc

runc 的代碼結構如下(略去了部分內容):

?  runc git:(master) tree -L 1 -F --dirsfirst                  
.
├── contrib/
├── libcontainer/
├── man/
├── script/
├── tests/
├── vendor/
├── checkpoint.go
├── create.go
├── delete.go
├── Dockerfile
├── events.go
├── exec.go
├── init.go
├── kill.go
├── LICENSE
├── list.go
├── main.go
├── Makefile
├── notify_socket.go
├── pause.go
├── PRINCIPLES.md
├── ps.go
├── README.md
├── restore.go
├── rlimit_linux.go
├── run.go
├── signalmap.go
├── signalmap_mipsx.go
├── signals.go
├── spec.go
├── start.go
├── state.go
├── tty.go
├── update.go
├── utils.go
└── utils_linux.go

main.go 是入口文件,根目錄下很多 .go 文件是對應的命令(比如 run.go 對應 runc run 命令的實現),其他是一些功能性文件。

最核心的目錄是 libcontainer ,它是啟動容器進程的最終執行者, runc 可以理解為對 libcontainer 的封裝,以符合 OCI 的方式讀取配置和文件,調用 libcontainer 完成真正的工作。如果熟悉 docker 的話,可能會知道 libcontainer 本來是 docker 引擎的核心代碼,用以取代之前 lxc driver。

我們會追尋 runc run 命令的執行過程,看看代碼的調用和實現。

main.go 使用 github.com/urfave/cli 庫進行命令行解析,主要的思路是先聲明各種參數解析、命令執行函數,運行的時候 cli 會解析命令行傳過來的參數,把它們變成定義好的變量,調用指定的命令來運行。

func main() {
    app := cli.NewApp()
    app.Name = "runc"
    ...

app.Commands = []cli.Command{
    checkpointCommand,
    createCommand,
    deleteCommand,
    eventsCommand,
    execCommand,
    initCommand,
    killCommand,
    listCommand,
    pauseCommand,
    psCommand,
    restoreCommand,
    resumeCommand,
    runCommand,
    specCommand,
    startCommand,
    stateCommand,
    updateCommand,
}
...

if err := app.Run(os.Args); err != nil {
    fatal(err)
}

}</code></pre>

從上面可以看到命令函數列表,也就是 runc 支持的所有命令,命令行會實現命令的轉發,我們關心的 runCommand 定義在 run.go 文件,它的執行邏輯是:

Action: func(context *cli.Context) error {
    if err := checkArgs(context, 1, exactArgs); err != nil {
        return err
    }
    if err := revisePidFile(context); err != nil {
        return err
    }
    spec, err := setupSpec(context)

status, err := startContainer(context, spec, CT_ACT_RUN, nil)
if err == nil {
    os.Exit(status)
}
return err

},</code></pre>

可以看到整個過程分為了四步:

  1. 檢查參數個數是否符合要求
  2. 如果指定了 pid-file,把路徑轉換為絕對路徑
  3. 根據配置讀取 config.json 文件中的內容,轉換成 spec 結構對象
  4. 然后根據配置啟動容器

其中 spec 的定義在 github.com/opencontainers/runtime-spec/specs-go/config.go#Spec ,其實就是對應了 OCI bundle 中 config.json 的字段,最重要的內容在 startContainer 函數中:

utils_linux.go#startContainer

func startContainer(context cli.Context, spec specs.Spec, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) {
    id := context.Args().First()
    if id == "" {
        return -1, errEmptyID
    }
    ......

container, err := createContainer(context, id, spec)
if err != nil {
    return -1, err
}
......

r := &runner{
    enableSubreaper: !context.Bool("no-subreaper"),
    shouldDestroy:   true,
    container:       container,
    listenFDs:       listenFDs,
    notifySocket:    notifySocket,
    consoleSocket:   context.String("console-socket"),
    detach:          context.Bool("detach"),
    pidFile:         context.String("pid-file"),
    preserveFDs:     context.Int("preserve-fds"),
    action:          action,
    criuOpts:        criuOpts,
}
return r.run(spec.Process)

}</code></pre>

這個函數的內容也不多,主要分成兩部分:

  1. 調用 createContainer 創建出來容器,這個容器只是一個邏輯上的概念,保存了 namespace、cgroups、mounts、capabilities 等所有 Linux 容器需要的配置
  2. 然后創建 runner 對象,調用 r.run 運行容器。這才是運行最終容器進程的地方,它會啟動一個新進程,把進程放到配置的 namespaces 中,設置好 cgroups 參數以及其他內容

我們先來看 utils_linux.go#createContainer :

func createContainer(context cli.Context, id string, spec specs.Spec) (libcontainer.Container, error) {
    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,
        Rootless:         isRootless(),
    })
    ....

factory, err := loadFactory(context)
....
return factory.Create(id, config)

}</code></pre>

它最終會返回一個 libcontainer.Container 對象,上面提到,這并不是一個運行的容器,而是邏輯上的容器概念,包含了 linux 上運行一個容器需要的所有配置信息。

函數的內容分為兩部分:

  1. 創建 config 對象,這個配置對象的定義在 libcontainer/configs/config.go#Config ,包含了容器運行需要的所有參數。 specconv.CreateLibcontainerConfig 這一個函數就是把 spec 轉換成 libcontainer 內部的 config 對象。這個 config 對象是平臺無關的,從邏輯上定義了容器應該是什么樣的配置
  2. 通過 libcontainer 提供的 factory,創建滿足 libcontainer.Container 接口的對象

libcontainer.Container 是個接口,定義在 libcontainer/container_linux.go 文件中:

type Container interface {
    BaseContainer

// 下面這些接口是平臺相關的,也就是 linux 平臺提供的特殊功能

// 使用 criu 把容器狀態保存到磁盤
Checkpoint(criuOpts *CriuOpts) error

// 利用 criu 從磁盤中重新 load 容器
Restore(process *Process, criuOpts *CriuOpts) error

// 暫停容器的執行
Pause() error

// 繼續容器的執行
Resume() error

// 返回一個 channel,可以從里面讀取容器的 OOM 事件
NotifyOOM() (<-chan struct{}, error)

// 返回一個  channel,可以從里面讀取容器內存壓力事件
NotifyMemoryPressure(level PressureLevel) (<-chan struct{}, error)

}</code></pre>

里面包含了 Linux 平臺特有的功能,基礎容器接口為 BaseContainer ,定義在 libcontainer/container.go 文件中,它定義了容器通用的方法:

type BaseContainer interface {
    // 返回容器 ID
    ID() string

// 返回容器運行狀態
Status() (Status, error)

// 返回容器詳細狀態信息
State() (*State, error)

// 返回容器的配置
Config() configs.Config

// 返回運行在容器里所有進程的 PID
Processes() ([]int, error)

// 返回容器的統計信息,主要是網絡接口信息和 cgroup 中能收集的統計數據
Stats() (*Stats, error)

// 設置容器的配置內容,可以動態調整容器
Set(config configs.Config) error

// 在容器中啟動一個進程
Start(process *Process) (err error)

// 運行容器
Run(process *Process) (err error)

// 銷毀容器,就是刪除容器
Destroy() error

// 給容器的 init 進程發送信號
Signal(s os.Signal, all bool) error

// 告訴容器在 init 結束后執行用戶進程
Exec() error

}</code></pre>

可以看到,上面是容器應該支持的命令,包含了查詢狀態和創建、銷毀、運行等。

這里使用 factory 模式是為了支持不同平臺的容器,每個平臺實現自己的 factory ,根據運行平臺調用不同的實現就行。不過 runc 目前只支持 linux 平臺,所以我們看 libcontainer/factory_linux.go 中的實現:

func New(root string, options ...func(*LinuxFactory) error) (Factory, error) {
    if root != "" {
        if err := os.MkdirAll(root, 0700); err != nil {
            return nil, newGenericError(err, SystemError)
        }
    }
    l := &LinuxFactory{
        Root:      root,
        InitPath:  "/proc/self/exe",
        InitArgs:  []string{os.Args[0], "init"},
        Validator: validate.New(),
        CriuPath:  "criu",
    }
    Cgroupfs(l)
    for _, opt := range options {
        if opt == nil {
            continue
        }
        if err := opt(l); err != nil {
            return nil, err
        }
    }
    return l, nil
}

func (l LinuxFactory) Create(id string, config configs.Config) (Container, error) { ...... containerRoot := filepath.Join(l.Root, id) if err := os.MkdirAll(containerRoot, 0711); err != nil { return nil, newGenericError(err, SystemError) } ......

c := &linuxContainer{
    id:            id,
    root:          containerRoot,
    config:        config,
    initPath:      l.InitPath,
    initArgs:      l.InitArgs,
    criuPath:      l.CriuPath,
    newuidmapPath: l.NewuidmapPath,
    newgidmapPath: l.NewgidmapPath,
    cgroupManager: l.NewCgroupsManager(config.Cgroups, nil),
}
......
c.state = &stoppedState{c: c}
return c, nil

}</code></pre>

New 創建了一個 linux 平臺的 factory,從 LinuxFactory 的 fields 可以看到,它里面保存了和 linux 平臺相關的信息。

Create 返回的是 linuxContainer 對象,它是 libcontainer.Container 接口的實現。有了 libcontainer.Container 對象之后,回到 utils_linux.go#Runner 中看它是如何運行容器的:

func (r runner) run(config specs.Process) (int, error) {

// 根據 OCI specs.Process 生成 libcontainer.Process 對象
// 如果出錯,運行 destroy 清理產生的中間文件
process, err := newProcess(*config)
if err != nil {
    r.destroy()
    return -1, err
}

......
var (
    detach = r.detach || (r.action == CT_ACT_CREATE)
)
handler := newSignalHandler(r.enableSubreaper, r.notifySocket)

// 根據是否進入到容器終端來配置 tty,標準輸入、標準輸出和標準錯誤輸出
tty, err := setupIO(process, rootuid, rootgid, config.Terminal, detach, r.consoleSocket)
defer tty.Close()

switch r.action {
case CT_ACT_CREATE:
    err = r.container.Start(process)
case CT_ACT_RESTORE:
    err = r.container.Restore(process, r.criuOpts)
case CT_ACT_RUN:
    err = r.container.Run(process)
default:
    panic("Unknown action")
}

......
status, err := handler.forward(process, tty, detach)
if detach {
    return 0, nil
}
r.destroy()
return status, err

}</code></pre>

runner 是一層封裝,主要工作是配置容器的 IO,根據命令去調用響應的方法。 newProcess(*config) 將 OCI spec 中的 process 對象轉換成 libcontainer 中的 process,process 的定義在 libcontainer/process.go#Process ,包括進程的命令、參數、環境變量、用戶、標準輸入輸出等。

有了 process ,下一步就是運行這個進程 r.container.Run(process) , Run 會調用內部的 libcontainer/container_linux.go#start() 方法:

func (c linuxContainer) start(process Process, isInit bool) error {
    parent, err := c.newParentProcess(process, isInit)

if err := parent.start(); err != nil {
    return newSystemErrorWithCause(err, "starting container process")
}

c.created = time.Now().UTC()
if isInit {
    ...... 
    for i, hook := range c.config.Hooks.Poststart {
        if err := hook.Run(s); err != nil {
            return newSystemErrorWithCausef(err, "running poststart hook %d", i)
        }
    }
} 
return nil

}</code></pre>

運行容器進程,在容器進程完全起來之前,需要利用父進程和容器進程進行通信,因此這里封裝了一個 paerentProcess 的概念,

func (c *linuxContainer) newParentProcess(p *Process, doInit bool) (parentProcess, error) {
    parentPipe, childPipe, err := utils.NewSockPair("init")
    cmd, err := c.commandTemplate(p, childPipe)
    ......
    return c.newInitProcess(p, cmd, parentPipe, childPipe)
}

parentPipe 和 childPipe 就是父進程和創建出來的容器 init 進程通信的管道,這個管道用于在 init 容器進程啟動之后做一些配置工作,非常重要,后面會看到它們的使用。

最終創建的 parentProcess 是 libcontainer/process_linux.go#initProcess 對象,

type initProcess struct {
    cmd             *exec.Cmd
    parentPipe      *os.File
    childPipe       *os.File
    config          *initConfig
    manager         cgroups.Manager
    intelRdtManager intelrdt.Manager
    container       *linuxContainer
    fds             []string
    process         *Process
    bootstrapData   io.Reader
    sharePidns      bool
}
  • cmd 是 init 程序,也就是說啟動的容器子進程是 runc init ,后面我們會說明它的作用
  • paerentPipe 和 childPipe 是父子進程通信的管道
  • bootstrapDta 中保存了容器 init 初始化需要的數據
  • process 會保存容器 init 進程,用于父進程獲取容器進程信息和與之交互

有了 parentProcess ,接下來它的 start() 方法會被調用:

func (p *initProcess) start() error {
    defer p.parentPipe.Close()
    err := p.cmd.Start()
    p.process.ops = p
    p.childPipe.Close()

// 把容器 pid 加入到 cgroup 中
if err := p.manager.Apply(p.pid()); err != nil {
    return newSystemErrorWithCause(err, "applying cgroup configuration for process")
}

// 給容器進程發送初始化需要的數據
if _, err := io.Copy(p.parentPipe, p.bootstrapData); err != nil {
    return newSystemErrorWithCause(err, "copying bootstrap data to pipe")
}

// 等待容器進程完成 namespace 的配置
if err := p.execSetns(); err != nil {
    return newSystemErrorWithCause(err, "running exec setns process for init")
}

// 創建網絡 interface
if err := p.createNetworkInterfaces(); err != nil {
    return newSystemErrorWithCause(err, "creating network interfaces")
}

// 給容器進程發送進程配置信息
if err := p.sendConfig(); err != nil {
    return newSystemErrorWithCause(err, "sending config to init process")
}

// 和容器進程進行同步
// 容器 init 進程已經準備好環境,準備運行容器中的用戶進程
// 所以這里會運行 prestart 的鉤子函數
ierr := parseSync(p.parentPipe, func(sync *syncT) error {
    ......
    return nil
})

// Must be done after Shutdown so the child will exit and we can wait for it.
if ierr != nil {
    p.wait()
    return ierr
}
return nil

}</code></pre>

這里可以看到管道的用處:父進程把 bootstrapData 發送給子進程,子進程根據這些數據配置 namespace、cgroups,apparmor 等參數;等待子進程完成配置,進行同步。

容器子進程會做哪些事情呢?用同樣的方法,可以找到 runc init 程序運行的邏輯代碼在 libcontainer/standard_init_linux.go#Init() ,它做的事情包括:

  1. 配置 namespace
  2. 配置網絡和路由規則
  3. 準備 rootfs
  4. 配置 console
  5. 配置 hostname
  6. 配置 apparmor profile
  7. 配置 sysctl 參數
  8. 初始化 seccomp 配置
  9. 配置 user namespace

上面這些就是 linux 容器的大部分配置,完成這些之后,它就調用 Exec 執行用戶程序:

if err := syscall.Exec(name, l.config.Args[0:], os.Environ()); err != nil {
    return newSystemErrorWithCause(err, "exec user process")
}

NOTE:其實,init 在執行自身的邏輯之前,會被 libcontainer/nsenter 劫持,nsenter 是 C 語言編寫的代碼,目的是為容器配置 namespace,它會從 init pipe 中讀取 namespace 的信息,調用setns 把當前進程加入到指定的 namespace 中。

之后,它會調用 clone 創建一個新的進程,初始化完成之后,把子進程的進程號發送到管道中,nsenter 完成任務退出,子進程會返回,讓 init 接管,對容器進行初始化。

至此,容器的所有內容都 ok,而且容器里的用戶進程也啟動了。

runc 的代碼調用關系如上圖所示,可以在新頁面打開查看大圖。主要邏輯分成三塊:

  • 最上面的紅色是命令行封裝,這是根據 OCI 標準實現的接口,它能讀取 OCI 標準的容器 bundle,并實現了 OCI 指定 run、start、create 等命令
  • 中間的紫色部分就是 libcontainer,它是 runc 的核心內容,是對 linux namespace、cgroups 技術的封裝
  • 右下角的綠色部分是真正的創建容器子進程的部分

參考資料

 

來自:http://cizixs.com/2017/11/05/oci-and-runc

 

 本文由用戶 ROSJacob 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!