docker 源碼分析 六(基於1.8.2版本),Docker run啓動過程

上一篇大體瞭解了docker 容器的建立過程,其實主要仍是從文件系統的視角分析了建立一個容器時須要得創建 RootFS,創建volumes等步驟;本章來分析一下創建好一個容器後,將這個容器運行起來的過程,linux

本章主要分析一下 docker deamon端的實現方法;根據前面幾章的介紹能夠容易找到,客戶端的實現代碼在api/client/run.go中,大致步驟是首先經過上一篇文章中的createContainer()方法創建一個container,而後經過調用cli.call("POST", "/containers/"+createResponse.ID+"/start", nil, nil)來實現將這個container啓動;在api/server/server.go中,客戶端請求對應的mapping爲 "/containers/{name:.*}/start":   s.postContainersStart,實現方法postContainerStart在api/server/container.go文件中,代碼以下:git

func (s *Server) postContainersStart(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {github

    if vars == nil {sql

        return fmt.Errorf("Missing parameter")docker

    }數據庫

    var hostConfig *runconfig.HostConfigjson

    if r.Body != nil && (r.ContentLength > 0 || r.ContentLength == -1) {windows

        if err := checkForJSON(r); err != nil {api

            return err數組

        }

 

        c, err := runconfig.DecodeHostConfig(r.Body)

        if err != nil {

            return err

        }

        hostConfig = c

    }

    if err := s.daemon.ContainerStart(vars["name"], hostConfig); err != nil {

        if err.Error() == "Container already started" {

            w.WriteHeader(http.StatusNotModified)

            return nil

        }

        return err

    }

    w.WriteHeader(http.StatusNoContent)

    return nil

}

邏輯很是簡單,首先從request中解析參數,而後調用s.daemon.ContainerStart(vars["name"],hostConfig)啓動容器,最後將結果寫回response;主要的實現部分在s.daemon.ContainerStart(vars["name"],hostConfig)之中。在daemon/start.go中;

func (daemon *Daemon) ContainerStart(name string, hostConfig *runconfig.HostConfig) error {

    container, err := daemon.Get(name)

    if err != nil {

        return err

    } 

    if container.IsPaused() {

        return fmt.Errorf("Cannot start a paused container, try unpause instead.")

    }

    if container.IsRunning() {

        return fmt.Errorf("Container already started")

    }

    // Windows does not have the backwards compatibility issue here.

    if runtime.GOOS != "windows" {

        // This is kept for backward compatibility - hostconfig should be passed when

        // creating a container, not during start.

        if hostConfig != nil {

            if err := daemon.setHostConfig(container, hostConfig); err != nil {

                return err

            }

        }

    } else {

        if hostConfig != nil {

            return fmt.Errorf("Supplying a hostconfig on start is not supported. It should be supplied on create")

        }

    }

 

    // check if hostConfig is in line with the current system settings.

    // It may happen cgroups are umounted or the like.

    if _, err = daemon.verifyContainerSettings(container.hostConfig, nil); err != nil {

        return err

    }

 

    if err := container.Start(); err != nil {

        return fmt.Errorf("Cannot start container %s: %s", name, err)

    }

    return nil

}

首先根據傳進來的名字,經過deamon.Get() (daemon/daemon.go)

func (daemon *Daemon) Get(prefixOrName string) (*Container, error) {

    if containerByID := daemon.containers.Get(prefixOrName); containerByID != nil {

        // prefix is an exact match to a full container ID

        return containerByID, nil

    }

 

    // GetByName will match only an exact name provided; we ignore errors

    if containerByName, _ := daemon.GetByName(prefixOrName); containerByName != nil {

        // prefix is an exact match to a full container Name

        return containerByName, nil

    }

 

    containerId, indexError := daemon.idIndex.Get(prefixOrName)

    if indexError != nil {

        return nil, indexError

    }

    return daemon.containers.Get(containerId), nil

}

首先從daemon.containers中根據name來進行查找,找出container是否已經存在了。daemon.container是contStore類型的結構體,其結構以下:

type contStore struct {

    s map[string]*Container

    sync.Mutex

}

接着經過GetByName查找:GetByName一樣在daemon/daemon.go中,代碼以下:

func (daemon *Daemon) GetByName(name string) (*Container, error) {

    fullName, err := GetFullContainerName(name)

    if err != nil {

        return nil, err

    }

    entity := daemon.containerGraph.Get(fullName)

    if entity == nil {

        return nil, fmt.Errorf("Could not find entity for %s", name)

    }

    e := daemon.containers.Get(entity.ID())

    if e == nil {

        return nil, fmt.Errorf("Could not find container for entity id %s", entity.ID())

    }

    return e, nil

}

daemon.containerGraph是graphdb.Database類型(pkg/graphdb/graphdb.go文件中), 

type Database struct {

    conn *sql.DB

    mux  sync.RWMutex

}

Database是一個存儲容器和容器之間關係的數據庫;目前Database是一個sqlite3數據庫,所在的路徑是/var/lib/docker/link/linkgraph.db中,其是在NewDaemon的實例化過程當中,傳遞進來的。

graphdbPath := filepath.Join(config.Root, "linkgraph.db")

graph, err := graphdb.NewSqliteConn(graphdbPath)

if err != nil {

     return nil, err

}

d.containerGraph = graph

數據庫中最主要有兩個表,分別是Entity,Edge,每個鏡像對應一個實體,存在Entity表;每一個鏡像與其父鏡像的關係存在Edge表。每個表在代碼中也對應着一個結構體:

// Entity with a unique id.

type Entity struct {

    id string

}

 

// An Edge connects two entities together.

type Edge struct {

    EntityID string

    Name     string

    ParentID string

}

經過建表語句也許更能直觀一些:

    createEntityTable = `

    CREATE TABLE IF NOT EXISTS entity (

        id text NOT NULL PRIMARY KEY

    );`

    createEdgeTable = `

    CREATE TABLE IF NOT EXISTS edge (

        "entity_id" text NOT NULL,

        "parent_id" text NULL,

        "name" text NOT NULL,

        CONSTRAINT "parent_fk" FOREIGN KEY ("parent_id") REFERENCES "entity" ("id"),

        CONSTRAINT "entity_fk" FOREIGN KEY ("entity_id") REFERENCES "entity" ("id")

        );

    `

最後一步就是經過GetByName查找完以後,接着根據daemon.idIndex.Get()進行查找,idIndex和前一篇中的鏡像的idIndex是同樣的,是一個trie的結構;

回到ContainerStart() 函數,在獲取了container以後,接着判斷container是不是中止和正在運行的,若是都不是, 在進行一些參數驗證(端口映射的設置、驗證exec driver、驗證內核是否支持cpu share,IO weight等)後,則啓動調用container.Start() (daemon/container.go)啓動container;

func (container *Container) Start() (err error) {

    container.Lock()

    defer container.Unlock()

    if container.Running {

        return nil

    }

    if container.removalInProgress || container.Dead {

        return fmt.Errorf("Container is marked for removal and cannot be started.")

    }

    // if we encounter an error during start we need to ensure that any other

    // setup has been cleaned up properly

    defer func() {

        if err != nil {

            container.setError(err)

            // if no one else has set it, make sure we don't leave it at zero

            if container.ExitCode == 0 {

                container.ExitCode = 128

            }

            container.toDisk()

            container.cleanup()

            container.LogEvent("die")

        }

    }()

    if err := container.Mount(); err != nil {

        return err

    } 

    // Make sure NetworkMode has an acceptable value. We do this to ensure

    // backwards API compatibility.

    container.hostConfig = runconfig.SetDefaultNetModeIfBlank(container.hostConfig)

    if err := container.initializeNetworking(); err != nil {

        return err

    }

    linkedEnv, err := container.setupLinkedContainers()

    if err != nil {

        return err

    }

    if err := container.setupWorkingDirectory(); err != nil {

        return err

    }

    env := container.createDaemonEnvironment(linkedEnv)

    if err := populateCommand(container, env); err != nil {

        return err

    }

    mounts, err := container.setupMounts()

    if err != nil {

        return err

    }

    container.command.Mounts = mounts

    return container.waitForStart()
}

defer func() 裏面的做用就是若是start container出問題的話,進行一些清理工做; 

container.Mount() 掛在container的aufs文件系統;

initializeNetworking() 對網絡進行初始化,docker網絡模式有三種,分別是 bridge模式(每一個容器用戶單獨的網絡棧),host模式(與宿主機共用一個網絡棧),contaier模式(與其餘容器共用一個網絡棧,猜想kubernate中的pod所用的模式);根據config和hostConfig中的參數來肯定容器的網絡模式,而後調動libnetwork包來創建網絡,關於docker網絡的部分後面會單獨拿出一章出來梳理;

container.setupLinkedContainers() 將經過--link相連的容器中的信息獲取過來,而後將其中的信息轉成環境變量(是[]string數組的形式,每個元素相似於"NAME=xxxx")的形式

返回; 

setupWorkingDirectory() 創建容器執行命令時的工做目錄;

createDaemonEnvironment() 將container中的自有的一些環境變量和以前的linkedEnv和合在一塊兒(append),而後返回;

populateCommand(container, env) 主要是爲container的execdriver(最終啓動容器的) 設置網絡模式、設置namespace(pid,ipc,uts)等、資源(resources)限制等,而且設置在容器內執行的Command,Command中含有容器內進程的啓動命令;

container.setupMounts() 返回container的全部掛載點;

最後調用container.waitForStart()函數啓動容器;

 

func (container *Container) waitForStart() error {

    container.monitor = newContainerMonitor(container, container.hostConfig.RestartPolicy)

 

    // block until we either receive an error from the initial start of the container's

    // process or until the process is running in the container

    select {

    case <-container.monitor.startSignal:

    case err := <-promise.Go(container.monitor.Start):

        return err

    }

    return nil

}

首先實例化出來一個containerMonitor,monitor的做用主要是監控容器內第一個進程的執行,若是執行沒有成功,那麼monitor能夠按照必定的重啓策略(startPolicy)來進行重啓; 

看下一下montitor(daemon/monitor.go)中的Start()函數,最主要的部分是

m.container.daemon.Run(m.container, pipes, m.callback)

在daemon/daemon.go文件中, Run方法:

func (daemon *Daemon) Run(c *Container, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (execdriver.ExitStatus, error) {
     return daemon.execDriver.Run(c.command, pipes, startCallback)
}

docker的execDriver有兩個:lxc 和 native;lxc是較早的driver,native是默認的,用的是libcontainer;因此最終這個Run的方式是調用daemon/execdriver/native/driver.go中的Run() 方法:

func (d *Driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (execdriver.  ExitStatus, error) {

    // take the Command and populate the libcontainer.Config from it

    container, err := d.createContainer(c)

    if err != nil {

        return execdriver.ExitStatus{ExitCode: -1}, err

    }

 

    p := &libcontainer.Process{

        Args: append([]string{c.ProcessConfig.Entrypoint}, c.ProcessConfig.Arguments...),

        Env:  c.ProcessConfig.Env,

        Cwd:  c.WorkingDir,

        User: c.ProcessConfig.User,

    }

 

    if err := setupPipes(container, &c.ProcessConfig, p, pipes); err != nil {

        return execdriver.ExitStatus{ExitCode: -1}, err

    }

 

    cont, err := d.factory.Create(c.ID, container)

    if err != nil {

        return execdriver.ExitStatus{ExitCode: -1}, err

    }

    d.Lock()

    d.activeContainers[c.ID] = cont

    d.Unlock()

    defer func() {

        cont.Destroy()

        d.cleanContainer(c.ID)

    }()

 

    if err := cont.Start(p); err != nil {

        return execdriver.ExitStatus{ExitCode: -1}, err

    }

 

    if startCallback != nil {

        pid, err := p.Pid()

        if err != nil {

            p.Signal(os.Kill)

            p.Wait()

            return execdriver.ExitStatus{ExitCode: -1}, err

        }

        startCallback(&c.ProcessConfig, pid)

    }

 

    oom := notifyOnOOM(cont)

    waitF := p.Wait

    if nss := cont.Config().Namespaces; !nss.Contains(configs.NEWPID) {

        // we need such hack for tracking processes with inherited fds,

        // because cmd.Wait() waiting for all streams to be copied

        waitF = waitInPIDHost(p, cont)

    }

    ps, err := waitF()

    if err != nil {

        execErr, ok := err.(*exec.ExitError)

        if !ok {

             return execdriver.ExitStatus{ExitCode: -1}, err

        }

        ps = execErr.ProcessState

    }

    cont.Destroy()

    _, oomKill := <-oom

    return execdriver.ExitStatus{ExitCode: utils.ExitStatus(ps.Sys().(syscall.WaitStatus)), OOMKilled: oomKill}, nil

}

d.createContainer(c) 根據command實例化出來一個container須要的配置;Capabilities、Namespace、Group、mountpoints等,首先根據模板生成固定的配置(daemon/execdriver/native/template/default_template.go),而後在根據command創建容器特定的namespace

接着實例化一個libcontainer.Process{},裏面的Args參數就是用戶輸入的entrypoint和cmd參數的組合,這也是未來容器的第一個進程(initProcess)要運行的一部分;

setupPipes(container, &c.ProcessConfig, p, pipes); 將container類(pipes)的標準輸入輸出與 libcontainer.Process (也是未來容器中的的init processs,就是變量p)進行綁定,這樣就能夠獲取初始進程的輸入和輸出;

cont, err := d.factory.Create(c.ID, container)  調用driver.factory(~/docker_src/vendor/src/github.com/opencontainers/runc/libcontainer/factory_linux.go )來實例化一個linux container,結構以下:

 linuxContainer{

        id:            id,

        root:          containerRoot,

        config:        config,

        initPath:      l.InitPath,

        initArgs:      l.InitArgs,

        criuPath:      l.CriuPath,

        cgroupManager: l.NewCgroupsManager(config.Cgroups, nil),

 }

這個linuxContainer類和以前的container類是不一樣的,這個是execdriver專有的類,其中比較主要的,ID就是containerID,initPath:是dockerinit的路徑,initArgs是docker init的參數,而後是CriuPath(用於給容器作checkpoint),cgroupMangeer:管理容器的進程所在的資源; 

dockerinit要說一下,dockerinit是一個固定的二進制文件,是一個容器運行起來以後去執行的第一個可執行文件,dockerinit的做用是在新的namespace中設置掛在資源,初始化網絡棧等等,固然還有一做用是由dockerinit來負責執行用戶設定的entrypoint和cmd;執行entrypoint和cmd,執行entrypoint和cmd的時候,與dockerinit是在同一個進程中;

cont.Start(p); 經過linuxcontainer運行以前的libcontainer.Process,這個步驟稍後會詳細講解;

接下來就是常規的步驟了,調用callback函數、監控container是否會有內存溢出的問題(經過cgroupmanager)、而後p.Wait()等待libcontainer.Process執行完畢、無誤執行完畢後接着調用destroy銷燬linuxcontainer,而後返回執行狀態;

接下來對linuxcontainer的start(vendor/src/github.com/opencontainers/runc/libcontainer/container_linux.go)過程詳細介紹一下;

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

    c.m.Lock()

    defer c.m.Unlock()

    status, err := c.currentStatus()

    if err != nil {

        return err

    }

    doInit := status == Destroyed

    parent, err := c.newParentProcess(process, doInit)

    if err != nil {

        return newSystemError(err)

    }

    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 newSystemError(err)

    }

    process.ops = parent

    if doInit {

        c.updateState(parent)

    }

    return nil

}

這個Start()函數的做用就是開啓容器的第一個進程initProcess,docker daemon開啓一個新的容器,其實就是fork出一個新的進程(這個進程有本身的namespace,從而實現容器間的隔離),這個進程同時也是容器的初始進程,這個初始進程用來執行dockerinit、entrypoint、cmd等一系列操做;

status, err := c.currentStatus() 首先判斷一下容器的初始進程是否已經存在,不存在的話會返回destroyd狀態;

parent, err := c.newParentProcess(process, doInit)  開啓新的進程,下面插進來一下關於newParentProcess的代碼

func (c *linuxContainer) newParentProcess(p *Process, doInit bool) (parentProcess, error) {

    parentPipe, childPipe, err := newPipe()

    if err != nil {

        return nil, newSystemError(err)

    }

    cmd, err := c.commandTemplate(p, childPipe)

    if err != nil {

        return nil, newSystemError(err)

    }

    if !doInit {

        return c.newSetnsProcess(p, cmd, parentPipe, childPipe), nil

    }

    return c.newInitProcess(p, cmd, parentPipe, childPipe)

}

 

func (c *linuxContainer) commandTemplate(p *Process, childPipe *os.File) (*exec.Cmd, error) {

    cmd := &exec.Cmd{

        Path: c.initPath,

        Args: c.initArgs,

    }

    cmd.Stdin = p.Stdin

    cmd.Stdout = p.Stdout

    cmd.Stderr = p.Stderr

    cmd.Dir = c.config.Rootfs

    if cmd.SysProcAttr == nil {

        cmd.SysProcAttr = &syscall.SysProcAttr{}

    }

    cmd.ExtraFiles = append(p.ExtraFiles, childPipe)

    cmd.Env = append(cmd.Env, fmt.Sprintf("_LIBCONTAINER_INITPIPE=%d", stdioFdCount+len(cmd.ExtraFiles)-1))

    if c.config.ParentDeathSignal > 0 {

        cmd.SysProcAttr.Pdeathsig = syscall.Signal(c.config.ParentDeathSignal)

    }

    return cmd, nil

}

上面兩個函數是相互關聯的,上面的函數調用了下面的函數,

newParentProcess中首先調用了

parentPipe, childPipe, err := newPipe() 來建立一個socket pair,造成一個管道;這個管道是docker daemon 與 未來的dockerinit進行通訊的渠道, 上面說過dockerinit的做用是初始化新的namespace 內的一些重要資源,但這些資源是須要docker daemon 在宿主機上申請的,如:veth pair,docker daemon 在本身的命名空間中建立了這些內容以後,經過這個管道將數據交給 dockerinit

接着cmd, err := c.commandTemplate(p, childPipe)。這部分主要有兩個做用,將dockerinit及其參數分裝成go語言中的exec.Cmd類,

&exec.Cmd{

        Path: c.initPath,

        Args: c.initArgs,

}

這個Cmd類就是未來要真正執行的進程;其餘一些事情是綁定Cmd的表述輸入輸入到libcontainer.Process(以前已經將輸入輸出綁定到container類),還有將管道的childpipe一端綁定到Cmd類的打開的文件中。

接着在newParentProcess中,返回了 newInitProcess(p, cmd, parentPipe, childPipe),其實質是返回了一個initProcess類(vendor/src/github.com/opencontainers/runc/libcontainer/process_linux.go); 

initProcess{

        cmd:        cmd,

        childPipe:  childPipe,

        parentPipe: parentPipe,

        manager:    c.cgroupManager,

        config:     c.newInitConfig(p),

}

其中的cmd,就是以前封裝好的exec.Cmd類、而後childPipe已經綁定到了cmd的文件描述符中、parentPipe是pipe的另外一端、manager是cgroup控制資源的做用、config是將以前的libcontainer.Process的配置(其中包括entrypoint和cmd的配置)轉化成一些配置信息,這部分配置信息將經過parentPipe發給cmd的childpipe,最終由dockerinit來運行、接下來會講到;

而後回到 Start()函數中, parent就是一個initProcess類,緊接着就是調用這個類的start()方法了 

func (p *initProcess) start() error {

    defer p.parentPipe.Close()

    err := p.cmd.Start()

    p.childPipe.Close()

    if err != nil {

        return newSystemError(err)

    }

    fds, err := getPipeFds(p.pid())

    if err != nil {

        return newSystemError(err)

    }

    p.setExternalDescriptors(fds)

 

    if err := p.manager.Apply(p.pid()); err != nil {

        return newSystemError(err)

    }

    defer func() {

        if err != nil {

            // TODO: should not be the responsibility to call here

            p.manager.Destroy()

        }

    }()

    if err := p.createNetworkInterfaces(); err != nil {

        return newSystemError(err)

    }

    if err := p.sendConfig(); err != nil {

        return newSystemError(err)

    }

    // wait for the child process to fully complete and receive an error message

    // if one was encoutered

    var ierr *genericError

    if err := json.NewDecoder(p.parentPipe).Decode(&ierr); err != nil && err != io.EOF {

        return newSystemError(err)

    }

    if ierr != nil {

        return newSystemError(ierr)

    }

    return nil

}

最主要的幾個步驟,p.cmd.Start() 首先運行cmd的命令;

p.manager.Apply(p.pid()) cmd運行起來以後,是一個新的進程,也是container中的第一個進程,會有一個pid,將這個pid加入到cgroup配置中,確保之後由初始進程fork出來的子進程也能遵照cgroup的資源配置;

createNetworkInterfaces() 爲進程創建網絡配置,並放到config配置中;

p.sendConfig() 將配置(包括網絡配置、entrypoint、cmd等)經過parentPipe發給cmd進程,並有cmd中的dockerinit執行;

 json.NewDecoder(p.parentPipe).Decode(&ierr);  等待cmd的執行是否會有問題;

 

容器的啓動主要過程就是 docker 將container的主要配置封裝成一個Command類,而後交給execdriver(libcontainer),libcontainer將command中的配置生成一個libcontainer.process類和一個linuxcontainer類,而後由linux container這個類運行libcontainer.process。運行的過程是生成一個os.exec.Cmd類(裏面包含dockerinit),啓動這個dockerinit,而後在運行entrypoint和cmd;

 

年前就先分析這麼多了,接下來要看看swarm、kubernates、和docker 網絡相關的東西;

相關文章
相關標籤/搜索