RunC 源码通读指南之 — Run

概述

RunC 是 Docker 贡献出来,按照 OCI 运行时标准制定的一种具体实现,是一个可执行应用程序包工具。可通过OCI 镜像格式标准文件包 bundles 来创建和运行容器以及对容器的生命周期管理详情参考。 本文《附录一》附有使用 runc 工具命令来运行容器的示例可供参考。

从 docker 容器的整个架构和执行流程视角来看是由 containerd-shim 组件调用了 runc 来创建和运行容器,其创建容器时的配置文件/run/docker/libcontainerd/​$containerID/config.json,进行读取转化为 spec 标准作为 runc 创建容器全局配置参数。

本文将重点聚焦在 runc run 命令的执行代码的整个流程上(容器的创建至容器的运行)的解析,而有些细节实现比如 namespace 、cgroup 、网络等将在此套系列文档有详细介绍可供参阅。

从 runc run 代码执行结构可简单分为为四块执行组成部分:1. Run 命令执行入口 2. 容器对象创建 3. 容器执行 init 初始化 4. 容器用户程序与运行 ,本文下面将顺序地进行展开解析。

CLI run 执行入口

Cli app 的 run 命令执行,读取命令参数和读取与转化 config.json 为 spec 标准配置。

!FILENAME run.go:65

Action: func(context *cli.Context) error {
    // 命令参数校验
        if err := checkArgs(context, 1, exactArgs); err != nil {
            return err
        }
    // 获取"pid-file"传参配置,转化为绝对路径
        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() 启动容器顶层代码执行过程:

  • 读取传入的 container id 参数
  • 通过 spec 配置与 id 等传参创建容器对象
  • 构建 runner 启动器并执行

!FILENAME utils_linux.go:430

func startContainer(context *cli.Context, spec *specs.Spec, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) {
  // 通过 spec 创建容器结构,在 createContainer 中将 spec 转换为了 runc 的 container config*
    id := context.Args().First() //命令行输入的container id参数
    if id == "" {
        return -1, errEmptyID
    }

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

  // +创建容器对象
    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 执行标志
        criuOpts:        criuOpts,                          // criu 热迁移选项
        init:            true,                              // 用于设置 process.Init 字段
    logLevel:        logLevel,                          // 日志级别 default info
    } 
  return r.run(spec.Process)  // run() 启动
}

Container 容器对象创建

RunC 代码实例化容器对象的代码模式是通过工厂方法实现,实例化 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 转换 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)   //+  创建工厂实例
    if err != nil {
        return nil, err
    }
    return factory.Create(id, config)      //+  工厂实例化容器对象 
}

loadFactory() 创建容器工厂 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 容器内外 uid 映射
    if err != nil {
        newuidmap = ""
    }
    newgidmap, err := exec.LookPath("newgidmap")  // newgidmap 容器内外 uid 映射
    if err != nil {
        newgidmap = ""utils_linux.go
    }

  // 创建容器工厂
    return libcontainer.New(abs, cgroupManager, intelRdtManager,
        libcontainer.CriuPath(context.GlobalString("criu")),
        libcontainer.NewuidmapPath(newuidmap),
        libcontainer.NewgidmapPath(newgidmap))
}

创建LinuxFactory类型的factoy对象,用于容器对象的创建工厂

!FILENAME libcontainer/factory_linux.go:131

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{
    // 存储容器状态的根目录,默认"/run/runc/"
        Root:      root,
    // 指向当前的 exe 程序,即 runc 本身
        InitPath:  "/proc/self/exe",   
    // os.Args[0] 是当前 runc 的路径,本质上和 InitPath 是一样的,即 runc init
        InitArgs:  []string{os.Args[0], "init"},
    // 配置校验器对象
        Validator: validate.New(),
    // 热迁移路径设置
        CriuPath:  "criu",
    }
    Cgroupfs(l)  //为 LinuxFactory 配置 NewCgroupsManage实现 func
    for _, opt := range options {
        if opt == nil {
            continue
        }
        if err := opt(l); err != nil {
            return nil, err
        }
    }
    return l, nil
}

基于全局配置,容器工厂创建 linuxContainer 容器对象

!FILENAME libcontainer/factory_linux.go:188

