RunC 源碼通讀指南之 NameSpace

概述

隨著 docker 的誕生和容器技術(shù)應(yīng)用與高速發(fā)展,長(zhǎng)期一直在后臺(tái)默默奉獻(xiàn)一些 linux 特性如 namespace、cgroup 等技術(shù)走向前臺(tái)。Namespace 是 linux 內(nèi)核所提供的特性,用于隔離內(nèi)核資源的方式,可以說(shuō)沒(méi)有隔離就不會(huì)存在容器。

Linux 官方描述" namespace 是對(duì)全局系統(tǒng)資源的一種封裝隔離,使得處于不同 namespace 的進(jìn)程擁有獨(dú)立的全局系統(tǒng)資源,改變一個(gè) namespace 中的系統(tǒng)資源只會(huì)影響當(dāng)前 namespace 里的進(jìn)程,對(duì)其他 namespace 中的進(jìn)程沒(méi)有影響。"詳細(xì)介紹namespace說(shuō)明參考 。 Linux 內(nèi)核里面實(shí)現(xiàn)了7種不同類型的 namespace:

名稱        宏定義             隔離內(nèi)容
Cgroup    CLONE_NEWCGROUP   Cgroup root directory 
IPC       CLONE_NEWIPC      System V IPC, POSIX message queues 
Network   CLONE_NEWNET      Network devices, stacks, ports, etc. 
Mount     CLONE_NEWNS       Mount points
PID       CLONE_NEWPID      Process IDs 
User      CLONE_NEWUSER     User and group IDs 
UTS       CLONE_NEWUTS      Hostname and NIS domain name 

本文將聚焦在 runC 源碼關(guān)于容器初始化過(guò)程中 namespace 如何應(yīng)用與實(shí)現(xiàn)資源隔離。

從容器的 run 執(zhí)行流程來(lái)看: 容器對(duì)象創(chuàng)建階段 startContainer() => createContainer() => loadFactory() => libcontainer.New() 完成 container 對(duì)象的創(chuàng)建后, startContainer() 中已創(chuàng)建的 runner 對(duì)象 run() 方法執(zhí)行,進(jìn)入容器對(duì)象運(yùn)行階段: startContainer() => runner.run() => newProcess() => runner.container.Run(process) => linuxContainer.start() => linuxContainer.newParentProcess(process) => =>linuxContainer.commandTemplate() => linuxContaine.newInitProcess() =>parent.start() => initProcess.start() 。

Parent.start() 執(zhí)行其實(shí)則是 runC init 命令的執(zhí)行:

  1. ParentProcces 創(chuàng)建runC init子進(jìn)程,中間會(huì)被 /runc/libcontainer/nsenter 劫持( c 代碼部分 preamble ),使 runc init 子進(jìn)程位于容器配置指定的各個(gè) namespace 內(nèi)(實(shí)現(xiàn) namespace配置 )
  2. ParentProcess 用init管道將容器配置信息傳輸給runC init進(jìn)程,runC init再據(jù)此配置信息進(jìn)行容器的初始化操作。初始化完成之后,再向另一個(gè)管道exec.fifo進(jìn)行寫(xiě)操作,進(jìn)入阻塞狀態(tài)等待runC start

因此本文我們將從兩個(gè)方面展開(kāi)分析,第一則是 runC init 流程執(zhí)行關(guān)于 namespace 設(shè)置的時(shí)機(jī),第二則是 c 代碼部分 nsenter 的實(shí)現(xiàn)( namespace 關(guān)鍵應(yīng)用代碼)。

RunC init 執(zhí)行流程與 namespace

創(chuàng)建容器的 init 進(jìn)程時(shí)相關(guān) namespace 配置項(xiàng)

!FILENAME libcontainer/container_linux.go:512

func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, messageSockPair, logFilePair filePair) (*initProcess, error) {
    cmd.Env = append(cmd.Env, "_LIBCONTAINER_INITTYPE="+string(initStandard))
    nsMaps := make(map[configs.NamespaceType]string)  
    for _, ns := range c.config.Namespaces {    // 容器 namesapces 配置
        if ns.Path != "" {
            nsMaps[ns.Type] = ns.Path
        }
    }
    _, sharePidns := nsMaps[configs.NEWPID]
  // 創(chuàng)建 init 進(jìn)程同步namespace配置項(xiàng)數(shù)據(jù)(后面有詳述bootstrapData)
    data, err := c.bootstrapData(c.config.Namespaces.CloneFlags(), nsMaps)
    if err != nil {
        return nil, err
    }
    init := &initProcess{
        cmd:             cmd,
        messageSockPair: messageSockPair,
        logFilePair:     logFilePair,
        manager:         c.cgroupManager,
        intelRdtManager: c.intelRdtManager,
        config:          c.newInitConfig(p),
        container:       c,
        process:         p,
        bootstrapData:   data,              // 指定 init process bootstrapData值
        sharePidns:      sharePidns,
    }
    c.initProcess = init
    return init, nil
}

InitProcess.start() 容器的初始化配置,此處 cmd.start() 調(diào)用實(shí)則是 runC init命令執(zhí)行:

  • 先執(zhí)行 nsenter C代碼部分,實(shí)現(xiàn)對(duì)container的process進(jìn)行Namespace相關(guān)設(shè)置如uid/gid、pid、uts、ns、cgroup等。
  • 返執(zhí)行 init 命令 Go 代碼部分,LinuxFactory.StartInitialization()對(duì)網(wǎng)絡(luò)/路由、rootfs、selinux、console、主機(jī)名、apparmor、Sysctl、seccomp、capability等容器配置

