"runc启动容器过程分析"

  "runc模块启动容器"

Posted by Xu on August 21, 2017

runc模块启动容器过程

经过contaienrd-shim这一层containerd到runc之间的夹层处理,上一章中containerd-shim已经将容器启动的工作交付到runc模块来处理,而调用runc命令如下:

runCtoCriuStartContainer


docker-runc –log /run/docker/libcontainerd/containerd/e41e4aa7a42c363ae019af5ad519d20d1d302380c9e72c02f6cb06b3d562637e/init/log.json –log-format json restore -d –image-path /var/local/p.haul-fs/rst-gk4dpK-17.08.16-20.45/img/1/mysql_checkpoint –work-path /var/local/p.haul-fs/rst-gk4dpK-17.08.16-20.45/img/1/mysql_checkpoint/criu.work/restore-2017-08-16T20:46:10-07:00 –tcp-established –ext-unix-sk –empty-ns network –pid-file /run/docker/libcontainerd/containerd/e41e4aa7a42c363ae019af5ad519d20d1d302380c9e72c02f6cb06b3d562637e/init/pid e41e4aa7a42c363ae019af5ad519d20d1d302380c9e72c02f6cb06b3d562637e ***

所以今天这一章节我们开始分析runc是如何完成启动容器的工作的:

首先我们需要了解到runc是一个实现OCI开放容器接口标准的命令行工具,它非常好的集成了已存在的进程监视器功能来为应用提供产品级别容器运行环境。容器使用bundles来配置,bundle是一个包含特定spec文件的目录及文件系统的目录。所以一个容器可以直接根据该容器相关信息及内容的目录来启动。

主函数入口:主要先配置runc app的flag相关信息和runc所有的Commands命令集合,最后执行app.run(os.Args)来进入具体命令的执行

app执行的步骤分为三步:

  • app.Before()用于处理log和log-format标志位来设置日志系统
  • app.Commands[name].Run()子命令的执行,在这里的调用中子命令为restore,然后调用restoreCommand.Action()。
  • app.After()做一些善后工作

这里的子命令app.Commands[name]为restoreCommand,所以我们主要分析该命令中Action内的恢复流程

	Action: func(context *cli.Context) error {
		imagePath := context.String("image-path")//获取checkpoint镜像文件的路径
		id := context.Args().First()//获取容器id
		if id == "" {
			return errEmptyID
		}
		if imagePath == "" {//如果没检查点文件路径则获取默认的检查点路径
			imagePath = getDefaultImagePath(context)
		}
		bundle := context.String("bundle")//获取容器文件系统的根目录
		if bundle != "" {
			if err := os.Chdir(bundle); err != nil {
				return err
			}
		}
		spec, err := loadSpec(specConfig)//根据config.json文件内容加载容器的配置信息
		if err != nil {
			return err
		}
		config, err := specconv.CreateLibcontainerConfig(&specconv.CreateOpts{
			CgroupName:       id,
			UseSystemdCgroup: context.GlobalBool("systemd-cgroup"),
			NoPivotRoot:      context.Bool("no-pivot"),
			Spec:             spec,
		})//根据给定的规格spec和cgroup名称创建一个新的libcontainer配置对象
		if err != nil {
			return err
		}
		status, err := restoreContainer(context, spec, config, imagePath)//根据配置信息和规格及检查点镜像路径来恢复容器
		if err == nil {
			os.Exit(status)
		}
		return err
	}

根据规格信息spec和配置信息config及检查点镜像文件路径来恢复容器运行

