docker 源码分析 四(基于1.8.2版本),Docker镜像的获取和存储

时间:2024-08-10 15:34:02

前段时间一直忙些其他事情,docker源码分析的事情耽搁了,今天接着写,上一章了解了docker client 和 docker daemon(会启动一个http server)是C/S的结构,client端发出的命令由docker daemon接收并处理。

我们在运行docker的时候,可能会使用到docker run命令(当然通过Dockerfile运行docker build命令也是一样的)时,如果本地没有你需要的镜像,docker daemon首先会去下载你需要的docker镜像,然后存储在本地;另外docker 镜像其实是一个很神奇的东西,它有多个层(layer)构成,每一个层的上一层是本层的父亲层(parent layer)。最上层(top layer)是可读可写层,用户对镜像的更新在这一层起作用,top layer之下的层都是只读层;这种实现方式其实也是一种文件系统,UnionFS。

本文就从上一章的结尾,分析一下docker pull 命令的实现,就是docker 怎样下载镜像并怎样存储的;

docker run的客户端命令所在文件是api/client/pull.go 下:

func (cli *DockerCli) CmdPull(args ...string) error {

cmd := Cli.Subcmd("pull", []string{"NAME[:TAG|@DIGEST]"}, "Pull an image or a repository from a registry", true)

allTags := cmd.Bool([]string{"a", "-all-tags"}, false, "Download all tagged images in the repository")

addTrustedFlags(cmd, true)

cmd.Require(flag.Exact, 1)

cmd.ParseFlags(args, true)

remote := cmd.Arg(0)

taglessRemote, tag := parsers.ParseRepositoryTag(remote)

if tag == "" && !*allTags {

tag = tags.DefaultTag

fmt.Fprintf(cli.out, "Using default tag: %s\n", tag)

} else if tag != "" && *allTags {

return fmt.Errorf("tag can't be used with --all-tags/-a")

}

ref := registry.ParseReference(tag)

// Resolve the Repository name from fqn to RepositoryInfo

repoInfo, err := registry.ParseRepositoryInfo(taglessRemote)

if err != nil {

return err

}

if isTrusted() && !ref.HasDigest() {

// Check if tag is digest

authConfig := registry.ResolveAuthConfig(cli.configFile, repoInfo.Index)

return cli.trustedPull(repoInfo, ref, authConfig)

}

v := url.Values{}

v.Set("fromImage", ref.ImageName(taglessRemote))

_, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull")

return err

}

ParseRepositoryTag(pkg/parsers/parsers.go)的作用就是从输入的pull 后面的字符串中提取出tag名字和剩下的部分,叫taglessRemote,

举个例子:

(a) api.example.com/ubuntu:10.04 会被拆分成 api.example.com/ubuntu 和 10.04 两个部分;

(b) ubuntu:10.04 会被拆分成 ubunu 和 10.04 两个部分;

(c) api.example.com@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb 还有一种digest(我理解就是对镜像生成的摘要)的形式,sha256是生成digest的方法;

这种包含@ 的形式会 分割成@左右两个部分,就是api.example.com 和 sha256:xxx...

如果分离出的tag为空 并且 alltags flag的值也为空的话(两者不能同时不为空),那么tag就会取默认值,默认值是latest;

ref := registry.ParseReference(tag) (registry/reference.go)的作用就是将分离出的tag 转成成内部的tagReference或者digestReference的形式;

repoInfo, err := registry.ParseRepositoryInfo(taglessRemote) (registry/config.go)的作用就是将taglessRemote转成RepositoryInfo的struct;

RepositoryInfo (registry/types.go)的结构如下,是用来描述一个镜像除了tag之外的部分,可能包括url路径:

type RepositoryInfo struct {

Index *IndexInfo             //registry 信息

RemoteName string         //"library/ubuntu-12.04-base"

LocalName string             //"ubuntu-12.04-base"

CanonicalName string      //"docker.io/library/ubuntu-12.04-base"

Official bool                    //像ubuntu的名字就是true,像xxx/ubuntu这种名字就是false;

}

这三种name之间的区别就如代码的注释中的一样,Index 也是一个表示registry信息的struct (registry/types.go),里面主要包括的name(registry的名字,例如官方docker.io),mirrors表示这个registry的镜像,表现为就是一个url的list;

源码中有几个repositoryInfo的例子也许能更直观点:

