容器运行时runc内部实现深入解读

runc 内部实现深入解读

最近把runc的实现代码仔细的看了一遍,有点复杂,特别是nsenter那块反反复复看了不下三遍才搞懂是个什么机制,不过确实写得不错,防止日后遗忘,另外也给其他朋友一点参考(网上有一些关于runc的介绍文章,但是没有nsenter这块的介绍)

概要设计

runc的实现分为两个大阶段:

  • 第一个阶段叫bootstrap, 设置一些环境以及配置信息, 创建匿名管道, 把容器的配置信息通过管道发送给第二阶段。
  • 第二个阶段代码里没有明确的名称,就把它叫做worker阶段吧, 主要就是创建子进程并根据管道传过来的配置信息,设置进程的namespace信息。

bootstrap和worker主要通过socketpair这种全双工管道交互,这么实现的主要考虑是worker的大部分时间处于自己的namespace中,所以有些事情自己做不了,必须让bootstrap协助完成。

另外bootstrap用GO语言实现,worker的开始的最重要的部分用C语言实现,后面的部分也是以GO语言实现,中间的切换非常美妙。

另外值得说一下的是runc支持linux,windows,solaris等多种操作系统,所以代码的一个通用规则是对于某个功能是定一个一个接口,然后针对各种平台的实现定义一个独立的文件,比如

container.go
container_linux.go
container_widnows.go
container_solaris.go

这样可以方便理解实现,也不要被代码量吓倒。

bootstrap代码分析

下面结合runc create <nn>来一起看一下runc具体执行流程:
create.go:

spec, err := setupSpec(context)

进入到中执行,先解析config.json文件,下载bundle,然后

status, err := startContainer(context, spec, CT_ACT_CREATE, nil)

开始了utils_linux.go,

func startContainer(context *cli.Context, spec *specs.Spec, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) {
	container, err := createContainer(context, id, spec)
	.....
	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,
		criuOpts:        criuOpts,
	}
	return r.run(spec.Process)
}

看一下createContainer的实现

func createContainer(context *cli.Context, id string, spec *specs.Spec) (libcontainer.Container, error) {
	....
	factory, err := loadFactory(context)
	return factory.Create(id, config)
}

loadFactory最重会走到factory_linux.go

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{
		Root:      root,
		InitArgs:  []string{"/proc/self/exe", "init"},
		Validator: validate.New(),
		CriuPath:  "criu",
	}
	Cgroupfs(l)
	for _, opt := range options {
		if err := opt(l); err != nil {
			return nil, err
		}
	}
	return l, nil
}

到目前为止都很简单,两个值得注意下的
InitArgs: []string{"/proc/self/exe", “init”}/proc/self/exe是指向自身,也就是runc,这个很重要,后面会涉及到
options里面定义了具体采用哪种方式的cgroups,给予文件的还是systemd的方式

好了,有了factory,那就用这个factory创建container结构体,

func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, error) {
	containerRoot := filepath.Join(l.Root, id)
	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)
	}
	if config.Rootless {
		RootlessCgroups(l)
	}
	c := &linuxContainer{
		id:            id,
		root:          containerRoot,
		config:        config,
		initArgs:      l.InitArgs,
		criuPath:      l.CriuPath,
		cgroupManager: l.NewCgroupsManager(config.Cgroups, nil),
	}
	c.state = &stoppedState{c: c}
	return c, nil
}

看到创建了一个linuxContainer结构体,并设置状态为Stopped, 最后开始run这个container,注意这个run不适真正的run,只是初始化container的许多信息而已。

func (r *runner) run(config *specs.Process) (int, error) {
	process, err := newProcess(*config)
	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++ {
		process.ExtraFiles = append(process.ExtraFiles, os.NewFile(uintptr(i), "PreserveFD:"+strconv.Itoa(i)))
	}
	...
	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)
	default:
		panic("Unknown action")
	}

最重进入container_linux.go : linuxContainer.Start

func (c *linuxContainer) start(process *Process, isInit bool) error {
	parent, err := c.newParentProcess(process, isInit)
	if err != nil {
		return newSystemErrorWithCause(err, "creating new parent process")
	}
	if err := parent.start(); err != nil {
		// terminate the process to ensure that it properly is reaped.
		if err := parent.terminate(); err != nil {
			logrus.Warn(err)
		}
		return newSystemErrorWithCause(err, "starting container process")
	}
	...
	
func (c *linuxContainer) newParentProcess(p *Process, doInit bool) (parentProcess, error) {
	parentPipe, childPipe, err := utils.NewSockPair("init")
	if err != nil {
		return nil, newSystemErrorWithCause(err, "creating new init pipe")
	}
	cmd, err := c.commandTemplate(p, childPipe)
	if err != nil {
		return nil, newSystemErrorWithCause(err, "creating new command template")
	}
	if !doInit {
		return c.newSetnsProcess(p, cmd, parentPipe, childPipe)
	}

	// We only set up fifoFd if we're not doing a `runc exec`. The historic
	// reason for this is that previously we would pass a dirfd that allowed
	// for container rootfs escape (and not doing it in `runc exec` avoided
	// that problem), but we no longer do that. However, there's no need to do
	// this for `runc exec` so we just keep it this way to be safe.
	if err := c.includeExecFifo(cmd); err != nil {
		return nil, newSystemErrorWithCause(err, "including execfifo in cmd.Exec setup")
	}
	return c.newInitProcess(p, cmd, parentPipe, childPipe)
}

主要做了三件事:

  • 创建SockPair管道
  • 创建cmd, 并把管道的一端给cmd
  • 根据状态创建initProcess 或者 setnsProcess

侧重看一下cmd:

func (c *linuxContainer) commandTemplate(p *Process, childPipe *os.File) (*exec.Cmd, error) {
	cmd := exec.Command(c.initArgs[0], c.initArgs[1:]...)
	cmd.Stdin = p.Stdin
	cmd.Stdout = p.Stdout
	cmd.Stderr = p.Stderr
	cmd.Dir = c.config.Rootfs
	if cmd.SysProc
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值