func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, error) {
  // 确保containerRoot目录被创建
    if l.Root == "" {
        return nil, newGenericError(fmt.Errorf("invalid root"), ConfigInvalid)
    }
  // 校验参数
    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)
    }
  
  // 创建 linux 容器结构
    c := &linuxContainer{
        id:            id,               // 容器 ID
        root:          containerRoot,    // 容器状态文件存放目录,默认是 /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(资源调配技术)配置
    if intelrdt.IsCatEnabled() || intelrdt.IsMbaEnabled() {
        c.intelRdtManager = l.NewIntelRdtManager(config, id, "")
    }
    c.state = &stoppedState{c: c}       // 开始置为"stopped"状态
    return c, nil
}

Runner 执行和 init 容器初始化

根据 startContainer() 顶层的执行流程,在创建容器化对象后,构建 runner 对象并执行run()。此 run() 则是容器的运行阶段的入口,它包含两大块执行过程:Start() 容器环境初始化启动 和 exec () 容器用户程序执行,start() 初始化启动过程比较复杂一些容器的初始工作都在此过程中,包含cgroup 、namespace等等核心初始化工作(专文说明),而 start() 过程则比较简单仅取消bootstrap进程的阻塞态,让其完成执行entrypoint。后面将进行详细的描述。

Runner.run() 创建初始化进程对象,调用 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 创建 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)                   // +调用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 配置初始化并创建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 执行流程调用:

  • Start() Init 进程执行启动
  • exec() 用户进程EntryPoint 执行

libcontainer/container_linux.go:250

func (c *linuxContainer) Run(process *Process) error {
    if err := c.Start(process); err != nil {   // +容器环境 init 启动
        return err
    }
    if process.Init {
        return c.exec()                          // +EntryPoint 执行
    }
    return nil
}

调用 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() 为一个完整的上层容器实始化执行流程代码,首先通过上面传参的 process 进程对象创建 “父” 进程并启动(核心逻辑处),完成启动后保存容器状态到 state.json 文件(默认"/run/runc/$containerID/ state.json"),最后如果容器有定义运行后勾子将被调用执行。

libcontainer/container_linux.go:335

func (c *linuxContainer) start(process *Process) error {
  // +创建的父进程
    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")
    }
    // 容器启动状态 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 容器运行后勾子执行
            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() 创建父进程的过程:

  • 创建父子进程通信的 pipe ( bootstrapData 配置数据用此传递)
  • 创建 cmd 对象 ( 此处的cmd 对象就是执行 runc init ,后面有详述 )
  • 返回 newInitProcess() initProcess 对象

libcontainer/container_linux.go:441

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

  //...
  // +创建父进程的 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)   
}

创建父进程的 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)
   // 通过环境变量 _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) {
  // 这里通过环境变量 _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 配置,然后序列化成字节数据
    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 配置序列化数据
        sharePidns:      sharePidns,
    }
    c.initProcess = init        
    return init, nil                              //返回 init 进程对象
}

InitProcess.start() 则是容器运行最核心的代码执行逻辑块:

当前执行进程我们称之为“bootstrap进程“,cmd.Start() 实则执行了 "runc init" 命令,同时也激活了nsenter 模块C 代码的优先执行配置namespace (详细可参阅《RunC 源码通读指南之 NameSpace》),完后返回执行 init Go 代码部分完成后续的初始化工作,最后向管道 exec.fifo 进行写操作,init 进程进入阻塞状态等待信号完成容器内的entrypoint执行。

!FILENAME libcontainer/process_linux.go:282

func (p *initProcess) start() error {
    defer p.messageSockPair.parent.Close()
  //  当前执行空间进程称为bootstrap进程
  //  启动了 cmd,即启动了 runc init 命令,创建 runc init 子进程 
  //  同时也激活了C代码nsenter模块的执行(为了 namespace 的设置 clone 了三个进程parent、child、init)
  //  C 代码执行后返回 go 代码部分,最后的 init 子进程为了好区分此处命名为" nsInit "(即配置了Namespace的init)
  //  runc init go代码为容器初始化其它部分(网络、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")
    }

  //...
  // 创建网络接口
    if err := p.createNetworkInterfaces(); err != nil {
        return newSystemErrorWithCause(err, "creating network interfaces")
    }
  // 发送 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 启动前执行勾子
            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 勾子执行
      //  配置 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 {
            //...
        // 执行勾子定义任务
                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 代码执行逻辑