// RepositoryInfo Examples:

// {

//   "Index" : {

//     "Name" : "docker.io",

//     "Mirrors" : ["https://registry-2.docker.io/v1/", "https://registry-3.docker.io/v1/"],

//     "Secure" : true,

//     "Official" : true,

//   },

//   "RemoteName" : "library/debian",

//   "LocalName" : "debian",

//   "CanonicalName" : "docker.io/debian"

//   "Official" : true,

// }

//

// {

//   "Index" : {

//     "Name" : "127.0.0.1:5000",

//     "Mirrors" : [],

//     "Secure" : false,

//     "Official" : false,

//   },

//   "RemoteName" : "user/repo",

//   "LocalName" : "127.0.0.1:5000/user/repo",

//   "CanonicalName" : "127.0.0.1:5000/user/repo",

//   "Official" : false,

// }

如果稍后docker daemon要访问的registry 需要验证,则通过 repo.Index 和 cli.configFile (api/client/cli.go) 取出对应registry的认证信息 authConfig,autoConfig在cliconfig/config.go文件中:

type AuthConfig struct {

Username      string `json:"username,omitempty"`

Password      string `json:"password,omitempty"`

Auth          string `json:"auth"`

Email         string `json:"email"`

ServerAddress string `json:"serveraddress,omitempty"`

}

接着调用trustedPull (api/client/trust.go)方法,最终trustPull方法也会通过restful API来调用

_, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull") 方法来将pull image的请求发送给docker server进行处理;

本系列文章的前两章中有介绍,docker server对应pull请求的handler是postImagesCreate (api/server/image.go)。

// Creates an image from Pull or from Import

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

.......

if image != "" { //pull

if tag == "" {

image, tag = parsers.ParseRepositoryTag(image)

}

metaHeaders := map[string][]string{}

for k, v := range r.Header {

if strings.HasPrefix(k, "X-Meta-") {

metaHeaders[k] = v

}

}

imagePullConfig := &graph.ImagePullConfig{

MetaHeaders: metaHeaders,

AuthConfig:  authConfig,

OutStream:   output,

}

err = s.daemon.Repositories().Pull(image, tag, imagePullConfig)

} else { //import

if tag == "" {

repo, tag = parsers.ParseRepositoryTag(repo)

}

src := r.Form.Get("fromSrc")

// 'err' MUST NOT be defined within this block, we need any error

// generated from the download to be available to the output

// stream processing below

var newConfig *runconfig.Config

newConfig, err = builder.BuildFromConfig(s.daemon, &runconfig.Config{}, r.Form["changes"])

if err != nil {

return err

}

err = s.daemon.Repositories().Import(src, repo, tag, message, r.Body, output, newConfig)

}

if err != nil {

if !output.Flushed() {

return err

}

sf := streamformatter.NewJSONStreamFormatter()

output.Write(sf.FormatError(err))

}

return nil

}

postImagesCreate函数只截取重要的部分,这里省略号的部分主要是从http request中提取出image名称等参数,当image不为空的时候,由于docker server也需要与 docker registry 通过http交互来下载docker网络镜像,所以首先封装了 imagePullConfig 参数,在与registry通信的时候使用。接下来调用

err = s.daemon.Repositories().Pull(image, tag, imagePullConfig)

s.daemon.Repositories() (daemon/daemon.go) 是*graph.TagStore (graph/tags.go)类型, TagStore是一个比较重要的类型: 它保存着Graph用来完成对镜像的存储,管理着各种repository,同时pullingPool 和 pushingPool 保证同一个时间段只能有一个相同的镜像被下载和上传;

type TagStore struct {

path  string

graph *Graph

// Repositories is a map of repositories, indexed by name.

Repositories map[string]Repository

trustKey     libtrust.PrivateKey

sync.Mutex

// FIXME: move push/pull-related fields

// to a helper type

pullingPool     map[string]chan struct{}

pushingPool     map[string]chan struct{}

registryService *registry.Service

eventsService   *events.Events

trustService    *trust.Store

}

接着是 TagStore的Pull()方法,

