RunC 源碼通讀指南之 — Run

概述

RunC 是 Docker 貢獻出來魁淳,按照 OCI 運行時標準制定的一種具體實現(xiàn),是一個可執(zhí)行應用程序包工具荧降」诮荆可通過OCI 鏡像格式標準文件包 bundles 來創(chuàng)建和運行容器以及對容器的生命周期管理詳情參考。 本文《附錄一》附有使用 runc 工具命令來運行容器的示例可供參考绸贡。

從 docker 容器的整個架構和執(zhí)行流程視角來看是由 containerd-shim 組件調(diào)用了 runc 來創(chuàng)建和運行容器盯蝴,其創(chuàng)建容器時的配置文件/run/docker/libcontainerd/?$containerID/config.json,進行讀取轉(zhuǎn)化為 spec 標準作為 runc 創(chuàng)建容器全局配置參數(shù)。

本文將重點聚焦在 runc run 命令的執(zhí)行代碼的整個流程上(容器的創(chuàng)建至容器的運行)的解析听怕,而有些細節(jié)實現(xiàn)比如 namespace 捧挺、cgroup 、網(wǎng)絡等將在此套系列文檔有詳細介紹可供參閱尿瞭。

從 runc run 代碼執(zhí)行結構可簡單分為為四塊執(zhí)行組成部分:1. Run 命令執(zhí)行入口 2. 容器對象創(chuàng)建 3. 容器執(zhí)行 init 初始化 4. 容器用戶程序與運行 ,本文下面將順序地進行展開解析闽烙。

CLI run 執(zhí)行入口

Cli app 的 run 命令執(zhí)行,讀取命令參數(shù)和讀取與轉(zhuǎn)化 config.json 為 spec 標準配置声搁。

!FILENAME run.go:65

Action: func(context *cli.Context) error {
    // 命令參數(shù)校驗
        if err := checkArgs(context, 1, exactArgs); err != nil {
            return err
        }
    // 獲取"pid-file"傳參配置黑竞,轉(zhuǎn)化為絕對路徑
        if err := revisePidFile(context); err != nil {
            return err
        }
    // 讀取 config.json
        spec, err := setupSpec(context)
        if err != nil {
            return err
        }
   // +startContainer() 啟動容器
        status, err := startContainer(context, spec, CT_ACT_RUN, nil)
        if err == nil {
            // exit with the container's exit status so any external supervisor is
            // notified of the exit with the correct exit status.
            os.Exit(status)
        }
        return err
    },

StartContainer() 啟動容器頂層代碼執(zhí)行過程:

  • 讀取傳入的 container id 參數(shù)
  • 通過 spec 配置與 id 等傳參創(chuàng)建容器對象
  • 構建 runner 啟動器并執(zhí)行

!FILENAME utils_linux.go:430

func startContainer(context *cli.Context, spec *specs.Spec, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) {
  // 通過 spec 創(chuàng)建容器結構捕发,在 createContainer 中將 spec 轉(zhuǎn)換為了 runc 的 container config*
    id := context.Args().First() //命令行輸入的container id參數(shù)
    if id == "" {
        return -1, errEmptyID
    }

    notifySocket := newNotifySocket(context, os.Getenv("NOTIFY_SOCKET"), id)
    if notifySocket != nil {
        notifySocket.setupSpec(context, spec)
    }

  // +創(chuàng)建容器對象
    container, err := createContainer(context, id, spec)   
    if err != nil {
        return -1, err
    }

    if notifySocket != nil {
        err := notifySocket.setupSocket()
        if err != nil {
            return -1, err
        }
    }

    // Support on-demand socket activation by passing file descriptors into the container init process.
    listenFDs := []*os.File{}
    if os.Getenv("LISTEN_FDS") != "" {
        listenFDs = activation.Files(false)
    }

    logLevel := "info"
    if context.GlobalBool("debug") {
        logLevel = "debug"
    }

  // 構建 runner 啟動器
    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,                            // CT_ACT_RUN 執(zhí)行標志
        criuOpts:        criuOpts,                          // criu 熱遷移選項
        init:            true,                              // 用于設置 process.Init 字段
    logLevel:        logLevel,                          // 日志級別 default info
    } 
  return r.run(spec.Process)  // run() 啟動
}

Container 容器對象創(chuàng)建

RunC 代碼實例化容器對象的代碼模式是通過工廠方法實現(xiàn),實例化 LinuxFactory 類型工廠和 linuxContainer 類型容器對象很魂。

!FILENAME utils_linux.go:230

func createContainer(context *cli.Context, id string, spec *specs.Spec) (libcontainer.Container, error) {
    rootlessCg, err := shouldUseRootlessCgroupManager(context)
    if err != nil {
        return nil, err
    }
  // spec 轉(zhuǎn)換 config
    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,
    })
    if err != nil {
        return nil, err
    }

    factory, err := loadFactory(context)   //+  創(chuàng)建工廠實例
    if err != nil {
        return nil, err
    }
    return factory.Create(id, config)      //+  工廠實例化容器對象 
}

loadFactory() 創(chuàng)建容器工廠 libcontainer.Factory 扎酷,配置 cgroup 管理器 、root path 遏匆、intel RDT 管理器 霞玄、user map、熱遷移路徑拉岁。