!FILENAME libcontainer/process_linux.go:282

func (p *initProcess) start() error {
  //  當(dāng)前執(zhí)行空間進(jìn)程稱為bootstrap進(jìn)程
  //  啟動(dòng)了 cmd,即啟動(dòng)了 runc init 命令,創(chuàng)建 runc init 子進(jìn)程 
  //  同時(shí)也激活了C代碼nsenter模塊的執(zhí)行(為了 namespace 的設(shè)置 clone 了三個(gè)進(jìn)程parent、child、init)
  //  C 代碼執(zhí)行后返回 go 代碼部分,最后的 init 子進(jìn)程為了好區(qū)分此處命名為" nsInit "(即配置了Namespace的init)
  //  runc init go代碼為容器初始化其它部分(網(wǎng)絡(luò)、rootfs、路由、主機(jī)名、console、安全等)
  
    err := p.cmd.Start()   // +runc init 命令執(zhí)行,Namespace應(yīng)用代碼執(zhí)行空間時(shí)機(jī)
  //...
    if p.bootstrapData != nil {
     // 將 bootstrapData 寫(xiě)入到 parent pipe 中,此時(shí) runc init 可以從 child pipe 里讀取到這個(gè)數(shù)據(jù)
        if _, err := io.Copy(p.messageSockPair.parent, p.bootstrapData); err != nil {
            return newSystemErrorWithCause(err, "copying bootstrap data to pipe")
        }
    }
  //...
}

此時(shí)來(lái)到 runC init 命令執(zhí)行代碼部分,前面有說(shuō)到先執(zhí)行 nsenter C 代碼邏輯(后面詳述),再返回到 Go init 代碼部分,而Go init 代碼部分不是本文 namespace 介紹的重點(diǎn),考慮到執(zhí)行流程理解的連續(xù)性,我先簡(jiǎn)述一下此塊,有助于將整個(gè)過(guò)程串聯(lián)起來(lái)理解。

RunC init 命令執(zhí)行 Go 調(diào)用 C 代碼稱之 preamble ,即在 import nsenter 模塊時(shí)機(jī)將會(huì)在 Go 的 runtime 啟動(dòng)之前,先執(zhí)行此先導(dǎo)代碼塊,nsenter 的初始化 init(void) 方法內(nèi)對(duì) nsexec() 調(diào)用 。

!FILENAME init.go:10

    _ "github.com/opencontainers/runc/libcontainer/nsenter"

!FILENAME libcontainer/nsenter/nsenter.go:3

package nsenter
/*
#cgo CFLAGS: -Wall
extern void nsexec();
void __attribute__((constructor)) init(void) {
    nsexec();
}
*/
import "C"

注:此處 C 代碼 nsexec() 分析部分將后面將詳細(xì)解析

再執(zhí)行 go 代碼 init 命令執(zhí)行邏輯部分,創(chuàng)建 factory 對(duì)象,執(zhí)行 factory.StartInitialization() => linuxStandardInit.Init() 完成容器的相關(guān)初始化配置(網(wǎng)絡(luò)/路由、rootfs、selinux、console、主機(jī)名、apparmor、Sysctl、seccomp、capability 等)

!FILENAME init.go:15

func init() {
 //...
var initCommand = cli.Command{
    Name:  "init",
    Usage: `initialize the namespaces and launch the process (do not call it outside of runc)`,
    Action: func(context *cli.Context) error {
        factory, _ := libcontainer.New("")                          // +創(chuàng)建 factory 對(duì)象
        if err := factory.StartInitialization(); err != nil {       // +執(zhí)行 init 初始化
            os.Exit(1)
        }
        panic("libcontainer: container init failed to exec")
    },
}

libcontainer.New() 創(chuàng)建 factory 對(duì)象返回

!FILENAME libcontainer/factory_linux.go:131

func New(root string, options ...func(*LinuxFactory) error) (Factory, error) {
  //...
    l := &LinuxFactory{
  //...
    }
  //... 
    return l, nil
}

創(chuàng)建 container 容器對(duì)象

!FILENAME libcontainer/factory_linux.go:188

func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, error) {
  // 創(chuàng)建 linux 容器結(jié)構(gòu)
    c := &linuxContainer{ 
  //...
    }
    return c, nil
}

Linux 版本的 factory 實(shí)現(xiàn),查看 StartInitialization() 實(shí)現(xiàn)代碼

!FILENAME libcontainer/factory_linux.go:282

func (l *LinuxFactory) StartInitialization() (err error) {
  //...
    i, err := newContainerInit(it, pipe, consoleSocket, fifofd) 
  //...
  // newContainerInit()返回的initer實(shí)現(xiàn)對(duì)象的Init()方法調(diào)用 "linuxStandardInit.Init()"
  return i.Init()                    
}

網(wǎng)絡(luò)/路由、rootfs、selinux、console、主機(jī)名、apparmor、sysctl、seccomp、capability 等容器的相關(guān)初始化配置。管道 exec.fifo 進(jìn)行寫(xiě)操作,進(jìn)入阻塞狀態(tài)等待 runC start

!FILENAME libcontainer/standard_init_linux.go:46

