前面講到了docker容器得鏡像,鏡像實際上是docker容器的靜態部分,而docker容器則是docker鏡像的動態部分,即啓動了一個進程來運行,本篇最要來分析一下怎樣建立並運行一個容器。web
建立一個容器在客戶端實現是在api/client/create.go,其中得CmdCreate()方法,這個函數的做用是經過一個給定的image來啓動一個container;其中的createContainer()函數是最主要的實現部分;sql
//create the containerdocker
serverResp, err := cli.call("POST", "/containers/create?"+containerValues.Encode(), mergedConfig, nil)數據庫
//if image not found try to pull itjson
if serverResp.statusCode == 404 && strings.Contains(err.Error(), config.Image) {api
fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", ref.ImageName(repo))安全
// we don't want to write to stdout anything apart from container.IDapp
if err = cli.pullImageCustomOut(config.Image, cli.err); err != nil {函數
return nil, errpost
}
if trustedRef != nil && !ref.HasDigest() {
repoInfo, err := registry.ParseRepositoryInfo(repo)
if err != nil {
return nil, err
}
if err := cli.tagTrusted(repoInfo, trustedRef, ref); err != nil {
return nil, err
}
}
// Retry
if serverResp, err = cli.call("POST", "/containers/create?"+containerValues.Encode(), mergedConfig, nil); err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
defer serverResp.body.Close()
var response types.ContainerCreateResponse
if err := json.NewDecoder(serverResp.body).Decode(&response); err != nil {
return nil, err
}
for _, warning := range response.Warnings {
fmt.Fprintf(cli.err, "WARNING: %s\n", warning)
}
if containerIDFile != nil {
if err = containerIDFile.Write(response.ID); err != nil {
return nil, err
}
}
return &response, nil
}
大致的實現邏輯如上,首先調用createContainer來建立一個container;docker啓動的時候是須要鏡像得,根據前一篇得內容知道,若是鏡像已經下載到本地,那麼直接從本地獲取鏡像就好,若是鏡像不在本地,那麼打印出來打印出 在本地找不到鏡像,而後調用 cli.pullImageCustomOut 去獲取須要的鏡像(其實就是上一篇講的內容);在pull完鏡像以後,接着在retry一下 createContainer函數;
建立容器的操做對應在server端得實如今api/server/container.go中的postContainersCreate()函數;
func (s *Server) postContainersCreate(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := parseForm(r); err != nil {
return err
}
if err := checkForJSON(r); err != nil {
return err
}
var (
warnings []string
name = r.Form.Get("name")
)
config, hostConfig, err := runconfig.DecodeContainerConfig(r.Body)
if err != nil {
return err
}
adjustCPUShares := version.LessThan("1.19")
container, warnings, err := s.daemon.ContainerCreate(name, config, hostConfig, adjustCPUShares)
if err != nil {
return err
}
return writeJSON(w, http.StatusCreated, &types.ContainerCreateResponse{
ID: container.ID,
Warnings: warnings,
})
}
最主要的是 s.daemon.ContainerCreate() 方法:
func (daemon *Daemon) ContainerCreate(name string, config *runconfig.Config, hostConfig *runconfig.HostConfig, adjustCPUShares bool) (*Container, []string, error) {
if config == nil {
return nil, nil, fmt.Errorf("Config cannot be empty in order to create a container")
}
warnings, err := daemon.verifyContainerSettings(hostConfig, config)
daemon.adaptContainerSettings(hostConfig, adjustCPUShares)
if err != nil {
return nil, warnings, err
}
container, buildWarnings, err := daemon.Create(config, hostConfig, name)
if err != nil {
if daemon.Graph().IsNotExist(err, config.Image) {
_, tag := parsers.ParseRepositoryTag(config.Image)
if tag == "" {
tag = tags.DefaultTag
}
return nil, warnings, fmt.Errorf("No such image: %s (tag: %s)", config.Image, tag)
}
return nil, warnings, err
}
warnings = append(warnings, buildWarnings...)
return container, warnings, nil
}
最終調用的是daemon.Create(config, hostConfig, name)方法,在同一個文件中:
func (daemon *Daemon) Create(config *runconfig.Config, hostConfig *runconfig.HostConfig, name string) (retC *Container, retS []string, retErr error) {
var (
container *Container
warnings []string
img *image.Image
imgID string
err error
)
if config.Image != "" {
img, err = daemon.repositories.LookupImage(config.Image)
if err != nil {
return nil, nil, err
}
if err = daemon.graph.CheckDepth(img); err != nil {
return nil, nil, err
}
imgID = img.ID
}
if err := daemon.mergeAndVerifyConfig(config, img); err != nil {
return nil, nil, err
}
if hostConfig == nil {
hostConfig = &runconfig.HostConfig{}
}
if hostConfig.SecurityOpt == nil {
hostConfig.SecurityOpt, err = daemon.GenerateSecurityOpt(hostConfig.IpcMode, hostConfig.PidMode)
if err != nil {
return nil, nil, err
}
}
if container, err = daemon.newContainer(name, config, imgID); err != nil {
return nil, nil, err
}
defer func() {
if retErr != nil {
if err := daemon.rm(container, false); err != nil {
logrus.Errorf("Clean up Error! Cannot destroy container %s: %v", container.ID, err)
}
}
}()
if err := daemon.Register(container); err != nil {
return nil, nil, err
}
if err := daemon.createRootfs(container); err != nil {
return nil, nil, err
}
if err := daemon.setHostConfig(container, hostConfig); err != nil {
return nil, nil, err
}
if err := container.Mount(); err != nil {
return nil, nil, err
}
defer container.Unmount()
if err := createContainerPlatformSpecificSettings(container, config, img); err != nil {
return nil, nil, err
}
if err := container.ToDisk(); err != nil {
logrus.Errorf("Error saving new container to disk: %v", err)
return nil, nil, err
}
container.LogEvent("create")
return container, warnings, nil
}
一開始是一些獲取imgID,校驗鏡像層數(鏡像得層數不能過大,有MaxImageDepth參數用來控制,默認取值127),校驗config參數等操做;
daemon.newContainer(name, config, imgID)是來實例化一個新的container;接下來的一些操做就是爲啓動一個容器所作的操做:
首先是daemon.Register(container):(daemon/daemon.go)
在daemon中註冊container,經過daemon.containers 結構,使得daemon能夠經過map[containerId]*Container的結構來記錄container;還有爲container中自帶的mountPoint創建volume;
而後是daemon.createRootfs(container)
func (daemon *Daemon) createRootfs(container *Container) error {
// Step 1: create the container directory.
// This doubles as a barrier to avoid race conditions.
if err := os.Mkdir(container.root, 0700); err != nil {
return err
}
initID := fmt.Sprintf("%s-init", container.ID)
if err := daemon.driver.Create(initID, container.ImageID); err != nil {
return err
}
initPath, err := daemon.driver.Get(initID, "")
if err != nil {
return err
}
if err := setupInitLayer(initPath); err != nil {
daemon.driver.Put(initID)
return err
}
// We want to unmount init layer before we take snapshot of it
// for the actual container.
daemon.driver.Put(initID)
if err := daemon.driver.Create(container.ID, initID); err != nil {
return err
}
return nil
}
創建container.root文件夾,路徑是/var/lib/docker/containers/containerId;
建立InitID:容器ID-init
根據InitID和ImageID(此時imageID是InitID的父層)調用aufs.go的Create函數(在上一篇中分析過),在/var/lib/docker/aufs/mnt,/var/lib/docker/aufs/layers,/var/lib/docker/aufs/diff下創建InitID爲名稱的子文件夾,而後將ImageID和ImageID的全部父親ImageID寫入到layers中;init層是docker在全部鏡像之上創建的一層:主要做用是存放一些關於容器的配置信息;
initPath, err := daemon.driver.Get(initID, "") 一樣是調用aufs.go中的Get函數;這個函數比較重要,Get函數將全部祖先鏡像的數據都掛在到/var/lib/docker/aufs/mnt/initID目錄下,全部祖先鏡像的實際數據分別在 /var/lib/docker/aufs/diff/imgID目錄下;返回的initPath就是/var/lib/docker/aufs/mnt/initID。
setupInitLayer(initPath);建立初始化層,就是建立一個容器須要的基本目錄和文件,包括的內容有:
"/dev/pts": "dir",
"/dev/shm": "dir",
"/proc": "dir",
"/sys": "dir",
"/.dockerinit": "file",
"/.dockerenv": "file",
"/etc/resolv.conf": "file",
"/etc/hosts": "file",
"/etc/hostname": "file",
"/dev/console": "file",
"/etc/mtab": "/proc/mounts"
接着 在初始層之上創建容器ID層,同時經過Driver的Put函數減小初始層的引用次數,但此時容器ID層並無進行mount操做;
daemon.driver.Put(initID)
daemon.driver.Create(container.ID, initID);
到此createRootfs操做結束;
回到Create函數接下來是setHostConfig()(daemon/daemon.go)
func (daemon *Daemon) setHostConfig(container *Container, hostConfig *runconfig.HostConfig) error {
container.Lock()
if err := parseSecurityOpt(container, hostConfig); err != nil {
container.Unlock()
return err
}
container.Unlock()
// Do not lock while creating volumes since this could be calling out to external plugins
// Don't want to block other actions, like `docker ps` because we're waiting on an external plugin
if err := daemon.registerMountPoints(container, hostConfig); err != nil {
return err
}
container.Lock()
defer container.Unlock()
// Register any links from the host config before starting the container
if err := daemon.RegisterLinks(container, hostConfig); err != nil {
return err
}
container.hostConfig = hostConfig
container.toDisk()
return nil
}
parseSecurityOpt(container, hostConfig); 關於安全的一些參數得解析;
daemon.registerMountPoints(container, hostConfig); 註冊全部掛載到容器的數據卷,主要有三種方式和來源:
(1)容器自己原有自帶的掛載的數據卷,應該是容器的json鏡像文件中 "Volumes"這個key對應得內容;
(2)經過其餘數據卷容器(經過--volumes-from)掛載的數據卷;
(3)經過命令行參數(-v參數)掛載的與主機綁定的數據卷,與主機綁定得數據卷在docker中叫作bind-mounts,這種數據卷與通常的正常得數據卷是有些細微區別的;
daemon.RegisterLinks(container, hostConfig) (daemon/daemon_unix.go) 註冊互聯的容器,容器之間除了能夠經過 ip:端口 相互訪問,容器之間還能夠互聯(經過--link 容器名字 的方式),例如一個web容器能夠經過這種方式與一個數據庫容器互聯;互聯的容器之間能夠相互訪問,能夠經過環境變量和/etc/hosts 來公開鏈接信息。容器之間的互聯關係要註冊到sqlite3數據庫中,也就是daemon.containerGraph中,deamon.containerGraph是一個graphdb.Database類型,這個小型的數據庫默認的實現方式是sqllite,裏面存儲這容器之間的互聯關係;
而後繼續回到daemon.Create()函數,在setHostConfig()以後:
container.Mount()
由於上面提到了,在建立initLayer的時候,雖然創建了containerID的有關文件夾,可是並無進行對containerID及其父親鏡像進行掛載操做,這裏就是將containerID及其父鏡像進行掛載操做;
createContainerPlatformSpecificSettings(container, config, img)
這個函數的是處理config中得Volumes的,經過-v參數掛載的數據卷,但不是bind-volumes,bind-volumes是放置在hostConfig中的。config與hostConfig都是配置的結構,區別是config是隻與container相關的配置,hostConfig屬於與宿主機相關的配置選項;
回到daemon.Create()函數,接下來是container.ToDisk()
func (container *Container) toDisk() error {
data, err := json.Marshal(container)
if err != nil {
return err
}
pth, err := container.jsonPath()
if err != nil {
return err
}
if err := ioutil.WriteFile(pth, data, 0666); err != nil {
return err
}
return container.WriteHostConfig()
}
這段代碼就是將container中的config和hostconfig結構體存儲到磁盤上,存儲的路徑是/var/lib/docker/container/containerId/config.json 和 /var/lib/docker/container/containerId/hostConfig.json
到目前爲止,一個container須要的文件系統視角已經徹底創建起來,但這個container尚未啓動,尚未運行container裏面的命令,下一篇的內容討論container是怎樣運行起來的。