!FILENAME utils_linux.go:31

// loadFactory returns the configured factory instance for execing containers.
func loadFactory(context *cli.Context) (libcontainer.Factory, error) {
    root := context.GlobalString("root")   
    abs, err := filepath.Abs(root)             // 根目錄絕對路徑
    if err != nil {
        return nil, err
    }

    cgroupManager := libcontainer.Cgroupfs     // cgroup Manger 默認為 Cgroupfs 
    rootlessCg, err := shouldUseRootlessCgroupManager(context)
    if err != nil {
        return nil, err
    }
    if rootlessCg {
        cgroupManager = libcontainer.RootlessCgroupfs
    }
    if context.GlobalBool("systemd-cgroup") {  // systemd-cgroup 是否全局指定開啟
        if systemd.UseSystemd() {
            cgroupManager = libcontainer.SystemdCgroups
        } else {
            return nil, fmt.Errorf("systemd cgroup flag passed, but systemd support for managing cgroups is not available")
        }
    }

    intelRdtManager := libcontainer.IntelRdtFs   // intel RDT 
    if !intelrdt.IsCatEnabled() && !intelrdt.IsMbaEnabled() {
        intelRdtManager = nil
    }

    newuidmap, err := exec.LookPath("newuidmap")  // newuidmap 容器內(nèi)外 uid 映射
    if err != nil {
        newuidmap = ""
    }
    newgidmap, err := exec.LookPath("newgidmap")  // newgidmap 容器內(nèi)外 uid 映射
    if err != nil {
        newgidmap = ""utils_linux.go
    }

  // 創(chuàng)建容器工廠
    return libcontainer.New(abs, cgroupManager, intelRdtManager,
        libcontainer.CriuPath(context.GlobalString("criu")),
        libcontainer.NewuidmapPath(newuidmap),
        libcontainer.NewgidmapPath(newgidmap))
}

創(chuàng)建LinuxFactory類型的factoy對象,用于容器對象的創(chuàng)建工廠

!FILENAME libcontainer/factory_linux.go:131

func New(root string, options ...func(*LinuxFactory) error) (Factory, error) {
    if root != "" {
    //確保存儲容器狀態(tài)的根目錄創(chuàng)建
        if err := os.MkdirAll(root, 0700); err != nil {     
            return nil, newGenericError(err, SystemError)
        }
    }
    l := &LinuxFactory{
    // 存儲容器狀態(tài)的根目錄惰爬,默認"/run/runc/"
        Root:      root,
    // 指向當前的 exe 程序喊暖,即 runc 本身
        InitPath:  "/proc/self/exe",   
    // os.Args[0] 是當前 runc 的路徑,本質(zhì)上和 InitPath 是一樣的撕瞧,即 runc init
        InitArgs:  []string{os.Args[0], "init"},
    // 配置校驗器對象
        Validator: validate.New(),
    // 熱遷移路徑設置
        CriuPath:  "criu",
    }
    Cgroupfs(l)  //為 LinuxFactory 配置 NewCgroupsManage實現(xiàn) func
    for _, opt := range options {
        if opt == nil {
            continue
        }
        if err := opt(l); err != nil {
            return nil, err
        }
    }
    return l, nil
}

基于全局配置陵叽,容器工廠創(chuàng)建 linuxContainer 容器對象

!FILENAME libcontainer/factory_linux.go:188

func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, error) {
  // 確保containerRoot目錄被創(chuàng)建
    if l.Root == "" {
        return nil, newGenericError(fmt.Errorf("invalid root"), ConfigInvalid)
    }
  // 校驗參數(shù)
    if err := l.validateID(id); err != nil {
        return nil, err
    }
    if err := l.Validator.Validate(config); err != nil {
        return nil, newGenericError(err, ConfigInvalid)
    }
  // 容器根路徑
    containerRoot, err := securejoin.SecureJoin(l.Root, id)
    if err != nil {
        return nil, err
    }
    if _, err := os.Stat(containerRoot); err == nil {
        return nil, newGenericError(fmt.Errorf("container with id exists: %v", id), IdInUse)
    } else if !os.IsNotExist(err) {
        return nil, newGenericError(err, SystemError)
    }
    if err := os.MkdirAll(containerRoot, 0711); err != nil {
        return nil, newGenericError(err, SystemError)
    }
    if err := os.Chown(containerRoot, unix.Geteuid(), unix.Getegid()); err != nil {
        return nil, newGenericError(err, SystemError)
    }
  
  // 創(chuàng)建 linux 容器結構
    c := &linuxContainer{
        id:            id,               // 容器 ID
        root:          containerRoot,    // 容器狀態(tài)文件存放目錄,默認是 /run/runc/$容器ID/
        config:        config,           // 容器配置
        initPath:      l.InitPath,       // /proc/self/exe丛版,就是runc
        initArgs:      l.InitArgs,       // 即runc init
        criuPath:      l.CriuPath,       // 熱遷移path "criu"
    // Uid / Gid 配置
        newuidmapPath: l.NewuidmapPath,
        newgidmapPath: l.NewgidmapPath,
    // cgroup配置
        cgroupManager: l.NewCgroupsManager(config.Cgroups, nil),
    }
  // 英特爾RDT(資源調(diào)配技術)配置
    if intelrdt.IsCatEnabled() || intelrdt.IsMbaEnabled() {
        c.intelRdtManager = l.NewIntelRdtManager(config, id, "")
    }
    c.state = &stoppedState{c: c}       // 開始置為"stopped"狀態(tài)
    return c, nil
}