func (l *linuxStandardInit) Init() error {
  //...
  // 留意此兩個(gè)關(guān)于網(wǎng)絡(luò)nework/route配置,將專文詳細(xì)介紹network
  // 配置network,
  //  配置路由
  // selinux配置
  // + 準(zhǔn)備rootfs
  // 配置console
  // 完成rootfs設(shè)置
  // 主機(jī)名設(shè)置
  // 應(yīng)用apparmor配置
  // Sysctl系統(tǒng)參數(shù)調(diào)節(jié)
  // path只讀屬性配置
  // 告訴runC進(jìn)程,我們已經(jīng)完成了初始化工作
  // 進(jìn)程標(biāo)簽設(shè)置
  // seccomp配置
  // 設(shè)置正確的capability,用戶以及工作目錄
  // 確定用戶指定的容器進(jìn)程在容器文件系統(tǒng)中的路徑
  // 關(guān)閉管道,告訴runC進(jìn)程,我們已經(jīng)完成了初始化工作
  // 在exec用戶進(jìn)程之前等待exec.fifo管道在另一端被打開(kāi)
  // 我們通過(guò)/proc/self/fd/$fd打開(kāi)它
  // ......
  // 向exec.fifo管道寫(xiě)數(shù)據(jù),阻塞,直到用戶調(diào)用`runc start`,讀取管道中的數(shù)據(jù)
  // 此時(shí)當(dāng)前進(jìn)程已處于阻塞狀態(tài),等待信號(hào)執(zhí)行后面代碼
  //
    if _, err := unix.Write(fd, []byte("0")); err != nil {
        return newSystemErrorWithCause(err, "write 0 exec fifo")
    }
  // 關(guān)閉fifofd管道 fix CVE-2016-9962
  // 初始化Seccomp配置
  // 調(diào)用系統(tǒng)exec()命令,執(zhí)行entrypoint
    if err := syscall.Exec(name, l.config.Args[0:], os.Environ()); err != nil {
        return newSystemErrorWithCause(err, "exec user process")
    }
    return nil
}

此時(shí)整個(gè) run 的容器執(zhí)行流程在執(zhí)行用戶程序 entrypoint 后已接近尾聲。從整個(gè)執(zhí)行過(guò)程來(lái)看 namespace 的配置邏輯主要在 nsenter C 代碼內(nèi),下面先簡(jiǎn)要查看 runc 內(nèi)對(duì) namespace 相關(guān)的定義與實(shí)現(xiàn)方法,后面將詳細(xì)介紹 nsenter 的邏輯代碼實(shí)現(xiàn)。

RunC Namespace 定義與實(shí)現(xiàn)

先來(lái)看一下容器內(nèi)的執(zhí)行進(jìn)程 config 配置的 namespaces 定義

!FILENAME libcontainer/configs/config.go:81

// Config defines configuration options for executing a process inside a contained environment.
type Config struct {
  //...
    Namespaces Namespaces `json:"namespaces"`     // NameSpaces 在 config 定義
  //...
}

!FILENAME libcontainer/configs/namespaces.go:5

type Namespaces []Namespace     // Namespace 類型slice

!FILENAME libcontainer/configs/namespaces_linux.go:80

type Namespace struct {
    Type NamespaceType `json:"type"`
    Path string        `json:"path"`
}

GetPath() 獲取 namespace 路徑"/proc/$pid/ns/$nsType"

!FILENAME libcontainer/configs/namespaces_linux.go:85

// 獲取指定pid的指定類型 namespace 路徑"/proc/$pid/ns/$nsType"
func (n *Namespace) GetPath(pid int) string {
    return fmt.Sprintf("/proc/%d/ns/%s", pid, NsName(n.Type))
}

// Namespace類型字串轉(zhuǎn)化為系統(tǒng)文件名
func NsName(ns NamespaceType) string {
    switch ns {
    case NEWNET:
        return "net"
    case NEWNS:
        return "mnt"
    case NEWPID:
        return "pid"
    case NEWIPC:
        return "ipc"
    case NEWUSER:
        return "user"
    case NEWUTS:
        return "uts"
    case NEWCGROUP:
        return "cgroup"
    }
    return ""
}

Namespaces 類提供的操作方法列表

!FILENAME libcontainer/configs/namespaces_linux.go:89

// 刪除,從Namespaces slice中刪除指定類型的Namespace項(xiàng)
func (n *Namespaces) Remove(t NamespaceType) bool {
//...
}
// 增加
func (n *Namespaces) Add(t NamespaceType, path string) {
//...
}
// 是否存在
func (n *Namespaces) Contains(t NamespaceType) bool {
//...
}
// 獲取指定Namespace類型的Path
func (n *Namespaces) PathOf(t NamespaceType) string {
//...
}

ParentProcess 用 init 管道將容器配置信息傳輸給 runc init 進(jìn)程,那么我們就來(lái)看一下 init 管道所傳輸?shù)?bootstrapData 數(shù)據(jù)內(nèi)容的定義,bootstrapData()最后返回序列化后的數(shù)據(jù)讀取器io reader

!FILENAME libcontainer/container_linux.go:1945