func restoreContainer(context *cli.Context, spec *specs.Spec, config *configs.Config, imagePath string) (int, error) {
	var (
		rootuid = 0
		rootgid = 0
		id      = context.Args().First()
	)
	factory, err := loadFactory(context)//返回一个被配置好的LinuxFactory对象用于执行容器
	if err != nil {
		return -1, err
	}
	container, err := factory.Load(id)//首先根据id来得到容器根路径ContainerRoot,再从该路径下的state.json文件中读取状态信息,根据LinuxFactory和状态信息来配置 linuxContainer对象
	if err != nil {
	//当加载失败时选择创建一个新的LinuxContainer
		container, err = factory.Create(id, config)
		if err != nil {
			return -1, err
		}
	}
	options := criuOptions(context)//返回criuOpts对象

	status, err := container.Status()//返回该容器目前的状态信息
	if err != nil {
		logrus.Error(err)
	}
	if status == libcontainer.Running {
		fatalf("Container with id %s already running", id)
	}

	setManageCgroupsMode(context, options)

	if err := setEmptyNsMask(context, options); err != nil {
		return -1, err
	}

	// ensure that the container is always removed if we were the process
	// that created it.
	detach := context.Bool("detach")
	if !detach {
		defer destroy(container)
	}
	process := &libcontainer.Process{}
	tty, err := setupIO(process, rootuid, rootgid, "", false, detach)
	if err != nil {
		return -1, err
	}
	defer tty.Close()
	handler := newSignalHandler(tty, !context.Bool("no-subreaper"))
	if err := container.Restore(process, options); err != nil {
	//尝试调用criu恢复容器
		return -1, err
	}
	if err := tty.ClosePostStart(); err != nil {
		return -1, err
	}
	if pidFile := context.String("pid-file"); pidFile != "" {
		if err := createPidFile(pidFile, process); err != nil {
			process.Signal(syscall.SIGKILL)
			process.Wait()
			return -1, err
		}
	}
	if detach {
		return 0, nil
	}
	return handler.forward(process)
}

criureq请求配置

设置criu工作路径,获取容器的挂载信息,describtor.json文件描述符信息,pipe类型文件信息,然后配置到criureq对象中去,调用criuswrk来进行rpc调用