Runner 執(zhí)行和 init 容器初始化

根據(jù) startContainer() 頂層的執(zhí)行流程巩掺,在創(chuàng)建容器化對象后,構建 runner 對象并執(zhí)行run()页畦。此 run() 則是容器的運行階段的入口胖替,它包含兩大塊執(zhí)行過程:Start() 容器環(huán)境初始化啟動 和 exec () 容器用戶程序執(zhí)行,start() 初始化啟動過程比較復雜一些容器的初始工作都在此過程中豫缨,包含cgroup 独令、namespace等等核心初始化工作(專文說明),而 start() 過程則比較簡單僅取消bootstrap進程的阻塞態(tài)好芭,讓其完成執(zhí)行entrypoint燃箭。后面將進行詳細的描述。

Runner.run() 創(chuàng)建初始化進程對象舍败,調(diào)用 linuxContainer.run() 運行進程

utils_linux.go:271

func (r *runner) run(config *specs.Process) (int, error) {
    var err error
    defer func() {
        if err != nil {
            r.destroy()
        }
    }()
    if err = r.checkTerminal(config); err != nil {
        return -1, err
    }
  // +基于config 創(chuàng)建 init process對象 (指定 "run init")
    process, err := newProcess(*config, r.init, r.logLevel)
    if err != nil {
        return -1, err
    }
    if len(r.listenFDs) > 0 {
        process.Env = append(process.Env, fmt.Sprintf("LISTEN_FDS=%d", len(r.listenFDs)), "LISTEN_PID=1")
        process.ExtraFiles = append(process.ExtraFiles, r.listenFDs...)
    }
    baseFd := 3 + len(process.ExtraFiles)
    for i := baseFd; i < baseFd+r.preserveFDs; i++ {
        _, err = os.Stat(fmt.Sprintf("/proc/self/fd/%d", i))
        if err != nil {
            return -1, errors.Wrapf(err, "please check that preserved-fd %d (of %d) is present", i-baseFd, r.preserveFDs)
        }
        process.ExtraFiles = append(process.ExtraFiles, os.NewFile(uintptr(i), "PreserveFD:"+strconv.Itoa(i)))
    }
    rootuid, err := r.container.Config().HostRootUID()    
    if err != nil {
        return -1, err
    }
    rootgid, err := r.container.Config().HostRootGID()
    if err != nil {
        return -1, err
    }
    var (
        detach = r.detach || (r.action == CT_ACT_CREATE)
    )
    
  // 處理io和tty相關配置
    handler := newSignalHandler(r.enableSubreaper, r.notifySocket)
    tty, err := setupIO(process, rootuid, rootgid, config.Terminal, detach, r.consoleSocket)
    if err != nil {
        return -1, err
    }
    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)                   // +調(diào)用linuxContainer.run()
    default:
        panic("Unknown action")
    }
    if err != nil {
        return -1, err
    }
    if err = tty.waitConsole(); err != nil {
        r.terminate(process)
        return -1, err
    }
    if err = tty.ClosePostStart(); err != nil {
        r.terminate(process)
        return -1, err
    }
    if r.pidFile != "" {
        if err = createPidFile(r.pidFile, process); err != nil {
            r.terminate(process)
            return -1, err
        }
    }
    status, err := handler.forward(process, tty, detach)
    if err != nil {
        r.terminate(process)
    }
    if detach {
        return 0, nil
    }
    r.destroy()
    return status, err
}

newProcess() 基于 spec 配置初始化并創(chuàng)建libconatiner.process 進程對象返回

!FILENAME utils_linux.go:106

func newProcess(p specs.Process, init bool, logLevel string) (*libcontainer.Process, error) {
    lp := &libcontainer.Process{
        Args: p.Args,
        Env:  p.Env,
    User:            fmt.Sprintf("%d:%d", p.User.UID, p.User.GID),   // uid:gid
        Cwd:             p.Cwd,
        Label:           p.SelinuxLabel,            // selinux 標簽
        NoNewPrivileges: &p.NoNewPrivileges, 
        AppArmorProfile: p.ApparmorProfile,         // Apparmor 配置 
        Init:            init,                      // runc init
        LogLevel:        logLevel,
    }

    if p.ConsoleSize != nil {                     // console 窗口設置
        lp.ConsoleWidth = uint16(p.ConsoleSize.Width)
        lp.ConsoleHeight = uint16(p.ConsoleSize.Height)
    }
 
    if p.Capabilities != nil {                    // capabilities 配置
        lp.Capabilities = &configs.Capabilities{}
        lp.Capabilities.Bounding = p.Capabilities.Bounding
        lp.Capabilities.Effective = p.Capabilities.Effective
        lp.Capabilities.Inheritable = p.Capabilities.Inheritable
        lp.Capabilities.Permitted = p.Capabilities.Permitted
        lp.Capabilities.Ambient = p.Capabilities.Ambient
    }
    for _, gid := range p.User.AdditionalGids {    // gid 配置
        lp.AdditionalGroups = append(lp.AdditionalGroups, strconv.FormatUint(uint64(gid), 10))
    }
    for _, rlimit := range p.Rlimits {             // limit 資源限制配置
        rl, err := createLibContainerRlimit(rlimit)
        if err != nil {
            return nil, err
        }
        lp.Rlimits = append(lp.Rlimits, rl)
    }
    return lp, nil
}