func (c *linuxContainer) bootstrapData(cloneFlags uintptr, nsMaps map[configs.NamespaceType]string) (io.Reader, error) {
  // 創(chuàng)建 netlink 消息
    r := nl.NewNetlinkRequest(int(InitMsg), 0)

    // 寫(xiě)入 cloneFlags 
    r.AddData(&Int32msg{
        Type:  CloneFlagsAttr,
        Value: uint32(cloneFlags),
    })

    // 寫(xiě)入自定義 namespace paths
    if len(nsMaps) > 0 {
        nsPaths, err := c.orderNamespacePaths(nsMaps)
        if err != nil {
            return nil, err
        }
        r.AddData(&Bytemsg{
            Type:  NsPathsAttr,
            Value: []byte(strings.Join(nsPaths, ",")),
        })
    }

  // 為新 user 寫(xiě)入 ns paths
    _, joinExistingUser := nsMaps[configs.NEWUSER]
    if !joinExistingUser {
        // write uid mappings
        if len(c.config.UidMappings) > 0 {
            if c.config.RootlessEUID && c.newuidmapPath != "" {
                r.AddData(&Bytemsg{
                    Type:  UidmapPathAttr,
                    Value: []byte(c.newuidmapPath),
                })
            }
            b, err := encodeIDMapping(c.config.UidMappings)
            if err != nil {
                return nil, err
            }
            r.AddData(&Bytemsg{
                Type:  UidmapAttr,
                Value: b,
            })
        }

        // 寫(xiě) gid mappings
        if len(c.config.GidMappings) > 0 {
            b, err := encodeIDMapping(c.config.GidMappings)
            if err != nil {
                return nil, err
            }
            r.AddData(&Bytemsg{
                Type:  GidmapAttr,
                Value: b,
            })
            if c.config.RootlessEUID && c.newgidmapPath != "" {
                r.AddData(&Bytemsg{
                    Type:  GidmapPathAttr,
                    Value: []byte(c.newgidmapPath),
                })
            }
            if requiresRootOrMappingTool(c.config) {
                r.AddData(&Boolmsg{
                    Type:  SetgroupAttr,
                    Value: true,
                })
            }
        }
    }

    if c.config.OomScoreAdj != nil {
        // 如存在配置 OomScorAdj ,寫(xiě) oom_score_adj 
        r.AddData(&Bytemsg{
            Type:  OomScoreAdjAttr,
            Value: []byte(fmt.Sprintf("%d", *c.config.OomScoreAdj)),
        })
    }

    // 寫(xiě) rootless
    r.AddData(&Boolmsg{
        Type:  RootlessEUIDAttr,
        Value: c.config.RootlessEUID,
    })

    return bytes.NewReader(r.Serialize()), nil
}

Nsenter C代碼解析

剛讀這段代碼時(shí)有些理解上混亂,多層父子進(jìn)行之間交錯(cuò)傳遞,經(jīng)過(guò)反復(fù)仔細(xì)重讀和推敲代碼后才逐漸清晰作者的 代碼邏輯思想。

在初期理解代碼邏輯時(shí)本人存在的幾個(gè)疑惑點(diǎn):

  1. 為什么需要 fork 三層級(jí)關(guān)系的進(jìn)程來(lái)實(shí)現(xiàn) namespaces 的配置?

  2. 是否每次 fork 的子進(jìn)程將繼承其父的 namespaces 配置 ?

  3. 是否有什么值傳回給bootstrap進(jìn)程?

我相信看完代碼分析后能得到答案。

Runc init 會(huì)有三個(gè)進(jìn)程:

  • 第一個(gè)進(jìn)程稱為“ parent ”,讀取 bootstrapData 并解析為 Config,對(duì) User map 設(shè)置,并通過(guò)消息協(xié)調(diào)后面兩個(gè)進(jìn)程的運(yùn)行管理,在收到 grandchild 回復(fù)任務(wù)完成消息后退出。
  • 第二個(gè)進(jìn)程稱為“ child ”,由 Parent 創(chuàng)建,完成 namespace 的設(shè)置 ,fork 出 grandChild 進(jìn)程并發(fā)送給Parent 后發(fā)送任務(wù)完成消息后退出。
  • 第三個(gè)進(jìn)程稱為“ grandChild ”或" init ",進(jìn)行最后的環(huán)境準(zhǔn)備工作(sid、uid、gid、cgroup namespace),執(zhí)行完成后return 至 init Go runtime 代碼處繼續(xù)執(zhí)行最后進(jìn)入 go 代碼。

先來(lái)看下 Init pipe 配置 datas 讀取并解析后的 config 定義

!FILENAME libcontainer/nsenter/nsexec.c:70

struct nlconfig_t {
    char *data;

    /* Process settings. */
    uint32_t cloneflags;
    char *oom_score_adj;
    size_t oom_score_adj_len;

    /* User namespace settings. */
    char *uidmap;
    size_t uidmap_len;
    char *gidmap;
    size_t gidmap_len;
    char *namespaces;
    size_t namespaces_len;
    uint8_t is_setgroup;

    /* Rootless container settings. */
    uint8_t is_rootless_euid;   /* boolean */
    char *uidmappath;
    size_t uidmappath_len;
    char *gidmappath;
    size_t gidmappath_len;
};

Nsexec() 為 nsenter 主干執(zhí)行邏輯代碼,所有 namespaces 配置都在此 func 內(nèi)執(zhí)行完成

!FILENAME libcontainer/nsenter/nsexec.c:575

