容器运行时 源码分析

源码地址

https://github.com/opencontainers/runc

tagv1.2.5

整体流程

一个容器启动主要分为三大部分,如文章题目所示

  • create: 主要是为了解析、组装容器启动的配置和与子进程的消息通道等;

  • init : 主要根据容器配置启动容器整个运行环境,包括熟知ns,cgroups, seccomp, apparmor, caps等;

  • start : 主要是为了通知init 进程启动容器;

runc create:

  1. 运行runc create时,后台生成该命令的进程,我们称该进程为parent;

  2. parent进程中运行runc init,我们称runc init进程为child进程;

runc init:

  1. child进程开始准备用户进程的运行环境,此时parent和child进程通过pipe进行通信;

  2. child进程准备好用户进程的运行环境后,通知parent退出,自己则被exec.fifo阻塞;

  3. 由于parent退出(即runc create退出),child成孤独进程,进而被1进程接收;

  4. child进程一直被exec.fifo阻塞;

runc start:

  1. 运行runc start时,会打开exec.fifo,使child的阻塞消除,runc start退出;

  2. 由于阻塞消除,child进程继续往下执行;

  3. child进程使用用户定义的命令替换runc init,从而child进程成为容器内的主进程;

  4. 容器启动完成。

runc create

startContainer

  1. 生成容器配置

  2. 启动runc init 进程

// utils_linux.go

func startContainer(context *cli.Context, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) {
    if err := revisePidFile(context); err != nil {
        return -1, err
    }
    
    // 生成容器配置
    // 读取容器运行时的 config.json 配置, 转化为spec 结构体
    spec, err := setupSpec(context)
    if err != nil {
        return -1, err
    }
    // ...
    
    // 使用spec 构建容器其他配置
    container, err := createContainer(context, id, spec)
    if err != nil {
        return -1, err
    }

    // ...
    
    
    //  创建容器
    //  runner 是装载 init 进程的核心,在此前的工作都是以组装配置和校对配置为主,
    //  现在正式把配置内容装载后运行init进程;
    r := &runner{
 
        // 是否指定当前进程不收集僵尸进程,托孤行为
        enableSubreaper: !context.Bool("no-subreaper"),
        shouldDestroy:   !context.Bool("keep"),
        container:       container,
        listenFDs:       listenFDs,
        notifySocket:    notifySocket,
        consoleSocket:   context.String("console-socket"),
        pidfdSocket:     context.String("pidfd-socket"),
        detach:          context.Bool("detach"),
        pidFile:         context.String("pid-file"),
        preserveFDs:     context.Int("preserve-fds"),
        action:          action,
        // 热迁移工具的参数,在create 命令下该参数是空的
        criuOpts:        criuOpts,
        // 是否需要初始化
        init:            true,
    }
    return r.run(spec.Process)
}

createContainer

// utils_linux.go

func createContainer(context *cli.Context, id string, spec *specs.Spec) (*libcontainer.Container, error) {
    //是否使用非root 的cgroup
    rootlessCg, err := shouldUseRootlessCgroupManager(context)
    if err != nil {
        return nil, err
    }
    // 根据OCI 规范创建 container 配置文件
    // [进入CreateLibcontainerConfig]
    config, err := specconv.CreateLibcontainerConfig(&specconv.CreateOpts{
        CgroupName:       id,
        // 是否使用systemd-cgroup, 不使用的话默认选择 user.slice
        UseSystemdCgroup: context.GlobalBool("systemd-cgroup"),
        //是否不 pivotroot, 一般只有rootfs 在闪存上才不固定rootfs
        NoPivotRoot:      context.Bool("no-pivot"),
        NoNewKeyring:     context.Bool("no-new-keyring"),
        Spec:             spec,
        RootlessEUID:     os.Geteuid() != 0,
        //获取EUID, 用于系统决定用户对系统资源的访问权限,通常情况下等于RUID。 非root 情况启动;
        RootlessCgroups:  rootlessCg,
    })
    if err != nil {
        return nil, err
    }

    root := context.GlobalString("root")
    // 创建容器
    return libcontainer.Create(root, id, config)
}

CreateLibcontainerConfig

创建container 配置,主要包含

  • 指定工作路径

  • 指定根目录

  • 增加挂载设备

  • 绑定cgroup

  • 设置oom selinux等

  • 注册启动钩子

// libcontainer/specconv/spec_linux.go