LinuxContainer.Run() 為上層 CT_ACT_RUN 執(zhí)行流程調(diào)用:

  • Start() Init 進程執(zhí)行啟動
  • exec() 用戶進程EntryPoint 執(zhí)行

libcontainer/container_linux.go:250

func (c *linuxContainer) Run(process *Process) error {
    if err := c.Start(process); err != nil {   // +容器環(huán)境 init 啟動
        return err
    }
    if process.Init {
        return c.exec()                          // +EntryPoint 執(zhí)行
    }
    return nil
}

調(diào)用 linuxContainer.start()

libcontainer/container_linux.go:233

func (c *linuxContainer) Start(process *Process) error {
  //...
  if err := c.start(process); err != nil {        // +linuxContainer.start() 運行 process
        if process.Init {
            c.deleteExecFifo()
        }
        return err
    }
    return nil
}

linuxContainer.start() 為一個完整的上層容器實始化執(zhí)行流程代碼招狸,首先通過上面?zhèn)鲄⒌?process 進程對象創(chuàng)建 “父” 進程并啟動(核心邏輯處),完成啟動后保存容器狀態(tài)到 state.json 文件(默認"/run/runc/$containerID/ state.json")邻薯,最后如果容器有定義運行后勾子將被調(diào)用執(zhí)行裙戏。

libcontainer/container_linux.go:335

func (c *linuxContainer) start(process *Process) error {
  // +創(chuàng)建的父進程
    parent, err := c.newParentProcess(process)
    if err != nil {
        return newSystemErrorWithCause(err, "creating new parent process")
    }
    parent.forwardChildLogs()
  // +啟動父進程  
    if err := parent.start(); err != nil {
        // terminate the process to ensure that it properly is reaped.
        if err := ignoreTerminateErrors(parent.terminate()); err != nil {
            logrus.Warn(err)
        }
        return newSystemErrorWithCause(err, "starting container process")
    }
    // 容器啟動狀態(tài) state 保存(寫入 state.json 文件)
    c.created = time.Now().UTC()
    if process.Init {
        c.state = &createdState{
            c: c,
        }
        state, err := c.updateState(parent)
        if err != nil {
            return err
        }
        c.initProcessStartTime = state.InitProcessStartTime

        if c.config.Hooks != nil {
            s, err := c.currentOCIState()
            if err != nil {
                return err
            }
      // postStrat 容器運行后勾子執(zhí)行
            for i, hook := range c.config.Hooks.Poststart {
                if err := hook.Run(s); err != nil {
                    if err := ignoreTerminateErrors(parent.terminate()); err != nil {
                        logrus.Warn(err)
                    }
                    return newSystemErrorWithCausef(err, "running poststart hook %d", i)
                }
            }
        }
    }
    return nil
}

newParentProcess() 創(chuàng)建父進程的過程:

  • 創(chuàng)建父子進程通信的 pipe ( bootstrapData 配置數(shù)據(jù)用此傳遞)
  • 創(chuàng)建 cmd 對象 ( 此處的cmd 對象就是執(zhí)行 runc init ,后面有詳述 )
  • 返回 newInitProcess() initProcess 對象

libcontainer/container_linux.go:441

func (c *linuxContainer) newParentProcess(p *Process) (parentProcess, error) {
  // 創(chuàng)建用于父子進程通信的 pipe
    parentInitPipe, childInitPipe, err := utils.NewSockPair("init")
    if err != nil {
        return nil, newSystemErrorWithCause(err, "creating new init pipe")
    }
    messageSockPair := filePair{parentInitPipe, childInitPipe}

  //...
  // +創(chuàng)建父進程的 cmd
    cmd, err := c.commandTemplate(p, childInitPipe, childLogPipe)  
    if err != nil {
        return nil, newSystemErrorWithCause(err, "creating new command template")
    }

  //...
  // +返回標準 init 進程
    return c.newInitProcess(p, cmd, messageSockPair, logFilePair)   
}

創(chuàng)建父進程的 cmd 對象

libcontainer/container_linux.go:473