func (c *linuxContainer) Restore(process *Process, criuOpts *CriuOpts) error {
	c.m.Lock()
	defer c.m.Unlock()
	if err := c.checkCriuVersion("1.5.2"); err != nil {
		return err
	}//检查criu版本
	if criuOpts.WorkDirectory == "" {
		criuOpts.WorkDirectory = filepath.Join(c.root, "criu.work")
	} //设置criu工作目录路径
	// Since a container can be C/R'ed multiple times,
	// the work directory may already exist.
	if err := os.Mkdir(criuOpts.WorkDirectory, 0655); err != nil && !os.IsExist(err) {
		return err
	}
	workDir, err := os.Open(criuOpts.WorkDirectory)//创建并打开工作目录
	if err != nil {
		return err
	}
	defer workDir.Close()
	if criuOpts.ImagesDirectory == "" {
		return fmt.Errorf("invalid directory to restore checkpoint")
	}
	imageDir, err := os.Open(criuOpts.ImagesDirectory)//打开检查点镜像文件目录
	if err != nil {
		return err
	}
	defer imageDir.Close()
	// CRIU has a few requirements for a root directory:
	// * it must be a mount point
	// * its parent must not be overmounted
	// c.config.Rootfs is bind-mounted to a temporary directory
	// to satisfy these requirements.
	root := filepath.Join(c.root, "criu-root")
	if err := os.Mkdir(root, 0755); err != nil {
		return err
	}//创建criu-root目录,之后删除
	defer os.Remove(root)
	root, err = filepath.EvalSymlinks(root)
	if err != nil {
		return err
	}
	err = syscall.Mount(c.config.Rootfs, root, "", syscall.MS_BIND|syscall.MS_REC, "")//挂载criu-root目录
	if err != nil {
		return err
	}
	defer syscall.Unmount(root, syscall.MNT_DETACH)
	t := criurpc.CriuReqType_RESTORE//该rpc请求为restore请求
	req := &criurpc.CriuReq{//设置criu rpc请求体内容
		Type: &t,
		Opts: &criurpc.CriuOpts{
			ImagesDirFd:    proto.Int32(int32(imageDir.Fd())),
			WorkDirFd:      proto.Int32(int32(workDir.Fd())),
			EvasiveDevices: proto.Bool(true),
			LogLevel:       proto.Int32(4),
			LogFile:        proto.String("restore.log"),
			RstSibling:     proto.Bool(true),
			Root:           proto.String(root),
			ManageCgroups:  proto.Bool(true),
			NotifyScripts:  proto.Bool(true),
			ShellJob:       proto.Bool(criuOpts.ShellJob),
			ExtUnixSk:      proto.Bool(criuOpts.ExternalUnixConnections),
			TcpEstablished: proto.Bool(criuOpts.TcpEstablished),
			FileLocks:      proto.Bool(criuOpts.FileLocks),
			EmptyNs:        proto.Uint32(criuOpts.EmptyNs),
		},
	}

	for _, m := range c.config.Mounts {//遍历配置文件中的Mounts信息
		switch m.Device {
		case "bind":
			c.addCriuRestoreMount(req, m)
			break
		case "cgroup":
			binds, err := getCgroupMounts(m)
			if err != nil {
				return err
			}
			for _, b := range binds {
				c.addCriuRestoreMount(req, b)
			}
			break
		}
	}

	if criuOpts.EmptyNs&syscall.CLONE_NEWNET == 0 {
		c.restoreNetwork(req, criuOpts)
	}

	// append optional manage cgroups mode
	if criuOpts.ManageCgroupsMode != 0 {
		if err := c.checkCriuVersion("1.7"); err != nil {
			return err
		}
		mode := criurpc.CriuCgMode(criuOpts.ManageCgroupsMode)
		req.Opts.ManageCgroupsMode = &mode
	}

	var (
		fds    []string
		fdJSON []byte
	)
	if fdJSON, err = ioutil.ReadFile(filepath.Join(criuOpts.ImagesDirectory, descriptorsFilename)); err != nil {//读取文件描述符descriptor.json中的数据
		return err
	}

	if err := json.Unmarshal(fdJSON, &fds); err != nil {//将文件描述符的数据解码到fds结构体对象
		return err
	}
	for i := range fds {//遍历结构体对象若存在pipe类型文件,获取他的key和fd
		if s := fds[i]; strings.Contains(s, "pipe:") {
			inheritFd := new(criurpc.InheritFd)
			inheritFd.Key = proto.String(s)
			inheritFd.Fd = proto.Int32(int32(i))
			req.Opts.InheritFd = append(req.Opts.InheritFd, inheritFd)
		}
	}
	return c.criuSwrk(process, req, criuOpts, true)//调用criu,发送criu请求
}

打开criurpc服务,发送请求数据

根据criureq来配置criu请求的准备工作,包括criu客户端和服务端的建立,及criu swrk 3命令的执行来打开criu rpc服务进程提供criu服务,最后向criu客户端写入请求数据,获取响应数据后解析