func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) {
    //让runc 的工作目录固定在 spec.Root.Path 指定的目录下,没有指定即当前目录
    cwd, err := getwd()
    if err != nil {
        return nil, err
    }
    spec := opts.Spec
    if spec.Root == nil {
        return nil, errors.New("root must be specified")
    }
    // 指定rootfs, 在 config.json 里面指定了当前目录的 rootfs 文件夹
    rootfsPath := spec.Root.Path
    if !filepath.IsAbs(rootfsPath) {
        rootfsPath = filepath.Join(cwd, rootfsPath)
    }
    labels := []string{}
    for k, v := range spec.Annotations {
        labels = append(labels, k+"="+v)
    }
    // 将已有的createOpts 组装到最终的 config 上
    config := &configs.Config{
        Rootfs:          rootfsPath,
        NoPivotRoot:     opts.NoPivotRoot,
        Readonlyfs:      spec.Root.Readonly,
        Hostname:        spec.Hostname,
        Domainname:      spec.Domainname,
        Labels:          append(labels, "bundle="+cwd),
        NoNewKeyring:    opts.NoNewKeyring,
        RootlessEUID:    opts.RootlessEUID,
        RootlessCgroups: opts.RootlessCgroups,
    }

    // 根据规范挂载目录,对应的是config.json 的 mounts 字段
// 如: /proc, /dev, /dev/pts, /dev/shm, /dev/mqueue, /sys/, /sys/fs/cgroup 等
    for _, m := range spec.Mounts {
        cm, err := createLibcontainerMount(cwd, m)
        if err != nil {
            return nil, fmt.Errorf("invalid mount %+v: %w", m, err)
        }
        config.Mounts = append(config.Mounts, cm)
    }

    defaultDevs, err := createDevices(spec, config)
    if err != nil {
        return nil, err
    }

// 创建cgroup 资源控制的配置, 传入默认分区, 返回 cgroup 资源配置        
/* 可控的资源对象
var legacySubsystems = []subsystem{
    &fs.CpusetGroup{},
    &fs.DevicesGroup{},
    &fs.MemoryGroup{},
    &fs.CpuGroup{},
    &fs.CpuacctGroup{},
    &fs.PidsGroup{},
    &fs.BlkioGroup{},
    &fs.HugetlbGroup{},
    &fs.PerfEventGroup{},
    &fs.FreezerGroup{},
    &fs.NetPrioGroup{},
    &fs.NetClsGroup{},
    &fs.NameGroup{GroupName: "name=systemd"},
    &fs.RdmaGroup{},
    &fs.NameGroup{GroupName: "misc"},
}  
*/

    c, err := CreateCgroupConfig(opts, defaultDevs)
    if err != nil {
        return nil, err
    }

    config.Cgroups = c
    // ...

    // ...
   
    if spec.Process != nil {
        //  设置 oom scoret
        config.OomScoreAdj = spec.Process.OOMScoreAdj
        //  privileges
        config.NoNewPrivileges = spec.Process.NoNewPrivileges
        //  umask
        config.Umask = spec.Process.User.Umask
        
        // selinux
        config.ProcessLabel = spec.Process.SelinuxLabel
        //  赋予容器部分root的能力
        if spec.Process.Capabilities != nil {
            config.Capabilities = &configs.Capabilities{
                Bounding:    spec.Process.Capabilities.Bounding,
                Effective:   spec.Process.Capabilities.Effective,
                Permitted:   spec.Process.Capabilities.Permitted,
                Inheritable: spec.Process.Capabilities.Inheritable,
                Ambient:     spec.Process.Capabilities.Ambient,
            }
        }
        if spec.Process.Scheduler != nil {
            s := *spec.Process.Scheduler
            config.Scheduler = &s
        }

        if spec.Process.IOPriority != nil {
            ioPriority := *spec.Process.IOPriority
            config.IOPriority = &ioPriority
        }
    }
    // 容器生命周期钩子
    createHooks(spec, config)
    config.Version = specs.Version
    return config, nil
}

runner.run

  1. 设置文件描述符

  2. 设置容器的主进程为收容者

  3. 设置IO, 前台启动还是后台启动IO输出不同

  4. 启动容器

// utils_linux.go