func (c *linuxContainer) commandTemplate(p *Process, childInitPipe *os.File, childLogPipe *os.File) (*exec.Cmd, error) {
  // 這里可以看到 cmd 就是 runc init
    cmd := exec.Command(c.initPath, c.initArgs[1:]...)
    cmd.Args[0] = c.initArgs[0]
  
  // 將設置給容器 entrypoint 的 std 流給了 runc init 命令,這些流最終會通過 runc init 傳遞給 entrypoint
    cmd.Stdin = p.Stdin
    cmd.Stdout = p.Stdout
    cmd.Stderr = p.Stderr
    cmd.Dir = c.config.Rootfs
    if cmd.SysProcAttr == nil {
        cmd.SysProcAttr = &syscall.SysProcAttr{}
    }
    cmd.Env = append(cmd.Env, fmt.Sprintf("GOMAXPROCS=%s", os.Getenv("GOMAXPROCS")))
    cmd.ExtraFiles = append(cmd.ExtraFiles, p.ExtraFiles...)
    if p.ConsoleSocket != nil {
        cmd.ExtraFiles = append(cmd.ExtraFiles, p.ConsoleSocket)
        cmd.Env = append(cmd.Env,
            fmt.Sprintf("_LIBCONTAINER_CONSOLE=%d", stdioFdCount+len(cmd.ExtraFiles)-1),
        )
    }
  // 這個 childInitPipe 用于跟父進程通信(父進程就是當前這個 runc 進程)
    cmd.ExtraFiles = append(cmd.ExtraFiles, childInitPipe)
   // 通過環(huán)境變量 _LIBCONTAINER_INITPIPE 把 fd 號傳遞給 runc init弛说,由于 std 流會占用前三個 fd 編號(0挽懦,1,2)
    // 所以 fd 要加上 3(stdioFdCount)
    cmd.Env = append(cmd.Env,
        fmt.Sprintf("_LIBCONTAINER_INITPIPE=%d", stdioFdCount+len(cmd.ExtraFiles)-1),
        fmt.Sprintf("_LIBCONTAINER_STATEDIR=%s", c.root),
    )

  //...
    return cmd, nil
}

返回 initProcess 對象

libcontainer/container_linux.go:512

func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, messageSockPair, logFilePair filePair) (*initProcess, error) {
  // 這里通過環(huán)境變量 _LIBCONTAINER_INITTYPE 設置 init 類型為 standard(initStandard
    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]
   // 構造 namespace 配置木人,然后序列化成字節(jié)數(shù)據(jù)
    data, err := c.bootstrapData(c.config.Namespaces.CloneFlags(), nsMaps)
    if err != nil {
        return nil, err
    }
    init := &initProcess{   
        cmd:             cmd,                       //  cmd 對象,也就是 run int
        messageSockPair: messageSockPair,           //  通信 sockpair
        logFilePair:     logFilePair,
        manager:         c.cgroupManager,     
        intelRdtManager: c.intelRdtManager,
        config:          c.newInitConfig(p),
        container:       c,                         // 容器對象
        process:         p,                         // 傳參的進程對象
        bootstrapData:   data,                      // namespaces 配置序列化數(shù)據(jù)
        sharePidns:      sharePidns,
    }
    c.initProcess = init        
    return init, nil                              //返回 init 進程對象
}

InitProcess.start() 則是容器運行最核心的代碼執(zhí)行邏輯塊:

當前執(zhí)行進程我們稱之為“bootstrap進程“信柿,cmd.Start() 實則執(zhí)行了 "runc init" 命令冀偶,同時也激活了nsenter 模塊C 代碼的優(yōu)先執(zhí)行配置namespace (詳細可參閱《RunC 源碼通讀指南之 NameSpace》),完后返回執(zhí)行 init Go 代碼部分完成后續(xù)的初始化工作渔嚷,最后向管道 exec.fifo 進行寫操作进鸠,init 進程進入阻塞狀態(tài)等待信號完成容器內(nèi)的entrypoint執(zhí)行。

!FILENAME libcontainer/process_linux.go:282

