Docker RunC init 执行流程之namespace建立源码深刻剖析-Docker商业环境实战

专一于大数据及容器云核心技术解密,可提供全栈的大数据+云原平生台咨询方案,请持续关注本套博客。若有任何学术交流,可随时联系。更多内容请关注《数据云技术社区》公众号。 linux

1 RunC create 启动流程(2:建立容器 3:运行容器)

1.1 核心流程

create.go:
 setupSpec(context)
 utils_linux.go:
     startContainer(context, spec, CT_ACT_CREATE, nil) 
       |- createContainer
          |- specconv.CreateLibcontainerConfig
          |- loadFactory(context)
             |- libcontainer.New(......)
          |- factory.Create(id, config)
复制代码

1.2 startContainer总驱动

  • create命令的响应入口在 create.go
使用 create 命令建立容器
sudo runc create mybusybox
复制代码

  • setupSpec:从命令行输入中找到-b 指定的 OCI bundle 目录,若没有此参数,则默认是当前目录。读取config.json文件,将其中的内容转换为Go的数据结构specs.Spec,该结构定义在文件 github.com/opencontainers/runtime-spec/specs-go/config.go,里面的内容都是OCI标准描述的。

1.3 总驱动startContainer-(建立容器并运行:startContainer->createContainer|runner.run)

  • startContainer:尝试建立启动容器,注意这里的第三个参数是 CT_ACT_CREATE, 表示仅建立容器。本文使用linux平台,所以实际调用的是 utils_linux.go 中的startContainer()。startContainer()根据用户将用户输入的 id 和刚才的获得的 spec 做为输入,调用 createContainer() 方法建立容器,再经过一个runner.run()方法启动它。

2 建立容器

  • 在runC中,Container用来表示一个容器对象,它是一个抽象接口,它内部包含了BaseContainer接口。从其内部的方法的名字就能够看出,都是管理容器的基本操做。

2.1 loadFactory架构

  • 对于linux平台,Factory 建立 Container 实际上就是 LinuxFactory 建立 linuxContainer
/* utils/utils_linux.go */
func loadFactory(context *cli.Context) (libcontainer.Factory, error) {
    .....
    return libcontainer.New(abs, cgroupManager, intelRdtManager,
        libcontainer.CriuPath(context.GlobalString("criu")),
        libcontainer.NewuidmapPath(newuidmap),
        libcontainer.NewgidmapPath(newgidmap))
}
复制代码

2.2 runc init核心(开启新进程:/proc/self/exe init)

  • 为了建立新namespace,注意这里有拦截操做
/* libcontainer/factory_linux.go */
func New(root string, options ...func(*LinuxFactory) error) (Factory, error) {
    .....
    l := &LinuxFactory{
        .....
        InitPath:  "/proc/self/exe",
        InitArgs:  []string{os.Args[0], "init"},
    }
    ......
    return l, nil
}
复制代码

2.3 factory.Create 建立容器

func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, error) {
    ....
    c := &linuxContainer{
        id:            id,    
        config:        config,
        initPath:      l.InitPath,
        initArgs:      l.InitArgs,
    }
     .....
    return c, nil

}
复制代码
  • 将配置存放到 config, 数据类型是 Config.config
  • 加载 Factory,实际返回 LinuxFactory
  • 调用 Factory 的Create()方法

3 运行容器

3.1 核心流程

|- runner.run(spec.Process)
      |- newProcess(*config, r.init) 
      |- r.container.Start(process)
         |- c.createExecFifo()
         |- c.start(process)
            |- c.newParentProcess(process)
            |- parent.start()
复制代码

3.2 运行容器

  • 调用 newProcess() 方法, 用 spec.Process 建立 libcontainer.Process,注意第二个参数是 true ,表示新建立的 process 会做为新建立容器的第一个 process。
  • 根据 r.action 的值决定如何操做获得的 libcontainer.Process
/* libcontainer/factory_linux.go */
func New(root string, options ...func(*LinuxFactory) error) (Factory, error) {
    .....
    l := &LinuxFactory{
        .....
        InitPath:  "/proc/self/exe",
        InitArgs:  []string{os.Args[0], "init"},
    }
    ......
    return l, nil
}