void nsexec(void)
{
    int pipenum;
    jmp_buf env;
    int sync_child_pipe[2], sync_grandchild_pipe[2];  //用于后面child和grandchild進(jìn)程通信
    struct nlconfig_t config = { 0 };

  // 配置發(fā)送給父進(jìn)程的 logs 管道
    setup_logpipe();

  // 從環(huán)境變量 _LIBCONTAINER_INITPIPE 中取得 child pipe 的 fd 編號(hào)
  // linuxContainer.commandTemplate() 指定了容器相關(guān)的環(huán)境變量" _LIBCONTAINER_* "
    pipenum = initpipe();
    if (pipenum == -1)
    // 由于正常啟動(dòng)的 runc 是沒(méi)有這個(gè)環(huán)境變量的,所以這里會(huì)直接返回,然后就開(kāi)始正常的執(zhí)行 go 程序了
        return;

   // 確保當(dāng)前的二進(jìn)制文件是已經(jīng)復(fù)制過(guò)的,用來(lái)規(guī)避 CVE-2019-5736 漏洞
   // ensure_cloned_binary 中使用了兩種方法:
   // - 使用 memfd,將二進(jìn)制文件寫(xiě)入 memfd,然后重啟 runc
   // - 復(fù)制二進(jìn)制文件到臨時(shí)文件,然后重啟 runc
    if (ensure_cloned_binary() < 0)
        bail("could not ensure we are a cloned binary");

    write_log(DEBUG, "nsexec started");

  // 從 child pipe 中讀取 namespace config 并解析為 config 結(jié)構(gòu)
  // "child pipe" 為 linuxContainer.newParentProcess() 創(chuàng)建 init pipe(sockPair)
    nl_parse(pipenum, &config);

  // 設(shè)置 oom score,這個(gè)只能在特權(quán)模式下設(shè)置,所以在這里就要修改完成
    update_oom_score_adj(config.oom_score_adj, config.oom_score_adj_len);

  // 設(shè)置進(jìn)程不可 dump
    if (config.namespaces) {
        if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) < 0)
            bail("failed to set process as non-dumpable");
    }

  // 創(chuàng)建和子進(jìn)程通信的 pipe,sync_child_pipe 前面有定義
    if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_child_pipe) < 0)
        bail("failed to setup sync pipe between parent and child");

  // 創(chuàng)建和孫進(jìn)程通信的 pipe,sync_grandchild_pipe 前面有定義
    if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_grandchild_pipe) < 0)
        bail("failed to setup sync pipe between parent and grandchild");

  // setjmp 將當(dāng)前執(zhí)行位置的環(huán)境保存下來(lái),用于多進(jìn)程環(huán)境下的程序跳轉(zhuǎn)
  // 此處因后面對(duì)自身進(jìn)行 fork 進(jìn)程,通過(guò)不同進(jìn)程的 env 值進(jìn)行跳轉(zhuǎn)邏輯執(zhí)行 
    switch (setjmp(env)) {
      // +后面詳述
      //...
  }

Parent 父進(jìn)程創(chuàng)建子進(jìn)程( Child 自身也創(chuàng)建子進(jìn)程稱為 Grandchild ).接收 child 配置 uid_map 和 gid_map 請(qǐng)求消息 ,為容器與宿主完成 uid/gid range 映射后發(fā)送確認(rèn)給 child ;在接收到 child 發(fā)送的 grand pid 后,通過(guò)容器外傳進(jìn)來(lái)的 child pipe 把子和孫進(jìn)程 PID,寫(xiě)回去,然后讓容器外的 runc(bootstrap進(jìn)程)接管 PID;然后等待child 完成任務(wù)消息。其后發(fā)送 grandchild 準(zhǔn)備運(yùn)行消息后等待 grandchild 回復(fù)完成任務(wù)消息后退出進(jìn)程。