func (p *initProcess) start() error {
    defer p.messageSockPair.parent.Close()
  //  當前執(zhí)行空間進程稱為bootstrap進程
  //  啟動了 cmd形病,即啟動了 runc init 命令,創(chuàng)建 runc init 子進程 
  //  同時也激活了C代碼nsenter模塊的執(zhí)行(為了 namespace 的設置 clone 了三個進程parent客年、child、init)
  //  C 代碼執(zhí)行后返回 go 代碼部分,最后的 init 子進程為了好區(qū)分此處命名為" nsInit "(即配置了Namespace的init)
  //  runc init go代碼為容器初始化其它部分(網(wǎng)絡漠吻、rootfs量瓜、路由、主機名途乃、console绍傲、安全等)
    err := p.cmd.Start()  
   
    //...
    // 為進程 runc init 應用 Cgroup (p.cmd.Process.Pid())
    if err := p.manager.Apply(p.pid()); err != nil {
        return newSystemErrorWithCause(err, "applying cgroup configuration for process")
    }
    
    //...
    // messageSockPair 管道寫入 bootstrapData 
    if _, err := io.Copy(p.messageSockPair.parent, p.bootstrapData); err != nil {
        return newSystemErrorWithCause(err, "copying bootstrap data to pipe")
    }
  
    // 獲取 nsInit pid
    childPid, err := p.getChildPid()
    if err != nil {
        return newSystemErrorWithCause(err, "getting the final child's pid from pipe")
    }

    //... 
    // 為 nsInit 進程應用 Cgroup 
    if err := p.manager.Apply(childPid); err != nil {
        return newSystemErrorWithCause(err, "applying cgroup configuration for process")
    }
    // 為 child 進程應用 intel RDT 
    if p.intelRdtManager != nil {
        if err := p.intelRdtManager.Apply(childPid); err != nil {
            return newSystemErrorWithCause(err, "applying Intel RDT configuration for process")
        }
    }
 
  // 設置 cgroup namesapce
    if p.config.Config.Namespaces.Contains(configs.NEWCGROUP) && p.config.Config.Namespaces.PathOf(configs.NEWCGROUP) == "" {
        if _, err := p.messageSockPair.parent.Write([]byte{createCgroupns}); err != nil {
            return newSystemErrorWithCause(err, "sending synchronization value to init process")
        }
    }

    // 等待子進程退出
    if err := p.waitForChildExit(childPid); err != nil {
        return newSystemErrorWithCause(err, "waiting for our first child to exit")
    }

  //...
  // 創(chuàng)建網(wǎng)絡接口
    if err := p.createNetworkInterfaces(); err != nil {
        return newSystemErrorWithCause(err, "creating network interfaces")
    }
  // 發(fā)送 initConfig 進程配置到 messageSockPair.parent 管道
    if err := p.sendConfig(); err != nil {
        return newSystemErrorWithCause(err, "sending config to init process")
    }
    var (
        sentRun    bool
        sentResume bool
    )

  // 解析runc init子進程的所有同步消息,當io.EOF返回
    ierr := parseSync(p.messageSockPair.parent, func(sync *syncT) error {
        switch sync.Type {
        case procReady:  // 
      // 配置 limit 資源限制
            if err := setupRlimits(p.config.Rlimits, p.pid()); err != nil {
                return newSystemErrorWithCause(err, "setting rlimits for ready process")
            }
            // // prestart hook 啟動前執(zhí)行勾子
            if !p.config.Config.Namespaces.Contains(configs.NEWNS) {
                // Setup cgroup before prestart hook, so that the prestart hook could apply cgroup permissions.??
                if err := p.manager.Set(p.config.Config); err != nil {
                    return newSystemErrorWithCause(err, "setting cgroup config for ready process")
                }
                if p.intelRdtManager != nil {
                    if err := p.intelRdtManager.Set(p.config.Config); err != nil {
                        return newSystemErrorWithCause(err, "setting Intel RDT config for ready process")
                    }
                }

                if p.config.Config.Hooks != nil {
                    s, err := p.container.currentOCIState()
                    if err != nil {
                        return err
                    }
                    // initProcessStartTime hasn't been set yet.
                    s.Pid = p.cmd.Process.Pid
                    s.Status = "creating"
                    for i, hook := range p.config.Config.Hooks.Prestart {
                        if err := hook.Run(s); err != nil {
                            return newSystemErrorWithCausef(err, "running prestart hook %d", i)
                        }
                    }
                }
            }
            // 與子進程 runC init 同步
            if err := writeSync(p.messageSockPair.parent, procRun); err != nil {
                return newSystemErrorWithCause(err, "writing syncT 'run'")
            }
            sentRun = true
        case procHooks:         // prochook 勾子執(zhí)行
      //  配置 cgroup
            if err := p.manager.Set(p.config.Config); err != nil {
                return newSystemErrorWithCause(err, "setting cgroup config for procHooks process")
            }
      // 配置 intel RDT 資源管理
            if p.intelRdtManager != nil {
                if err := p.intelRdtManager.Set(p.config.Config); err != nil {
                    return newSystemErrorWithCause(err, "setting Intel RDT config for procHooks process")
                }
            }
            if p.config.Config.Hooks != nil {
            //...
        // 執(zhí)行勾子定義任務
                for i, hook := range p.config.Config.Hooks.Prestart {
                    if err := hook.Run(s); err != nil {
                        return newSystemErrorWithCausef(err, "running prestart hook %d", i)
                    }
                }
            }
            // 與子進程 runc-init 同步
            if err := writeSync(p.messageSockPair.parent, procResume); err != nil {
                return newSystemErrorWithCause(err, "writing syncT 'resume'")
            }
            sentResume = true
        default:
            return newSystemError(fmt.Errorf("invalid JSON payload from child"))
        }
   
        return nil
    })
 
  //...
    return nil
}

RunC Init 容器初始化

Nsenter 模塊C 代碼執(zhí)行邏輯

RunC init 命令執(zhí)行 Go 調(diào)用 C 代碼稱之 preamble ,即在 import nsenter 模塊時機將會在 Go 的 runtime 啟動之前耍共,先執(zhí)行此先導代碼塊烫饼,nsenter 的初始化 init(void) 方法內(nèi)對 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"

nsexec clone 三個進程:

  • 第一個進程稱為“ parent ”,讀取 bootstrapData 并解析為 Config试读,對 User map 設置杠纵,并通過消息協(xié)調(diào)后面兩個進程的運行管理,在收到 grandchild 回復任務完成消息后退出钩骇。
  • 第二個進程稱為“ child ”,由 Parent 創(chuàng)建比藻,完成 namespace 的設置 ,fork 出 grandChild 進程并發(fā)送給Parent 后發(fā)送任務完成消息后退出倘屹。
  • 第三個進程稱為“ grandChild ”或" init "韩容,進行最后的環(huán)境準備工作(sid、uid唐瀑、gid群凶、cgroup namespace),執(zhí)行完成后return 至 init Go runtime 代碼處繼續(xù)執(zhí)行最后進入 go 代碼哄辣。

!FILENAME libcontainer/nsenter/nsexec.c:575

