1、bundle

docker处理流程到runc阶段,所需的如下已经准备就绪了。

  1. 容器名称(container id)
  2. 根文件系统(rootfs)
  3. 配置(config.json)

其中,rootfs和config.json称为bundle,docker的bundle有一个专门为此开放的规范称之为OCI: (Open Container Initiative Runtime Specification)。

/ˈbʌnd(ə)l/

简单来说,docker的bundle包含了完整的根目录及docker运行时的运行时库,文件,程序等等。

其中的config.json配置描述docker运行起来要执行的应用程序路径,执行的终端信息,还包括了各个操作系统平台下的一些操作细节,比如: 在Linux环境下需要开启哪些命名空间,文件的挂载点和cgoups资源限制等等。

2、runc源码分析

接下来的分析将runc的执行过程分为三部分:

  1. runc create
  2. runc init
  3. runc start

其中,runc createrunc init做准备工作,runc init 很少被直接使用,而是被 runc create 隐式地调用,runc init是本文要分析的重点。

另外,runc run其实是将三部走变成一步走。

3、runc create 

从createCommand开始,经过第一轮处理,runc create 会以runc init回到initCommand。过程比较清晰:

  1. 第1步中: setupSepc(context)加载bundle目录下的config.json配置。
  2. 在第5,6步,构造一个工厂函数,这个LinuxFactory的可执行文件设置为/proc/self/exe,其实就是runc自己,它的参数是init,也即,在后续过程中的某个地方执行runc init
  3. 在第7步之前,构造runner对象。

在 create 阶段,runner.init为true。然后调用run()。

如何修改dockerimage 如何修改docker的runc_运维

run()函数

  1. 根据spec里面process的配置信息调用newProcess创建process对象。
  2. 调用container.Start(process)来启动process进程。
func (r *runner) run(config *specs.Process) (int, error) {

    process, err := newProcess(*config)

    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")
	}
}

linuxContainer.Start()函数

  1. 首先调用newParentProcess来创建init的parent进程。
  2. 调用parent.start()异步启动parent进程。
  3. 根据parent进程的状态更新容器的状态为Created。
  4. 遍历spec里面的Poststart hook,分别调用。
func (c *Container) start(process *Process) (retErr error) {
	parent, err := c.newParentProcess(process)


	if err := parent.start(); err != nil {
		return fmt.Errorf("unable to start container process: %w", err)
	}
    
    rr := c.config.Hooks[configs.Poststart].RunHooks(s);

	return nil
}

newParentProcess函数

  1. 创建一个initProcess,里面既有init进程的信息,也有spec里面指定的process的信息。
  2. 创建一对pipe——parentPipe和childPipe,打开rootDir。
  3. 创建一个command,命令为runc init自身(通过/proc/self/exe软链接实现);标准io为当前进程的;工作目录为Rootfs;用ExtraFiles在新进程中保持打开childPipe和rootDir,并添加对应的环境变量。
  4. 调用newInitProcess进一步将parent process和command封装为initProcess。主要工作为添加初始化类型环境变量,将namespace、uid/gid映射等配置信息用bootstrapData封装为一个io.Reader等。

4、initProcess.start()

如何修改dockerimage 如何修改docker的runc_docker_02

说明:

  1. p.cmd.Start()执行后,会创建新进程调用nsexec()。
  2. nsexec()从JUMP_INIT分支return后,C函数部分执行完成,程序会执行到go部分的newContainerInit()。这部分在后文有详细介绍。
  3. 黑色虚线部分是数据发送和接收。例如: io.Copy()将p.bootstrapData发送,被nsexec()中nl_parse()接收处理。
  4. 红色虚线是PARENT和CHILD,PARENT和INIT进程间通信。具体后文有介绍。
     

exec子进程

为了更好的理解nsexec(),通过一张图将各个进程的关键点标出:

  • runc中实现切换新的namespace都是使用unshare()。
  • namespace切换发生在runc:[1:CHILD]。CLONE_NEWUSER首先需要单独做一次unshare()。CLONE_NEWUSER不能与CLONE_PARENT同时指定。具体细节读者可见:man 2 clone
  • runc:[1:CHILD]执行完unshare(cloneflags&~CLONE_NEWGROUP),主要的namespaces已经改变了,但CLONE_NEWPID这个namespace并未发生切换,需要再次clone()。
  • 一旦clone_parent(JUMP_INIT)开始执行,runc:[2:INIT]进程产生,pid namespae切换。runc:[1:CHILD]进程一只脚在容器内,一只脚在容器外;而runc:[2:INIT]是完全进入到容器的进程。
  • runc:[1:CHILD],runc:[0:PARENT] 相继退出,runc:[2:INIT]中执行完nsexec()函数后,后续流程开始进入runc init剩下的go runtime部分。
     

如何修改dockerimage 如何修改docker的runc_容器_03

参考文档:

runc源码分析——create和start

mount 内核源码_runc源码分析