!FILENAME libcontainer/nsenter/nsexec.c:700

        /*
         * Stage 0: We're in the parent. Our job is just to create a new child
         *          (stage 1: JUMP_CHILD) process and write its uid_map and
         *          gid_map. That process will go on to create a new process, then
         *          it will send us its PID which we will send to the bootstrap
         *          process.
         */
    // 第一次執(zhí)行的時(shí)候 setjmp 返回 0,對(duì)應(yīng) JUMP_PARENT
    case JUMP_PARENT:{
            int len;
            pid_t child, first_child = -1;
            bool ready = false;

            /* For debugging. */
            prctl(PR_SET_NAME, (unsigned long)"runc:[0:PARENT]", 0, 0, 0);
 
      // clone_parent 創(chuàng)建了和當(dāng)前進(jìn)程完全一致的一個(gè)進(jìn)程(子進(jìn)程)
      // 在 clone_parent 中,通過(guò) longjmp() 跳轉(zhuǎn)到 env 保存的位置
      // 并且 setjmp 返回值為 JUMP_CHILD
      // 這樣這個(gè)子進(jìn)程就會(huì)根據(jù) switch 執(zhí)行到 JUMP_CHILD 分支
      // 而當(dāng)前 runc init 和 子 runc init 之間通過(guò)上面創(chuàng)建的
      // sync_child_pipe 進(jìn)行同步通信
            child = clone_parent(&env, JUMP_CHILD);
            if (child < 0)
                bail("unable to fork: child_func");

     // 通過(guò) sync_child_pipe 循環(huán)讀取來(lái)自子進(jìn)程的消息,“消息”定義如下:
     // enum sync_t {
       //      SYNC_USERMAP_PLS = 0x40, /* Request parent to map our users. */
       //      SYNC_USERMAP_ACK = 0x41, /* Mapping finished by the parent. */
       //      SYNC_RECVPID_PLS = 0x42, /* Tell parent we're sending the PID. */
       //      SYNC_RECVPID_ACK = 0x43, /* PID was correctly received by parent. */
       //      SYNC_GRANDCHILD = 0x44,  /* The grandchild is ready to run. */
       //      SYNC_CHILD_READY = 0x45, /* The child or grandchild is ready to return. */
     //   };
    
      // 與 child 子進(jìn)程互通消息并處理
      // 通過(guò) sync_child_pipe 循環(huán)讀取來(lái)自子進(jìn)程的消息
            while (!ready) {
                enum sync_t s;

                syncfd = sync_child_pipe[1];
                close(sync_child_pipe[0]);
        
        // 等待(讀取) Child 的消息
                if (read(syncfd, &s, sizeof(s)) != sizeof(s))
                    bail("failed to sync with child: next state");

                switch (s) {
        // 這里設(shè)置 user map,因?yàn)樽舆M(jìn)程修改自身的 user namespace 之后,就沒(méi)有權(quán)限再設(shè)置 user map 了
                case SYNC_USERMAP_PLS:   // 收到子進(jìn)程請(qǐng)求設(shè)置 usermap 消息
            
                    if (config.is_rootless_euid && !config.is_setgroup)
                        update_setgroups(child, SETGROUPS_DENY);

                    /* Set up mappings. */
                    update_uidmap(config.uidmappath, child, config.uidmap, config.uidmap_len);
                    update_gidmap(config.gidmappath, child, config.gidmap, config.gidmap_len);
            
          // 向子進(jìn)程發(fā)送 SYNC_USERMAP_ACK,表示處理完成
                    s = SYNC_USERMAP_ACK;
                    if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {
                        kill(child, SIGKILL);
                        bail("failed to sync with child: write(SYNC_USERMAP_ACK)");
                    }
                    break;
                case SYNC_RECVPID_PLS:{   // 收到子進(jìn)程傳遞的 grandchild 的 PID 接收請(qǐng)求消息
                        first_child = child;
            // 接收孫進(jìn)程的pid
                        if (read(syncfd, &child, sizeof(child)) != sizeof(child)) {
                            kill(first_child, SIGKILL);
                            bail("failed to sync with child: read(childpid)");
                        }

                        s = SYNC_RECVPID_ACK;   // 回復(fù)接收確認(rèn)消息給 child 
                        if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {
                            kill(first_child, SIGKILL);
                            kill(child, SIGKILL);
                            bail("failed to sync with child: write(SYNC_RECVPID_ACK)");
                        }

                    // 通過(guò)容器外傳進(jìn)來(lái)的 child pipe 把子和孫進(jìn)程 PID,寫(xiě)回去,然后讓容器外的 runc 接管 PID
            // 這個(gè)是因?yàn)?clone_parent 的時(shí)候參數(shù)傳了 CLONE_PARENT,導(dǎo)致子孫的父進(jìn)程都是容器外的那
            // 個(gè) runc, 所以當(dāng)前進(jìn)程無(wú)法接管這些 PID
                        len = dprintf(pipenum, "{\"pid\": %d, \"pid_first\": %d}\n", child, first_child);
                        if (len < 0) {
                            kill(child, SIGKILL);
                            bail("unable to generate JSON for child pid");
                        }
                    }
                    break;
                case SYNC_CHILD_READY:      // 收到子進(jìn)程任務(wù)完成消息
          // 子進(jìn)程已經(jīng)處理完了所有事情,父進(jìn)程可退出循環(huán)
                    ready = true;
                    break;
                default:
                    bail("unexpected sync value: %u", s);
                }
            }

      // 與 Grandchild 孫進(jìn)程互通消息并處理
      // 通過(guò) sync_grandchild_pipe 循環(huán)讀取來(lái)自孫進(jìn)程的消息
            ready = false;
            while (!ready) {
                enum sync_t s;

                syncfd = sync_grandchild_pipe[1];
                close(sync_grandchild_pipe[0]);

                s = SYNC_GRANDCHILD;     //  發(fā)送 "SYNC_GRANDCHILD" 準(zhǔn)備運(yùn)行消息
                if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {
                    kill(child, SIGKILL);
                    bail("failed to sync with child: write(SYNC_GRANDCHILD)");
                }

                if (read(syncfd, &s, sizeof(s)) != sizeof(s))
                    bail("failed to sync with child: next state");

                switch (s) {
                case SYNC_CHILD_READY:   //  接收孫進(jìn)程任務(wù)完成消息
                    ready = true;
                    break;
                default:
                    bail("unexpected sync value: %u", s);
                }
            }
       // 退出。很明顯,當(dāng)前 runc init 退出的時(shí)候,子 runc init 一定也退出了,
       // 但是孫 runc init 還沒(méi)有退出
       // 這也是為什么容器外的 runc 等待子進(jìn)程退出,卻又向 pipe 里寫(xiě)數(shù)據(jù)的原因,
       // 因?yàn)閷O runc init 還在等著容器配置
       // 進(jìn)程正常退出(不給 go 代碼執(zhí)行的機(jī)會(huì))
            exit(0);
        }