void nsexec(void)
{
  //...
    switch (setjmp(env)) {
      //...
        case JUMP_PARENT:{
          //..
        }
        case JUMP_CHILD:{
          //...
        }
         case JUMP_INIT:{
           //...
         }
      //...
  }

注:此塊詳細代碼解析專文請參閱《RunC 源碼通讀指南之 NameSpace》

RunC init (Go 代碼部分)執(zhí)行邏輯

創(chuàng)建 factory 對象请梢,執(zhí)行 factory.StartInitialization() => linuxStandardInit.Init() 完成容器的相關初始化配置(網(wǎng)絡/路由、rootfs力穗、selinux毅弧、console、主機名当窗、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 對象
        if err := factory.StartInitialization(); err != nil {       // +執(zhí)行 init 初始化
            os.Exit(1)
        }
        panic("libcontainer: container init failed to exec")
    },
}

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

!FILENAME libcontainer/factory_linux.go:131

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

factory.StartInitialization() 初始化

!FILENAME libcontainer/factory_linux.go:282

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

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

!FILENAME libcontainer/factory_linux.go:188

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

linuxContainer.Init() 對網(wǎng)絡/路由元咙、rootfs梯影、selinux、console庶香、主機名甲棍、apparmor、sysctl赶掖、seccomp感猛、capability 等容器的相關初始化配置。管道 exec.fifo 進行寫操作奢赂,進入阻塞狀態(tài)等待 runC start

!FILENAME libcontainer/standard_init_linux.go:46