func (c *linuxContainer) criuSwrk(process *Process, req *criurpc.CriuReq, opts *CriuOpts, applyCgroups bool) error {
	fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_SEQPACKET|syscall.SOCK_CLOEXEC, 0)//建立socketpair的管道
	if err != nil {
		return err
	}

	logPath := filepath.Join(opts.WorkDirectory, req.GetOpts().GetLogFile())//获取criu日志路径
	criuClient := os.NewFile(uintptr(fds[0]), "criu-transport-client")//打开criu客户端文件,作为socketpair的一个端口
	criuServer := os.NewFile(uintptr(fds[1]), "criu-transport-server")//打开criu服务器端文件,作为socketpair另一个端口
	defer criuClient.Close()
	defer criuServer.Close()

	args := []string{"swrk", "3"}
	logrus.Debugf("Using CRIU %d at: %s", c.criuVersion, c.criuPath)
	logrus.Debugf("Using CRIU with following args: %s", args)
	cmd := exec.Command(c.criuPath, args...)
	if process != nil {
		cmd.Stdin = process.Stdin
		cmd.Stdout = process.Stdout
		cmd.Stderr = process.Stderr
	}
	cmd.ExtraFiles = append(cmd.ExtraFiles, criuServer)//该命令执行的进程会继承criuServer这个打开的文件

	if err := cmd.Start(); err != nil {
	//执行criu命令
		return err
	}
	criuServer.Close()

	defer func() {
		criuClient.Close()
		_, err := cmd.Process.Wait()
		if err != nil {
			return
		}
	}()

	if applyCgroups {
		err := c.criuApplyCgroups(cmd.Process.Pid, req)//给req配置cgroup的路径
		if err != nil {
			return err
		}
	}

	var extFds []string
	if process != nil {
		extFds, err = getPipeFds(cmd.Process.Pid)//获取执行cmd命令进程的(/proc/pid/fd)所有文件描述符内容
		if err != nil {
			return err
		}
	}

	logrus.Debugf("Using CRIU in %s mode", req.GetType().String())
	val := reflect.ValueOf(req.GetOpts())//获取criuOpts
	v := reflect.Indirect(val)//返回val指针所指向的CriuOpts对象
	for i := 0; i < v.NumField(); i++ {
	//NumField返回v结构体字段的数量
		st := v.Type()
		name := st.Field(i).Name
		if strings.HasPrefix(name, "XXX_") {
			continue
		}
		value := val.MethodByName("Get" + name).Call([]reflect.Value{})//获取val指针所指向对象所拥有的Get*name*方法的信息然后调用,得到CriuOpts中每个属性的值
		logrus.Debugf("CRIU option %s with value %v", name, value[0])
	}//这个for循环就是打印该结构体中的key和value
	data, err := proto.Marshal(req)//将criureq请求格式化为proto格式数据
	if err != nil {
		return err
	}
	_, err = criuClient.Write(data)//想criu客户端写入请求数据
	if err != nil {
		return err
	}

	buf := make([]byte, 10*4096)
	for true {
		n, err := criuClient.Read(buf)//循环读取criu客户端中的数据
		if err != nil {
			return err
		}
		if n == 0 {
			return fmt.Errorf("unexpected EOF")
		}
		if n == len(buf) {
			return fmt.Errorf("buffer is too small")
		}

		resp := new(criurpc.CriuResp)//新建一个criu响应结构体
		err = proto.Unmarshal(buf[:n], resp)//将criu客户端中的数据填充到响应结构体中去
		if err != nil {
			return err
		}
		if !resp.GetSuccess() {//判断criu请求是否成功
			typeString := req.GetType().String()
			return fmt.Errorf("criu failed: type %s errno %d\nlog file: %s", typeString, resp.GetCrErrno(), logPath)//失败返回错误
		}

		t := resp.GetType()
		switch {//判断响应的类型
		case t == criurpc.CriuReqType_NOTIFY:
			if err := c.criuNotifications(resp, process, opts, extFds); err != nil {
				return err
			}
			t = criurpc.CriuReqType_NOTIFY
			req = &criurpc.CriuReq{
				Type:          &t,
				NotifySuccess: proto.Bool(true),
			}
			data, err = proto.Marshal(req)
			if err != nil {
				return err
			}
			_, err = criuClient.Write(data)
			if err != nil {
				return err
			}
			continue
		case t == criurpc.CriuReqType_RESTORE:
		case t == criurpc.CriuReqType_DUMP:
			break
		default:
			return fmt.Errorf("unable to parse the response %s", resp.String())
		}

		break
	}

	// cmd.Wait() waits cmd.goroutines which are used for proxying file descriptors.
	// Here we want to wait only the CRIU process.
	st, err := cmd.Process.Wait()//等待criu命令的执行进程完成
	if err != nil {
		return err
	}
	if !st.Success() {
		return fmt.Errorf("criu failed: %s\nlog file: %s", st.String(), logPath)
	}
	return nil
}