Child 子進(jìn)程加入了 init pipe 傳遞的 namespaces 配置,unshare 設(shè)置了 user namespace,并通知 parent 對(duì) usermap(uid/gid map) 進(jìn)行配置后,將當(dāng)前容器的 uid 設(shè)置為 0 (root) ;最后創(chuàng)建將 fork 的 grantchild 進(jìn)程pid發(fā)送給 parent 。

!FILENAME libcontainer/nsenter/nsexec.c:969

        /*
         * Stage 1: We're in the first child process. Our job is to join any
         *          provided namespaces in the netlink payload and unshare all
         *          of the requested namespaces. If we've been asked to
         *          CLONE_NEWUSER, we will ask our parent (stage 0) to set up
         *          our user mappings for us. Then, we create a new child
         *          (stage 2: JUMP_INIT) for PID namespace. We then send the
         *          child's PID to our parent (stage 0).
         */  
    case JUMP_CHILD:{
            pid_t child;
            enum sync_t s;

            syncfd = sync_child_pipe[0];
            close(sync_child_pipe[1]);

            /* For debugging. */
            prctl(PR_SET_NAME, (unsigned long)"runc:[1:CHILD]", 0, 0, 0);

      // 通過(guò) setns 加入現(xiàn)有的 namespaces 
            if (config.namespaces)
                join_namespaces(config.namespaces);

      // 如果 clone flag 里有 CLONE_NEWUSER,說(shuō)明需要?jiǎng)?chuàng)建新的 user namespace,
      // 使用 unshare() 創(chuàng)建 user namespace 
            if (config.cloneflags & CLONE_NEWUSER) {
                if (unshare(CLONE_NEWUSER) < 0)
                    bail("failed to unshare user namespace");
                config.cloneflags &= ~CLONE_NEWUSER;

                /* Switching is only necessary if we joined namespaces. */
                if (config.namespaces) {
                    if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) < 0)
                        bail("failed to set process as dumpable");
                }
        
        // 等待父 runc init 配置 user map 
        // 發(fā)送 SYNC_USERMAP_PLS 消息給 parent ,并接收其 SYNC_USERMAP_ACK 確認(rèn)消息
                s = SYNC_USERMAP_PLS;
                if (write(syncfd, &s, sizeof(s)) != sizeof(s))
                    bail("failed to sync with parent: write(SYNC_USERMAP_PLS)");
                if (read(syncfd, &s, sizeof(s)) != sizeof(s))
                    bail("failed to sync with parent: read(SYNC_USERMAP_ACK)");
                if (s != SYNC_USERMAP_ACK)
                    bail("failed to sync with parent: SYNC_USERMAP_ACK: got %u", s);
                /* Switching is only necessary if we joined namespaces. */
                if (config.namespaces) {
                    if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) < 0)
                        bail("failed to set process as dumpable");
                }

        // 設(shè)置當(dāng)前進(jìn)程的 uid 為 0,即容器內(nèi)的 root 用戶
                if (setresuid(0, 0, 0) < 0)
                    bail("failed to become root in user namespace");
            }
        // 使用 unshare() 其他需要新建的 namespace
            if (unshare(config.cloneflags & ~CLONE_NEWCGROUP) < 0)
                bail("failed to unshare namespaces");


     // 創(chuàng)建孫進(jìn)程,當(dāng)前進(jìn)程已經(jīng)完成了 namespace 的設(shè)置,孫進(jìn)程會(huì)繼承這些設(shè)置
            child = clone_parent(&env, JUMP_INIT);
            if (child < 0)
                bail("unable to fork: init_func");

     // 將孫進(jìn)程 PID 傳給 parent 消息" SYNC_RECVPID_PLS + Grandchild_pid "
            s = SYNC_RECVPID_PLS;
            if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {
                kill(child, SIGKILL);
                bail("failed to sync with parent: write(SYNC_RECVPID_PLS)");
            }
            if (write(syncfd, &child, sizeof(child)) != sizeof(child)) {
                kill(child, SIGKILL);
                bail("failed to sync with parent: write(childpid)");
            }
    
      // 等待父 runc init 接收PID 確認(rèn)消息" SYNC_RECVPID_ACK "
            if (read(syncfd, &s, sizeof(s)) != sizeof(s)) {
                kill(child, SIGKILL);
                bail("failed to sync with parent: read(SYNC_RECVPID_ACK)");
            }
            if (s != SYNC_RECVPID_ACK) {
                kill(child, SIGKILL);
                bail("failed to sync with parent: SYNC_RECVPID_ACK: got %u", s);
            }

      // 發(fā)送 SYNC_CHILD_READY 給 parent , Child 任務(wù)已完成 
            s = SYNC_CHILD_READY;
            if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {
                kill(child, SIGKILL);   
                bail("failed to sync with parent: write(SYNC_CHILD_READY)");
            }
            // 子 runc init 的工作到此結(jié)束,進(jìn)程正常退出(不給 go 代碼執(zhí)行的機(jī)會(huì))
            exit(0);
        }

Grandchild (final child) 孫進(jìn)程是真正啟動(dòng)容器 entrypoint 的 init 進(jìn)程,并且在啟動(dòng)之前,進(jìn)行最后的環(huán)境準(zhǔn)備工作(sid、uid、gid、cgroup namespace),執(zhí)行完成后return 至 init Go runtime 代碼處繼續(xù)執(zhí)行。