/* libcontainer/factory_linux.go */
func New(root string, options ...func(*LinuxFactory) error) (Factory, error) {
    .....
    l := &LinuxFactory{
        .....
        InitPath:  "/proc/self/exe",
        InitArgs:  []string{os.Args[0], "init"},
    }
    ......
    return l, nil
}

func (r *runner) run(config *specs.Process) (int, error) { 
    ......
    process, err := newProcess(*config, r.init)                                  /*  第1部分 */
    ......
    switch r.action {
    case CT_ACT_CREATE:
        err = r.container.Start(process)   /* runc start */                      /*  第2部分 */
    case CT_ACT_RESTORE:
        err = r.container.Restore(process, r.criuOpts) /* runc restore */
    case CT_ACT_RUN:
        err = r.container.Run(process)     /* runc run */
    default:
        panic("Unknown action")
    }
    ......
    return status, err
}
复制代码

3.3 container.Start

func (c *linuxContainer) Start(process *Process) error {

    if process.Init {
        if err := c.createExecFifo(); err != nil {  /* 1.建立fifo   */
            return err
        }
    }
    if err := c.start(process); err != nil {        /* 2. 调用start() */
        if process.Init {
            c.deleteExecFifo()
        }
        return err
    }
    return nil
}
复制代码

3.4 parent.start()

func (c *linuxContainer) start(process *Process) error {
    parent, err := c.newParentProcess(process) /*  1. 建立parentProcess */

    err := parent.start();                     /*  2. 启动这个parentProcess */
    ......


func (c *linuxContainer) newParentProcess(p *Process) (parentProcess, error) {
    parentPipe, childPipe, err := utils.NewSockPair("init")  /* 1.建立 Socket Pair */

    cmd, err := c.commandTemplate(p, childPipe)              /* 2. 建立 *exec.Cmd */

    if !p.Init {
        return c.newSetnsProcess(p, cmd, parentPipe, childPipe) 
    }

    if err := c.includeExecFifo(cmd); err != nil {           /* 3.打开以前建立的fifo */
        return nil, newSystemErrorWithCause(err, "including execfifo in cmd.Exec setup")
    }
    return c.newInitProcess(p, cmd, parentPipe, childPipe)   /* 4.建立 initProcess */
}

- includeExecFifo() 方法打开以前建立的 fifo,也将其 fd 放到 cmd.ExtraFiles 中,同时将_LIBCONTAINER_FIFOFD=%d记录到 cmd.Env。
- 建立 InitProcess 了,这里首先将_LIBCONTAINER_INITTYPE="standard"加入cmd.Env,而后从 configs 读取须要新的容器建立的 Namespace 的类型,并将其打包到变量 data 中备用,最后再建立 InitProcess 本身,能够看到,这里将以前的一些资源和变量都联系了起来

func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, parentPipe, childPipe *os.File) (*initProcess, error) {
    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]
    data, err := c.bootstrapData(c.config.Namespaces.CloneFlags(), nsMaps)
    if err != nil {
        return nil, err
    }
    return &initProcess{
        cmd:             cmd,
        childPipe:       childPipe,
        parentPipe:      parentPipe,
        manager:         c.cgroupManager,
        intelRdtManager: c.intelRdtManager,
        config:          c.newInitConfig(p),
        container:       c,
        process:         p,          /*  sleep 5 在这里 */
        bootstrapData:   data,
        sharePidns:      sharePidns,
    }, nil
}
复制代码

3.5 initProcess.start()

  • p.cmd.Start() 启动 cmd 中设置的要执行的可执行文件 /proc/self/exe,参数是 init,这个函数会启动一个新的进程去执行该命令,而且不会阻塞。
  • io.Copy 将 p.bootstrapData 中的数据经过 p.parentPipe 发送给子进程
/* libcontainer/process_linux.go */
func (p *initProcess) start() error {
     
    p.cmd.Start()                 
    p.process.ops = p    
    io.Copy(p.parentPipe, p.bootstrapData)

    .....
}

/proc/self/exe 正是runc程序本身,因此这里至关因而执行runc init,也就是说,
咱们输入的是runc create命令,隐含着又去建立了一个新的子进程去执行runc init。
为何要额外从新建立一个进程呢?缘由是咱们建立的容器极可能须要运行
在一些独立的 namespace 中,好比 user namespace,这是经过 setns() 系统调用完成的,
复制代码