RunC init 命令执行 Go 调用 C 代码称之 preamble ,即在 import nsenter 模块时机将会在 Go 的 runtime 启动之前,先执行此先导代码块,nsenter 的初始化 init(void) 方法内对 nsexec() 调用

!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 设置,并通过消息协调后面两个进程的运行管理,在收到 grandchild 回复任务完成消息后退出。
  • 第二个进程称为“ child ”,由 Parent 创建,完成 namespace 的设置 ,fork 出 grandChild 进程并发送给Parent 后发送任务完成消息后退出。
  • 第三个进程称为“ grandChild ”或" init ",进行最后的环境准备工作(sid、uid、gid、cgroup namespace),执行完成后return 至 init Go runtime 代码处继续执行最后进入 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 代码部分)执行逻辑

创建 factory 对象,执行 factory.StartInitialization() => linuxStandardInit.Init() 完成容器的相关初始化配置(网络/路由、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("")                          // +创建 factory 对象
        if err := factory.StartInitialization(); err != nil {       // +执行 init 初始化
            os.Exit(1)
        }
        panic("libcontainer: container init failed to exec")
    },
}

libcontainer.New() 创建 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实现对象的Init()方法调用 "linuxStandardInit.Init()"
  return i.Init()                    
}

创建 container 容器对象

!FILENAME libcontainer/factory_linux.go:188

func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, error) {
  // 创建 linux 容器结构
    c := &linuxContainer{ 
  //...
    }
    return c, nil
}

linuxContainer.Init() 对网络/路由、rootfs、selinux、console、主机名、apparmor、sysctl、seccomp、capability 等容器的相关初始化配置。管道 exec.fifo 进行写操作,进入阻塞状态等待 runC start

!FILENAME libcontainer/standard_init_linux.go:46

func (l *linuxStandardInit) Init() error {
  //...
  // 此两个关于网络 nework/route 配置,将由网络专文详细介绍
  // 配置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 系统参数调节
    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进程,我们已经完成了初始化工作
    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)
    }

  // 确定用户指定的容器进程在容器文件系统中的路径
    name, err := exec.LookPath(l.config.Args[0])
    if err != nil {
        return err
    }
  
  // 关闭管道,告诉runC进程,我们已经完成了初始化工作
    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管道写数据,阻塞,直到用户调用`runc start`,读取管道中的数据
    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")
        }
    }
  // 调用系统exec()命令,执行entrypoint
    if err := syscall.Exec(name, l.config.Args[0:], os.Environ()); err != nil {
        return newSystemErrorWithCause(err, "exec user process")
    }
    return nil
}

容器用户程序与运行

我们可以看到下面容器的运行是非常简单的实现,因为在容器的 init 阶段已将所有环境都准备好了,此时只需读取管道中的数据(等同时于 bootstrap 进程发送继续执行信号),将进程处于阻塞状态的 init 进程继续后面代码执行用户定义的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 激活后会执行的代码:

!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")
        }
    }
    
  // 调用系统exec()命令,执行entrypoint
    if err := syscall.Exec(name, l.config.Args[0:], os.Environ()); err != nil {
        return newSystemErrorWithCause(err, "exec user process")
    }
    return nil
}

附录:

附录一:RunC 创建容器及命令

RunC run 创建与运行容器实例:

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

# 2. 创建一个config.json文件(标准的OCI格式的文件)
$> runc spec 

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

篇幅原因不附实例的config.json文件,可参考官方 config.json 和 OCI Runtime spec 运行时规范(中文)

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

# 创建
$> runc create $mycontainerid
# 启动
$> runc start $mycontainerid
# 查看
$> runc list
# 删除
$> runc delete $mycontainerid

相关文档

《RunC 源码通读指南之 Namespace》

《RunC 源码通读指南之 Cgroup》

《RunC 源码通读指南之 Create & Start》

《RunC 源码通读指南之 Networks》

~~ 本文 END ~~

你可能感兴趣的:(RunC 源码通读指南之 — Run)