func (r *runner) run(config *specs.Process) (int, error) {
    
    
    // 这里就是前面提到ExtraFiles, 设定的fd 从3开始加, ExtraFiles
    // 常用于增加容器 输入或 输出的 文件描述 符, 如将容器日志导出到文件。
    if len(r.listenFDs) > 0 {
        process.Env = append(process.Env, "LISTEN_FDS="+strconv.Itoa(len(r.listenFDs)), "LISTEN_PID=1")
        process.ExtraFiles = append(process.ExtraFiles, r.listenFDs...)
    }
    baseFd := 3 + len(process.ExtraFiles)
    procSelfFd, closer := utils.ProcThreadSelf("fd/")
    defer closer()
    for i := baseFd; i < baseFd+r.preserveFDs; i++ {
        _, err = os.Stat(filepath.Join(procSelfFd, strconv.Itoa(i)))
        if err != nil {
            return -1, fmt.Errorf("unable to stat preserved-fd %d (of %d): %w", i-baseFd, r.preserveFDs, err)
        }
        process.ExtraFiles = append(process.ExtraFiles, os.NewFile(uintptr(i), "PreserveFD:"+strconv.Itoa(i)))
    }
    

    
    // 启用 containerd enableSubreaper 选项后,containerd 将在容器内部
    // 的主进程上设置子进程收容者属性。这意味着容器内的子进程将由容器的主进程作为其子进程收容者,
    // 而不是由容器的直接父进程收容。
    handler := newSignalHandler(r.enableSubreaper, r.notifySocket)
    
    // 设置进程的IO
    // console 启动,还是后台启动,输入和输出的通道不同
    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:
    // 其实这几个action 最终实现的动作都差不多,后面还会有新的文章进行详解;
   // 本次主要讲的是 create 动作
        err = r.container.Start(process)
    case CT_ACT_RESTORE:
        err = r.container.Restore(process, r.criuOpts)
    case CT_ACT_RUN:
        err = r.container.Run(process)
    default:
        panic("Unknown action")
    }
    // ...
}

Container.start

process.Init 作为一个布尔值(true/false)用于标识 当前进程是否是容器的 init 进程。它的作用如下:

1. 当 process.Init == true 时,表示 该进程是容器的第一个进程(即 init 进程)。这个进程:

  • 负责 创建新的 Namespace(如 PID, NET, MNT 等)。

  • 负责 初始化容器(如 Rootfs 挂载、Cgroups 配置)。

  • 作为 容器的主进程,它的 PID 通常是 1(容器内部)。

  • 如果该进程退出,整个容器会停止。

2. 当 process.Init == false 时,表示 该进程是附加到现有容器中的新进程,通常用于 runc exec

  • 该进程 不会创建新的 Namespace,而是进入已有容器的 Namespace。

  • 该进程 不会影响容器的生命周期,即使它退出,容器仍然运行。

  • newSetnsProcess 处理,用于 runc exec 运行附加进程。

// libcontainer/container_linux.go
func (c *Container) start(process *Process) (retErr error) {
    if c.config.Cgroups.Resources.SkipDevices {
        return errors.New("can't start container with SkipDevices set")
    }
    // 容器的主进程创建
    if process.Init {
        if c.initProcessStartTime != 0 {
            return errors.New("container already has init process")
        }
        // 创建exec.fifo ,用于后续阻塞容器进程,,待外部启动。
        if err := c.createExecFifo(); err != nil {
            return err
        }
        defer func() {
            if retErr != nil {
                // 执行完毕后,删除exec.fifo文件,看到这句应该大概猜到上面exec.fifo 
                //文件可能和 start 进程的执行有关系
                c.deleteExecFifo()
            }
        }()
    }
    // 创建容器进程(场景为docker run) 或 创建已有容器附加进程(场景为docker exec)
    parent, err := c.newParentProcess(process)
    //...
    
    // 启动进程
    if err := parent.start(); err != nil { 
          return fmt.Errorf("unable to start container process: %w", err)
    } 

}

c.createExecFifo() 作用

runc exec 在一个正在运行的容器中执行新进程时:

  • createExecFifo() 创建一个 FIFO 文件,路径通常是:

/run/containerd/io.containerd.runtime.v2.task/<container-id>/exec.fifo

exec.fifo 的用途

场景 1:正常执行 runc exec

runc exec 进程启动后,会尝试 打开 exec.fifo 并阻塞等待

容器的 setns 过程(进入容器的 Namespace)完成后,runc 写入 exec.fifo

exec 进程检测到 FIFO 被写入,解除阻塞,开始执行。

场景 2:防止 runc exec 竞态

如果 exec 进程 在 Namespace 还未完全切换时就执行,可能会导致:

进程启动时仍在宿主机 Namespace,而不是容器的环境。

可能会导致 exec 进程访问错误的 Cgroups 或文件系统。

交互流程

假设 runc exec 运行 /bin/bash 进入容器:

r
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值