Hamo's

$ cat /boot/vmlinuz /dev/{log, random, zero, null}

Docker源码分析2-daemon启动

| Comments

在该系列的 上一篇文章中, 我们简单介绍了Docker的基本信息。在这篇文章中,我们将详细分析Docker daemon的启动代码。从这篇文章开始,我将用$SRC指代Docker源码所在的 目录。

启动入口

Docker的启动入口位于$SRC/docker目录,在分析代码之前,先介绍Golang的一个小知识。Golang的每一个package,都可以有零个,一个或者多个func init(),多个init函数会按照它们在源文件中出现的顺序被执行。除mainpakcage外,init函数在package被import的时候执行;而对于mainpakcage,其func init()将在程序启动时,先于func main()被执行。[1]

$SRC/docker目录下共有4个文件,flags.go docker.go daemon.go以及client.go

flags.go包含对于daemon和client都生效的命令行参数。主要是设定 client与daemon之间通讯方式相关的参数。当然,决定该次docker作为 client还是daemon运行的-d参数也在flags.go文件中被定义。具体如下:

1
flDaemon      = flag.Bool([]string{"d", "-daemon"}, false, "Enable daemon mode")

这样,当我们用参数-d或者--daemon调用docker时, flDaemon会被置为true(默认值为false),从而影响后序 代码的执行。 这里要注意的是,Golang的flag对所有参数都是默认使用-指定的,所以 绑定在flDaemon上的长参数,Docker选择了-daemon。这样,我们 才可以按照Unix的惯例,使用-指定短参数,使用--指定长参数。

接下来,执行会进入到真正的入口函数func main(),该函数位于 $SRC/docker/docker.go文件中。首先会被调用的是 reexec.init(),其作用是实现类似busybox的程序调用,即根据文件名 决定程序的功能。这里暂且不表,有兴趣的同学可以自行研究reexec package的源码,代码位于$SRC/reexec目录下。

在完成对命令行参数使用flag.Parse()的解析后,首先会判断是否使用了 -v--version参数,之后会根据flDebug的值设置 DEBUG环境变量,在Docker中,很多类似debug这样的全局设置都是通过 环境变量传递的,这样做的好处是简化程序对该设置的处理,当需要修改或获 取该项设置时,直接访问环境变量即可;坏处也显而易见,程序容易受到运行 时已经定义的环境变量的影响。

如果用户没有通过命令行参数指定daemon与client的连接方式,则会检查 DOCKER_HOST环境变量,如果环境变量也没有指定,则将unix socket unix:///var/run/docker.sock 作为默认的连接方式压入 flHosts变量。之后,因为我们以-d或者--daemon参数启动 dockerflDaemon变量为true,我们会进入 mainDaemon函数进行daemon相关的动作。该函数位于 $SRC/docker/daemon.go

进入daemon.go,首先会利用之前提过的init()注册daemon所 需要的flag,并通过引用其他package,调用各个package的init()函数。 这里需要着重介绍的是如下代码:

1
2
3
4
import (
  _ "github.com/docker/docker/daemon/execdriver/lxc"
  _ "github.com/docker/docker/daemon/execdriver/native"
)

在import package时,如果将包名重新定义为_,则该package无法在后面 的代码中被调用(Golang不允许引用而不被调用的包存在)。引用一个包再将其 重命名为_,其目的就是为了调用该包自己的func init()。比如这 里,lxc和native就各自利用各自的init函数注册了一个reexec项,为 /.dockerinitnative设置各自的动作。而具体以这两个名字作为 文件名运行docker程序时会有什么不同,我们会在以后的章节中介绍。

言归正传,在daemon的init()函数中注册的flag位于 $SRC/daemon/config.go,由于当前文件也属于main包,这些flag实际上 在程序运行一开始就已经被注册,并不是在进入到mainDaemon函数后才 被注册的。现在的叙述方法只是为了更清楚的描述代码的结构。为了简单起见, 接下来的分析都基于默认参数, 有兴趣的同学可以自行研究一下daemon都支持 那些参数,各自的含义是什么。

接下来,我们将进入daemon部分最核心的代码engine,engine代码位于 $SRC/engine目录下。engine将所有操作封装在其内部,通过其 Register方法向engine注册操作。engine内部还封装了标准输入,输出 及错误流,以及操作之间相互同步的工具。简而言之,engine是所有操作运行 的容器。

mainDaemon函数首先会调用engine.New()生成一个全新的engine, 注册一个commands操作,其作用是按字典顺序显示所有已经注册过的操作。 并将所有已经注册过的操作写入这个新生成的engine里。

再之后,会通过signal.Trap()函数将os.Interrupt syscall.SIGTERM等信号绑定到新生成的engine的Shutdown成员上。这样 当daemon收到以上信号时,engine可以得到通知并进行相应的操作。

再之后,builtins.Register()函数会向engine中注册如下操作:

  • initnetworkdriver -> bridge.InitDriver
  • serveapi -> apiserver.ServeApi
  • acceptconnections -> apiserver.AcceptConnections
  • events -> Events.Get
  • log -> Events.Log
  • subscriberscount -> Events.SubscribersCount
  • version -> builtins.dockerVersion
  • auth -> registry.Service.Auth
  • search -> registry.Service.Search

这里不加介绍的列出builtins注册的每个操作及其对应的函数,我们会在具体用 到他们的时候再详细介绍。

再之后的流程会分成两部分,一部分用来初始化和启动RESTful API,但并不接 受连接;另一部分用来初始化daemon的服务,并在初始化结束后设置RESTful API可以接受新连接。这样即可加快启动的速度,同时也可以保证当daemon可以 接受API连接时,daemon处于就绪状态。

我们将会在 下一篇文章中 详细介绍daemon服务和RESTful API服务的启动过程。

[1]http://golang.org/ref/spec#Package_initialization

Comments