4 拦截(CGO)

  • 先执行C代码nsenter模块(nsexec)--->在runc create namespace 中设置 clone 了三个进程parent、child、init)
  • 后执行go代码(init.go)--->初始化其它部分(网络、rootfs、路由、主机名、console、安全等)

4.1 容器启动(聚焦p.cmd.Start)

  • 先执行 nsenter C代码部分,实现对container的process进行Namespace相关设置如uid/gid、pid、uts、ns、cgroup等。
libcontainer/process_linux.go:282

func (p *initProcess) start() error {
  //  当前执行空间进程称为bootstrap进程
  //  启动了 cmd,即启动了 runc init 命令,建立 runc init 子进程 
  //  同时也激活了C代码nsenter模块的执行(在runc create namespace 中设置 clone 了三个进程parent、child、init))
 
  //  C 代码执行后返回 go 代码部分,最后的 init 子进程为了好区分此处命名为" nsInit "(即配置了Namespace的init)
  //  后执行go代码(init.go)--->初始化其它部分(网络、rootfs、路由、主机名、console、安全等)

    err := p.cmd.Start()   // +runc init 命令执行,Namespace应用代码执行空间时机
  //...
      if p.bootstrapData != nil {
     // 将 bootstrapData 写入到 parent pipe 中,此时 runc init 能够从 child pipe 里读取到这个数据
        if _, err := io.Copy(p.messageSockPair.parent, p.bootstrapData); err != nil {
            return newSystemErrorWithCause(err, "copying bootstrap data to pipe")
        }
    }
  //...
}
复制代码

4.2 拦截(先执行C代码,再执行runc init)

  • GO语言调用C代码的作法,叫作preamble,也就是说只要import这个nsenter模块就会在GO的runtime启动前先执行这个先导代码块,最终会执行nsexec这段亲切的C代码。
  • 而nsenter包中开头经过 cgo 嵌入了一段 C 代码, 调用 nsexec()
package nsenter
/*
/* nsenter.go */
#cgo CFLAGS: -Wall
extern void nsexec();
void __attribute__((constructor)) init(void) {
    nsexec();
}
*/
import "C"

void nsexec(void)
{
	/*
	 * If we don't have an init pipe, just return to the go routine. * We'll only get an init pipe for start or exec.
	 */
	pipenum = initpipe();
	if (pipenum == -1)
		return;

	/* Parse all of the netlink configuration. */
	nl_parse(pipenum, &config);

	update_oom_score_adj(config.oom_score_adj, config.oom_score_adj_len);
    ....
}
复制代码
  • p.cmd.start就是fork子进程执行cmd里的参数,以前的部分我也两次提到了这个cmd的设置很是重要,下面就来具体看看 exec.Command(c.initArgs[0], c.initArgs[1:]...) 其实就是exec.Command("/proc/self/exe", "init"),也就是fork一个子进程执行‘runc init’的动做。
init.go
import (
    "os"
    "runtime"

    "github.com/opencontainers/runc/libcontainer"
    _ "github.com/opencontainers/runc/libcontainer/nsenter"
    "github.com/urfave/cli"
)

factory, _ := libcontainer.New("")
if err := factory.StartInitialization(); err != nil {
}
复制代码

4.3 进程通讯

  • 上面这段 C 代码中,initpipe() 从环境中读取父进程以前设置的pipe(_LIBCONTAINER_INITPIPE记录的的文件描述符),而后调用 nl_parse 从这个管道中读取配置到变量 config ,那么谁会往这个管道写配置呢 ? 固然就是runc create父进程了。父进程经过这个pipe,将新建容器的配置发给子进程,

  • 发送的具体数据在 linuxContainer 的 bootstrapData() 函数中封装成netlink msg格式的消息。忽略大部分配置,要建立哪些类型的namespace,这些都是源自最初的config.json文件。

4.4 子进程孙进程

  • 子进程就从父进程处获得了namespace的配置,继续往下, nsexec() 又建立了两个socketpair,从注释中了解到,这是为了和它本身的子进程和孙进程进行通讯。
