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

相關文章
相關標籤/搜索