func (l *linuxStandardInit) Init() error {
  //...
  // 此兩個關于網(wǎng)絡 nework/route 配置陪白,將由網(wǎng)絡專文詳細介紹
  // 配置network,
    if err := setupNetwork(l.config); err != nil {
        return err
    }
  //  配置路由
    if err := setupRoute(l.config.Config); err != nil {
        return err
    }
  // selinux 配置
    label.Init()
  // 準備 rootfs
    if err := prepareRootfs(l.pipe, l.config); err != nil {
        return err
    }
  // 配置 console
    if l.config.CreateConsole {
        if err := setupConsole(l.consoleSocket, l.config, true); err != nil {
            return err
        }
        if err := system.Setctty(); err != nil {
            return errors.Wrap(err, "setctty")
        }
    }
  // 完成 rootfs 設置
    if l.config.Config.Namespaces.Contains(configs.NEWNS) {
        if err := finalizeRootfs(l.config.Config); err != nil {
            return err
        }
    }
  // 主機名設置
    if hostname := l.config.Config.Hostname; hostname != "" {
        if err := unix.Sethostname([]byte(hostname)); err != nil {
            return errors.Wrap(err, "sethostname")
        }
    }
  // 應用 apparmor 配置
    if err := apparmor.ApplyProfile(l.config.AppArmorProfile); err != nil {
        return errors.Wrap(err, "apply apparmor profile")
    }
  // Sysctl 系統(tǒng)參數(shù)調(diào)節(jié)
    for key, value := range l.config.Config.Sysctl {
        if err := writeSystemProperty(key, value); err != nil {
            return errors.Wrapf(err, "write sysctl key %s", key)
        }
    }
  // path 只讀屬性設置
    for _, path := range l.config.Config.ReadonlyPaths {
        if err := readonlyPath(path); err != nil {
            return errors.Wrapf(err, "readonly path %s", path)
        }
    }
    for _, path := range l.config.Config.MaskPaths {
        if err := maskPath(path, l.config.Config.MountLabel); err != nil {
            return errors.Wrapf(err, "mask path %s", path)
        }
    }
  // 獲取父進程退出信號
    pdeath, err := system.GetParentDeathSignal()
    if err != nil {
        return errors.Wrap(err, "get pdeath signal")
    }
  // 設置安全屬性 nonewprivileges
    if l.config.NoNewPrivileges {
        if err := unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); err != nil {
            return errors.Wrap(err, "set nonewprivileges")
        }
    }
  // 告訴runC進程,我們已經(jīng)完成了初始化工作
    if err := syncParentReady(l.pipe); err != nil {
        return errors.Wrap(err, "sync ready")
    }
  // 進程標簽設置
    if err := label.SetProcessLabel(l.config.ProcessLabel); err != nil {
        return errors.Wrap(err, "set process label")
    }
    defer label.SetProcessLabel("")
  
  // seccomp配置
    if l.config.Config.Seccomp != nil && !l.config.NoNewPrivileges {
        if err := seccomp.InitSeccomp(l.config.Config.Seccomp); err != nil {
            return err
        }
    }
  // 設置正確的capability膳灶,用戶以及工作目錄
    if err := finalizeNamespace(l.config); err != nil {
        return err
    }

    if err := pdeath.Restore(); err != nil {
        return errors.Wrap(err, "restore pdeath signal")
    }

    if unix.Getppid() != l.parentPid {
        return unix.Kill(unix.Getpid(), unix.SIGKILL)
    }

  // 確定用戶指定的容器進程在容器文件系統(tǒng)中的路徑
    name, err := exec.LookPath(l.config.Args[0])
    if err != nil {
        return err
    }
  
  // 關閉管道拷泽,告訴runC進程,我們已經(jīng)完成了初始化工作
    l.pipe.Close()

  
  // 在exec用戶進程之前等待exec.fifo管道在另一端被打開
  // 我們通過/proc/self/fd/$fd打開它
    fd, err := unix.Open(fmt.Sprintf("/proc/self/fd/%d", l.fifoFd), unix.O_WRONLY|unix.O_CLOEXEC, 0)
    if err != nil {
        return newSystemErrorWithCause(err, "open exec fifo")
    }
  //
  // 此處操作應注意袖瞻,作為容器運行的分界線,后面有說明 
  //
  // 向exec.fifo管道寫數(shù)據(jù)拆吆,阻塞聋迎,直到用戶調(diào)用`runc start`,讀取管道中的數(shù)據(jù)
    if _, err := unix.Write(fd, []byte("0")); err != nil {
        return newSystemErrorWithCause(err, "write 0 exec fifo")
    }

  // 關閉fifofd管道
    unix.Close(l.fifoFd)
  
  // 初始化Seccomp配置
    if l.config.Config.Seccomp != nil && l.config.NoNewPrivileges {
        if err := seccomp.InitSeccomp(l.config.Config.Seccomp); err != nil {
            return newSystemErrorWithCause(err, "init 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
}

容器用戶程序與運行

我們可以看到下面容器的運行是非常簡單的實現(xiàn)霉晕,因為在容器的 init 階段已將所有環(huán)境都準備好了,此時只需讀取管道中的數(shù)據(jù)(等同時于 bootstrap 進程發(fā)送繼續(xù)執(zhí)行信號)捞奕,將進程處于阻塞狀態(tài)的 init 進程繼續(xù)后面代碼執(zhí)行用戶定義的entrypoint程序牺堰。

libcontainer/container_linux.go:266

func (c *linuxContainer) exec() error {
    path := filepath.Join(c.root, execFifoFilename)

    fifoOpen := make(chan struct{})
    select {
    case <-awaitProcessExit(c.initProcess.pid(), fifoOpen):
        return errors.New("container process is already dead")
    case result := <-awaitFifoOpen(path):
        close(fifoOpen)
        if result.err != nil {
            return result.err
        }
        f := result.file
        defer f.Close()
        if err := readFromExecFifo(f); err != nil {      // 讀操作來解除bootstrap阻塞
            return err
        }
        return os.Remove(path)
    }
}

最后重新來看看 init 激活后會執(zhí)行的代碼:

!FILENAME libcontainer/standard_init_linux.go:192

func (l *linuxStandardInit) Init() error {
  //...
  // unix.Write()阻塞
  // 初始化Seccomp配置
  if l.config.Config.Seccomp != nil && l.config.NoNewPrivileges {
        if err := seccomp.InitSeccomp(l.config.Config.Seccomp); err != nil {
            return newSystemErrorWithCause(err, "init 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
}

附錄:

附錄一:RunC 創(chuàng)建容器及命令

RunC run 創(chuàng)建與運行容器實例:

# 1. 準備rootfs文件
$> mkdir /mycontainer; cd /mycontainer
$> docker export $(docker create busybox) | tar -C rootfs -xvf -

# 2. 創(chuàng)建一個config.json文件(標準的OCI格式的文件)
$> runc spec 

# 3. rootfs和config.json (OCI runtime bundles)都有了就可以創(chuàng)建容器
$> runc run $mycontainerid

篇幅原因不附實例的config.json文件若皱,可參考官方 config.json OCI Runtime spec 運行時規(guī)范(中文)

RunC 容器的整個生命周期管理操作:

# 創(chuàng)建
$> runc create $mycontainerid
# 啟動
$> runc start $mycontainerid
# 查看
$> runc list
# 刪除
$> runc delete $mycontainerid

相關文檔

《RunC 源碼通讀指南之 Namespace》

《RunC 源碼通讀指南之 Cgroup》

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

《RunC 源碼通讀指南之 Networks》

~~ 本文 END ~~

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末彩掐,一起剝皮案震驚了整個濱河市酒唉,隨后出現(xiàn)的幾起案子优幸,更是在濱河造成了極大的恐慌征字,老刑警劉巖寻咒,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兄朋,死亡現(xiàn)場離奇詭異常拓,居然都是意外死亡渐溶,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門弄抬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來茎辐,“玉大人,你說我怎么就攤上這事⊥下剑” “怎么了弛槐?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長慕蔚。 經(jīng)常有香客問我丐黄,道長,這世上最難降的妖魔是什么孔飒? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任灌闺,我火速辦了婚禮,結果婚禮上坏瞄,老公的妹妹穿的比我還像新娘桂对。我一直安慰自己,他們只是感情好鸠匀,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布蕉斜。 她就那樣靜靜地躺著,像睡著了一般缀棍。 火紅的嫁衣襯著肌膚如雪宅此。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天爬范,我揣著相機與錄音父腕,去河邊找鬼。 笑死青瀑,一個胖子當著我的面吹牛璧亮,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播斥难,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼枝嘶,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了哑诊?” 一聲冷哼從身側響起群扶,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎镀裤,沒想到半個月后穷当,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡淹禾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年馁菜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铃岔。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡汪疮,死狀恐怖峭火,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情智嚷,我是刑警寧澤卖丸,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站盏道,受9級特大地震影響稍浆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜猜嘱,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一衅枫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧朗伶,春花似錦弦撩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至点晴,卻和暖如春感凤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背粒督。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工陪竿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人坠陈。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像捐康,于是被迫代替她去往敵國和親仇矾。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容