func (s *TagStore) Pull(image string, tag string, imagePullConfig *ImagePullConfig) error {

......

for _, endpoint := range endpoints {

logrus.Debugf("Trying to pull %s from %s %s", repoInfo.LocalName, endpoint.URL, endpoint.Version)

if !endpoint.Mirror && (endpoint.Official || endpoint.Version == registry.APIVersion2) {

if repoInfo.Official {

s.trustService.UpdateBase()

}

}

puller, err := NewPuller(s, endpoint, repoInfo, imagePullConfig, sf)

if err != nil {

lastErr = err

continue

}

if fallback, err := puller.Pull(tag); err != nil {

if fallback {

if _, ok := err.(registry.ErrNoSupport); !ok {

// Because we found an error that's not ErrNoSupport, discard all subsequent ErrNoSupport errors.

discardNoSupportErrors = true

// save the current error

lastErr = err

} else if !discardNoSupportErrors {

// Save the ErrNoSupport error, because it's either the first error or all encountered errors

// were also ErrNoSupport errors.

lastErr = err

}

continue

}

logrus.Debugf("Not continuing with error: %v", err)

return err

}

s.eventsService.Log("pull", logName, "")

return nil

}

......

}

还是截取主要的部分,函数最开始的部分主要是通过TagStore中的registryService,通过传入的image找到repositoryInfo,然后通过repositoryInfo找到下载镜像的endpoint;

接下来针对每一个endpoint,建立一个Puller:puller, err := NewPuller(s, endpoint, repoInfo, imagePullConfig, sf) 开始拉取镜像;sf就是个jsonformatter;

NewPuller会根据endpoint的形式(endpoint应该遵循restful api的设计,url中含有版本号),决定采用version1还是version2版本,我主要分析v2的版本,在graph/pull_v2.go中:

func (p *v2Puller) Pull(tag string) (fallback bool, err error) {

// TODO(tiborvass): was ReceiveTimeout

p.repo, err = NewV2Repository(p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig)

if err != nil {

logrus.Debugf("Error getting v2 registry: %v", err)

return true, err

}

p.sessionID = stringid.GenerateRandomID()

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

if registry.ContinueOnError(err) {

logrus.Debugf("Error trying v2 registry: %v", err)

return true, err

}

return false, err

}

return false, nil

}

最主要的函数是pullV2Repository()函数,同样在graph/pull_v2.go目录下:

func (p *v2Puller) pullV2Repository(tag string) (err error) {

var tags []string

taggedName := p.repoInfo.LocalName

if len(tag) > 0 {

tags = []string{tag}

taggedName = utils.ImageReference(p.repoInfo.LocalName, tag)

} else {

var err error

manSvc, err := p.repo.Manifests(context.Background())

if err != nil {

return err

}

tags, err = manSvc.Tags()

if err != nil {

return err

}

}

c, err := p.poolAdd("pull", taggedName)

if err != nil {

if c != nil {

// Another pull of the same repository is already taking place; just wait for it to finish

p.config.OutStream.Write(p.sf.FormatStatus("", "Repository %s already being pulled by another client. Waiting.", p.repoInfo.CanonicalName))

<-c

return nil

}

return err

}

defer p.poolRemove("pull", taggedName)

var layersDownloaded bool

for _, tag := range tags {

// pulledNew is true if either new layers were downloaded OR if existing images were newly tagged

// TODO(tiborvass): should we change the name of `layersDownload`? What about message in WriteStatus?

pulledNew, err := p.pullV2Tag(tag, taggedName)

if err != nil {

return err

}

layersDownloaded = layersDownloaded || pulledNew

}

writeStatus(taggedName, p.config.OutStream, p.sf, layersDownloaded)

return nil

}

首先获得tagName,通过manifest获得tags清单,一个repository可能对应着多个tag,docker的镜像呈现的是树形关系,比如ubuntu是一个repository,实际的存储情况是可能会有一个基础镜像base,这个基础镜像上,增加一些新的内容(实际上就是增加一个新的读写层,写入东西进去)就会形成新的镜像,比如:ubuntu:12.12是一个镜像,那么ubuntu:14.01是在前者基础上进行若干修改操作而形成的新的镜像;所以要下载ubuntu:14.01这个镜像的话,必须要将其父镜像完全下载下来,这样下载之后的镜像才是完整的;

看一下 c, err := p.poolAdd("pull", taggedName)  (graph/tags.go文件)这个函数:

func (store *TagStore) poolAdd(kind, key string) (chan struct{}, error) {

store.Lock()

defer store.Unlock()

if c, exists := store.pullingPool[key]; exists {

return c, fmt.Errorf("pull %s is already in progress", key)

}

if c, exists := store.pushingPool[key]; exists {

return c, fmt.Errorf("push %s is already in progress", key)

}

c := make(chan struct{})

switch kind {

case "pull":

store.pullingPool[key] = c

case "push":

store.pushingPool[key] = c

default:

return nil, fmt.Errorf("Unknown pool type")

}

return c, nil

}

这个tagStore的函数之前提到过,就是保证同一时刻,只能有一个tag在上传或者下载;当下载完成后,会调用 defer p.poolRemove("pull", taggedName) 将这个限制打开;接下来就是实际下载的函数 pullV2Tag 了,是一段很长的代码:

func (p *v2Puller) pullV2Tag(tag, taggedName string) (verified bool, err error) {

logrus.Debugf("Pulling tag from V2 registry: %q", tag)

//

out := p.config.OutStream

manSvc, err := p.repo.Manifests(context.Background())

if err != nil {

return false, err

}

manifest, err := manSvc.GetByTag(tag)

if err != nil {

return false, err

}

verified, err = p.validateManifest(manifest, tag)

if err != nil {

return false, err

}

if verified {

logrus.Printf("Image manifest for %s has been verified", taggedName)

}

pipeReader, pipeWriter := io.Pipe()

go func() {

if _, err := io.Copy(out, pipeReader); err != nil {

logrus.Errorf("error copying from layer download progress reader: %s", err)

if err := pipeReader.CloseWithError(err); err != nil {

logrus.Errorf("error closing the progress reader: %s", err)

}

}

}()

defer func() {

if err != nil {

// All operations on the pipe are synchronous. This call will wait

// until all current readers/writers are done using the pipe then

// set the error. All successive reads/writes will return with this

// error.

pipeWriter.CloseWithError(errors.New("download canceled"))

}

}()

out.Write(p.sf.FormatStatus(tag, "Pulling from %s", p.repo.Name()))

var downloads []*downloadInfo

var layerIDs []string

defer func() {

p.graph.Release(p.sessionID, layerIDs...)

}()

for i := len(manifest.FSLayers) - 1; i >= 0; i-- {

img, err := image.NewImgJSON([]byte(manifest.History[i].V1Compatibility))

if err != nil {

logrus.Debugf("error getting image v1 json: %v", err)

return false, err

}

p.graph.Retain(p.sessionID, img.ID)

layerIDs = append(layerIDs, img.ID)

// Check if exists

if p.graph.Exists(img.ID) {

logrus.Debugf("Image already exists: %s", img.ID)

out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Already exists", nil))

continue

}

out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Pulling fs layer", nil))

d := &downloadInfo{

img:    img,

digest: manifest.FSLayers[i].BlobSum,

// TODO: seems like this chan buffer solved hanging problem in go1.5,

// this can indicate some deeper problem that somehow we never take

// error from channel in loop below

err: make(chan error, 1),

out: pipeWriter,

}

downloads = append(downloads, d)

go p.download(d)

}

// run clean for all downloads to prevent leftovers

for _, d := range downloads {

defer func(d *downloadInfo) {

if d.tmpFile != nil {

d.tmpFile.Close()

if err := os.RemoveAll(d.tmpFile.Name()); err != nil {

logrus.Errorf("Failed to remove temp file: %s", d.tmpFile.Name())

}

}

}(d)

}

var tagUpdated bool

for _, d := range downloads {

if err := <-d.err; err != nil {

return false, err

}

if d.layer == nil {

continue

}

// if tmpFile is empty assume download and extracted elsewhere

d.tmpFile.Seek(0, 0)

reader := progressreader.New(progressreader.Config{

In:        d.tmpFile,

Out:       out,

Formatter: p.sf,

Size:      d.size,

NewLines:  false,

ID:        stringid.TruncateID(d.img.ID),

Action:    "Extracting",

})

err = p.graph.Register(d.img, reader)

if err != nil {

return false, err

}

// FIXME: Pool release here for parallel tag pull (ensures any downloads block until fully extracted)

out.Write(p.sf.FormatProgress(stringid.TruncateID(d.img.ID), "Pull complete", nil))

tagUpdated = true

}

manifestDigest, _, err := digestFromManifest(manifest, p.repoInfo.LocalName)

if err != nil {

return false, err

}

// Check for new tag if no layers downloaded

if !tagUpdated {

repo, err := p.Get(p.repoInfo.LocalName)

if err != nil {

return false, err

}

if repo != nil {

if _, exists := repo[tag]; !exists {

tagUpdated = true

}

} else {

tagUpdated = true

}

}

if verified && tagUpdated {

out.Write(p.sf.FormatStatus(p.repo.Name()+":"+tag, "The image you are pulling has been verified. Important: image verification is a tech preview feature and should  not be relied on to provide security."))

}

firstID := layerIDs[len(layerIDs)-1]

if utils.DigestReference(tag) {

// TODO(stevvooe): Ideally, we should always set the digest so we can

// use the digest whether we pull by it or not. Unfortunately, the tag

// store treats the digest as a separate tag, meaning there may be an

// untagged digest image that would seem to be dangling by a user.

if err = p.SetDigest(p.repoInfo.LocalName, tag, firstID); err != nil {

return false, err

}

} else {

// only set the repository/tag -> image ID mapping when pulling by tag (i.e. not by digest)

if err = p.Tag(p.repoInfo.LocalName, tag, firstID, true); err != nil {

return false, err

}

}

if manifestDigest != "" {

out.Write(p.sf.FormatStatus("", "Digest: %s", manifestDigest))

}

return tagUpdated, nil

}