void nsexec(void)
{
   .....
    /* Pipe so we can tell the child when we've finished setting up. */ if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_child_pipe) < 0) // sync_child_pipe is an out parameter bail("failed to setup sync pipe between parent and child"); /* * We need a new socketpair to sync with grandchild so we don't have
     * race condition with child.
     */
    if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_grandchild_pipe) < 0)
        bail("failed to setup sync pipe between parent and grandchild");
   
}
复制代码
  • switch case 编写的状态机,大致结构以下,当前进程经过clone()系统调用建立子进程,子进程又经过clone()系统调用建立孙进程,而实际的建立/加入namespace是在子进程完成的
namespaces在runc init 2完成建立
runc init 1和runc init 2最终都会执行exit(0),

但runc init 3不会,它会继续执行runc init命令的后半部分。
所以最终只会剩下runc create进程和runc init 3进程

switch (setjmp(env)) {
  case JUMP_PARENT:{
           .....
           clone_parent(&env, JUMP_CHILD);
           .....
       }
  case JUMP_CHILD:{
           ......
           if (config.namespaces)
                join_namespaces(config.namespaces);
           clone_parent(&env, JUMP_INIT);
           ......
       }
  case JUMP_INIT:{
       }
复制代码

详情参考:https://segmentfault.com/a/1190000017576314
复制代码

4.5 runc create进程和runc init 3进程通信

  • namespaces在runc init 2完成建立,runc init 1和runc init 2最终都会执行exit(0),但runc init 3不会,它会继续执行runc init命令的后半部分。所以最终只会剩下runc create进程和runc init 3进程。

4.6 newContainerInit

  • newContainerInit() 函数首先尝试从 pipe 读取配置存放到变量 config 中,再存储到变量 linuxStandardInit 中返回
func (p *initProcess) start() error {

    ...... 
    p.execSetns()
    
    fds, err := getPipeFds(p.pid())
    p.setExternalDescriptors(fds)
    p.createNetworkInterfaces()
    
    p.sendConfig()
    
    parseSync(p.parentPipe, func(sync *syncT) error {
        switch sync.Type {
        case procReady:
            .....
            writeSync(p.parentPipe, procRun);
            sentRun = true
        case procHooks:
            .....
            // Sync with child.
            err := writeSync(p.parentPipe, procResume); 
            sentResume = true
        }

        return nil
    })
    ......

/* libcontainer/init_linux.go */
func newContainerInit(t initType, pipe *os.File, consoleSocket *os.File, fifoFd int) (initer, error) {
    var config *initConfig

    /* read config from pipe (from runc process) */
    son.NewDecoder(pipe).Decode(&config); 
    populateProcessEnvironment(config.Env);
    switch t {
    ......
    case initStandard:
        return &linuxStandardInit{
            pipe:          pipe,
            consoleSocket: consoleSocket,
            parentPid:     unix.Getppid(),
            config:        config, // <=== config
            fifoFd:        fifoFd,
        }, nil
    }
    return nil, fmt.Errorf("unknown init type %q", t)
}

   runc create                    runc init 3
       |                               |
  p.sendConfig() --- config -->  NewContainerInit()
复制代码
  • 回到 StartInitialization(),在获得 linuxStandardInit 后,便调用其 Init()方法了,也即初始的sleep 方法。
/* init.go */
func (l *LinuxFactory) StartInitialization() (err error) {
    ......
    i, err := newContainerInit(it, pipe, consoleSocket, fifofd)

    return i.Init()  
}

func (l *linuxStandardInit) Init() error {
   ......
   name, err := exec.LookPath(l.config.Args[0])

   syscall.Exec(name, l.config.Args[0:], os.Environ())
}
复制代码

4.7 开始执行用户最初设置程序

func (l *linuxStandardInit) Init() error {
   ......
   name, err := exec.LookPath(l.config.Args[0])

   syscall.Exec(name, l.config.Args[0:], os.Environ())
}
复制代码

5 总结

namespace建立源码比较深奥,再次总结于此,留记!!!git

专一于大数据及容器云核心技术解密,可提供全栈的大数据+云原平生台咨询方案,请持续关注本套博客。若有任何学术交流,可随时联系。更多内容请关注《数据云技术社区》公众号。 github

相关文章
相关标签/搜索