!FILENAME libcontainer/nsenter/nsexec.c:969

        /*
         * Stage 2: We're the final child process, and the only process that will
         *          actually return to the Go runtime. Our job is to just do the
         *          final cleanup steps and then return to the Go runtime to allow
         *          init_linux.go to run.
         */
    case JUMP_INIT:{
       // 
            enum sync_t s;

            syncfd = sync_grandchild_pipe[0];   
            close(sync_grandchild_pipe[1]);
            close(sync_child_pipe[0]);
            close(sync_child_pipe[1]);

            /* For debugging. */
            prctl(PR_SET_NAME, (unsigned long)"runc:[2:INIT]", 0, 0, 0);

      // 等待(讀取pipe) parent(祖父) 進(jìn)程的 SYNC_GRANDCHILD 準(zhǔn)備運(yùn)行消息
            if (read(syncfd, &s, sizeof(s)) != sizeof(s))
                bail("failed to sync with parent: read(SYNC_GRANDCHILD)");
            if (s != SYNC_GRANDCHILD)
                bail("failed to sync with parent: SYNC_GRANDCHILD: got %u", s);
     
      // 設(shè)置sid 
            if (setsid() < 0)
                bail("setsid failed");
    
      // 設(shè)置uid root
            if (setuid(0) < 0)
                bail("setuid failed");
    
      // 設(shè)置gid root
            if (setgid(0) < 0)
                bail("setgid failed");

            if (!config.is_rootless_euid && config.is_setgroup) {
                if (setgroups(0, NULL) < 0)
                    bail("setgroups failed");
            }

      // 等待來(lái)自容器外 runc 的 child pipe 的關(guān)于 cgroup namespace 的消息 0x80(CREATECGROUPNS)
            if (config.cloneflags & CLONE_NEWCGROUP) {
                uint8_t value;
        
        // 從 pipenum 讀取,請(qǐng)注意此處還從 bootstrap 進(jìn)程通迅 pipe 獲取配置
                if (read(pipenum, &value, sizeof(value)) != sizeof(value))
                    bail("read synchronisation value failed");
                if (value == CREATECGROUPNS) {
          // 使用 unshare() 創(chuàng)建 cgroup namespace
                    if (unshare(CLONE_NEWCGROUP) < 0)
                        bail("failed to unshare cgroup namespace");
                } else
                    bail("received unknown synchronisation value");
            }

      // 發(fā)送孫進(jìn)程準(zhǔn)備完成的消息給 parent, 此消息發(fā)送后 parent 進(jìn)程接收后已完成其全部任務(wù)退出
            s = SYNC_CHILD_READY;
            if (write(syncfd, &s, sizeof(s)) != sizeof(s))
                bail("failed to sync with patent: write(SYNC_CHILD_READY)");

      // 關(guān)閉資源
            /* Close sync pipes. */
            close(sync_grandchild_pipe[0]);
            /* Free netlink data. */
            nl_free(&config);

      // 父/祖父 runc init 都退出了
      // return,然后開(kāi)始執(zhí)行 go 代碼
            return;
        }
    default:
        bail("unexpected jump value");
    }

    /* Should never be reached. */
    bail("should never be reached");
}

此時(shí)代碼已 return 回到了 runC init 命令的 go 代碼繼續(xù)執(zhí)行,執(zhí)行的進(jìn)程空間仍是已完成 namespace 配置后的最后的進(jìn)程(即 grandchild 進(jìn)程在容器流程中稱為 init 進(jìn)程),后面的init go執(zhí)行流程本文前面已有簡(jiǎn)單介紹,更詳細(xì)的執(zhí)行流程分析可參照《RunC 源碼通讀指南之 Run》。

相關(guān)文檔

《RunC 源碼通讀指南之 Run》

《RunC 源碼通讀指南之 Create & Start》

《RunC 源碼通讀指南之 Cgroup》

《RunC 源碼通讀指南之 Networks》

~本文 END~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 概述 Runc 作為 OCI 運(yùn)行時(shí)標(biāo)準(zhǔn)的實(shí)現(xiàn)版本工具,其繼承早期版本 Docker 的核心庫(kù) libcontai...
    Xiao_Yang閱讀 2,813評(píng)論 0 3
  • Docker容器技術(shù)已經(jīng)發(fā)展了好些年,在很多項(xiàng)目都有應(yīng)用,線上運(yùn)行也很穩(wěn)定。整理了部分Docker的學(xué)習(xí)筆記以及新...
    __七把刀__閱讀 11,630評(píng)論 0 58
  • 寫(xiě)這個(gè)系列文章主要是對(duì)之前做項(xiàng)目用到的docker相關(guān)技術(shù)做一些總結(jié),包括docker基礎(chǔ)技術(shù)Linux命名空間,...
    __七把刀__閱讀 5,925評(píng)論 0 16
  • Kubelet 可通過(guò)配置項(xiàng) container-runtime,container-runtime-endpoi...
    陳sir的知識(shí)圖譜閱讀 5,206評(píng)論 0 2
  • 春天的午后,隨沒(méi)有正午時(shí)分那么暖和,卻也沒(méi)有了早晨的涼意。 夏天的午后,雖然太陽(yáng)已經(jīng)偏西,氣溫確比正午時(shí)分更高。 ...
    愛(ài)你_1314閱讀 522評(píng)論 4 9

友情鏈接更多精彩內(nèi)容