讲重点部分,先从manifest中获取(这个过程也是通过http去endpoint那里获取)出这个tag对应的所有image,我理解就是image和其所有父镜像,然后for循环进行遍历:

for i := len(manifest.FSLayers) - 1; i >= 0; i-- {

img, err := image.NewImgJSON([]byte(manifest.History[i].V1Compatibility))

if err != nil {

logrus.Debugf("error getting image v1 json: %v", err)

return false, err

}

p.graph.Retain(p.sessionID, img.ID)

layerIDs = append(layerIDs, img.ID)

......

另外附上Manifest的结构:

type Manifest struct {

Versioned

// Name is the name of the image's repository

Name string `json:"name"`

// Tag is the tag of the image specified by this manifest

Tag string `json:"tag"`

// Architecture is the host architecture on which this image is intended to

// run

Architecture string `json:"architecture"`

// FSLayers is a list of filesystem layer blobSums contained in this image

FSLayers []FSLayer `json:"fsLayers"`

// History is a list of unstructured historical data for v1 compatibility

History []History `json:"history"`

}

docker中的每一个镜像,由两部分组成,一部分是镜像的实际的数据内容,另一部分是镜像对应的json文件,json文件中有镜像的ID,同时json数据的"config"这个key中还会记录这个镜像的一些动态信息,例如:设置的环境变量,运行的命令等等。image的信息就是在image/image.go中。

接着会调用p.graph (graph/graph.go),graph维持着不同版本的镜像文件和他们之间的关系,这里面的driver默认是aufs.go   (daemon/graphdriver/aufs/aufs.go)

type Graph struct {

root             string

idIndex          *truncindex.TruncIndex

driver           graphdriver.Driver

imageMutex       imageMutex // protect images in driver.

retained         *retainedLayers

tarSplitDisabled bool

}

下载之前先将

p.graph.Retain(p.sessionID, img.ID)

将sessionID 和 img.ID加入到 graph的数据结构

type retainedLayers struct {

layerHolders map[string]map[string]struct{} // map[layerID]map[sessionID]

sync.Mutex

}

这个结构维护着哪些imageId已经被下载过;如果if p.graph.Exists(img.ID)  为true,说明这个镜像被下载过,直接continue,否则将这个镜像加入下载的downloadInfo里面去去;然后  go p.download(d) 开始下载镜像,下载镜像的过程首先根据之前说到的TagStore判断是不是有同样的镜像在下载过程中,如果没有调用ioutil.TempFile()将镜像内容下载到临时文件;函数结束后,会defer的函数对tempfile进行清理;

最后一个主要步骤是在graph注册image的id和内容;

err = p.graph.Register(d.img, reader)   (graph/graph.go)

func (graph *Graph) Register(img *image.Image, layerData io.Reader) (err error) {

if err := image.ValidateID(img.ID); err != nil {

return err

}

graph.imageMutex.Lock(img.ID)

defer graph.imageMutex.Unlock(img.ID)

// Skip register if image is already registered

if graph.Exists(img.ID) {

return nil

}

defer func() {

// If any error occurs, remove the new dir from the driver.

// Don't check for errors since the dir might not have been created.

if err != nil {

graph.driver.Remove(img.ID)

}

}()

if err := os.RemoveAll(graph.imageRoot(img.ID)); err != nil && !os.IsNotExist(err) {

return err

}

graph.driver.Remove(img.ID)

tmp, err := graph.mktemp("")

defer os.RemoveAll(tmp)

if err != nil {

return fmt.Errorf("mktemp failed: %s", err)

}

// Create root filesystem in the driver

if err := createRootFilesystemInDriver(graph, img, layerData); err != nil {

return err

}

// Apply the diff/layer

if err := graph.storeImage(img, layerData, tmp); err != nil {

return err

}

// Commit

if err := os.Rename(tmp, graph.imageRoot(img.ID)); err != nil {

return err

}

graph.idIndex.Add(img.ID)

return nil

}

首先是验证graph是否已经注册过image,如果已经注册过image,那么直接返回nil 退出;接着删除已有的路径,稍后会说,docker存储镜像的时候会新建几个目录,

graph.imageRoot(img.ID) 这个目录是 /var/lib/docker/graph/imageID, 这个路径下每一个文件夹名称是一个imageID,在docker daemon 初始化的时候,会生成新的graph 实例,graph实例会通过restore()方法(graph/graph.go)根据目录下的内容来加载已有的镜像;

graph.driver.Remove(img.ID) graph包含driver,这里用aufs举例,文件存储在/var/lib/docker/aufs目录下,这个目录下会有三个文件夹 mnt, layers, diff。每一个目录下都会有一个以镜像ID为名称的文件,mnt下面存放的是以这个镜像为可读写层的挂载点;layers存储这以这个镜像的所有的祖先镜像的ID列表,diff存储这个镜像的实际的文件系统中的内容;

在删除了可能残留的目录后,开始建立新的目录, createRootFilesystemInDriver(graph, img, layerData),调用driver的Create函数(daemon/graphdriver/aufs/aufs.go),

func (a *Driver) Create(id, parent string) error {

if err := a.createDirsFor(id); err != nil {

return err

}

// Write the layers metadata

f, err := os.Create(path.Join(a.rootPath(), "layers", id))

if err != nil {

return err

}

defer f.Close()

if parent != "" {

ids, err := getParentIds(a.rootPath(), parent)

if err != nil {

return err

}

if _, err := fmt.Fprintln(f, parent); err != nil {

return err

}

for _, i := range ids {

if _, err := fmt.Fprintln(f, i); err != nil {

return err

}

}

}

return nil

}

通过createDirsFor创建mnt/imageID和diff/imageID两个文件夹,然后建立layers/imageID的文件,然后将parentID和parent的祖先ID列表写入到这个文件;

接下来对实际的镜像的实际内容进行存储,graph.storeImage(img, layerData, tmp),storeImage函数(graph/graph.go):

func (graph *Graph) storeImage(img *image.Image, layerData io.Reader, root string) (err error) {

// Store the layer. If layerData is not nil, unpack it into the new layer

if layerData != nil {

if err := graph.disassembleAndApplyTarLayer(img, layerData, root); err != nil {

return err

}

}

if err := graph.saveSize(root, img.Size); err != nil {

return err

}

f, err := os.OpenFile(jsonPath(root), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600))

if err != nil {

return err

}

defer f.Close()

return json.NewEncoder(f).Encode(img)

}

disassembleAndApplyTarLayer将下载下来的img解压到/var/lib/docker/aufs/diff/imageID中,接下来将镜像的大小也存储为一个文件,存储的地点是通过这句函数tmp, err := graph.mktemp("")建立的临时目录/var/lib/docker/graph/tmp_xxxxx中,

文件名是layersize,接下来存储image的json数据,存储的位置也是在临时目录中的文件名为json的文件;

再这些数据都存储完之后,调用os.Rename(tmp, graph.imageRoot(img.ID)) 将之前的临时目录/var/lib/docker/graph/tmp_xxxxx 改成 /var/lib/docker/graph/imageID

Register函数的最后一步是 graph.idIndex.Add(img.ID) ,将ID加入idIndex,idIndex是一个trie结构,为了方便用户根据镜像的前缀来方便的查找镜像;

docker的镜像pull就先写到这儿,下一篇趁热打铁,分析一个docker run的秘密;