研究一个大项目的源码,最好是从main函数入口,一步一步与实际程序相结合,将实际代 码与相应相印证。所以本篇的主要内容就是docker主程序的启动流程以及命令行参数解析的 过程。

程序入口

docker的main函数位于github.com/docker/docker/docker/docker.go文件中。代码不 是很长,逻辑也比较简单,主要的内容便是解析命令行参数并且进行各项设定,启动 daemon等。下面将详述各个部分。

命令行参数解析

支持命令行参数几乎是所有cmd程序所必备的功能,先不看docker的代码,依据我们过去的 经验,应该不难理出命令行参数解析的一般流程 :

  1. 设定好程序所支持的命令行参数列表,长选项、短选项、数据类型、默认值、描述信 息……等

  2. 一个一个解析实际输入的参数,获取实际值。其中要考虑短选项的组合、错误的参数、 出错的提示信息……

在docker的main函数中,命令行参数解析的功能主要由mflag包提供,而在main里只 需要这一句调用 :

import (
	flag "github.com/docker/docker/pkg/mflag"
	...
)

flag.Parse()

看函数名的意思,应该就是直接开始解析了。那解析前的设定在哪呢?

init 设定

在golang中,main并不总是最早开始执行的代码。在执行一个package中的代码的时候,需 要先初始化其package-level的变量以及执行init函数,如果有的话。如果导入了其他 的包,也要先对其进行初始化。在docker中,命令行参数的初始化设定即是通过包内的变量及 init函数来进行的。

github.com/docker/docker/docker/flags.go:

flVersion     = flag.Bool([]string{"v", "-version"}, false, "Print version information and quit")
flDaemon      = flag.Bool([]string{"d", "-daemon"}, false, "Enable daemon mode")
flDebug       = flag.Bool([]string{"D", "-debug"}, false, "Enable debug mode")
...
flLogLevel    = flag.String([]string{"l", "-log-level"}, "info", "Set the logging level")

在命令行里输入docker看下:

arg-1

可以看到结果和代码是一一对应的。

各个参数的设定都是类似的,长/短选项,默认值,描述信息。进入其中一个函数看看:

github.com/docker/docker/pkg/mflag/flag.go:

 func Bool(names []string, value bool, usage string) *bool {
	 return CommandLine.Bool(names, value, usage)
 }

各个参数依其数据类型分类,我们先看看CommandLine是什么 :

github.com/docker/docker/pkg/mflag/flag.go :

 var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

 func NewFlagSet(name string, errorHandling ErrorHandling) *FlagSet {
	 f := &FlagSet{
		 name:          name,
		 errorHandling: errorHandling,
	 }
	 return f
 }

CommandLine属于package-level的变量,在被docker的main包导入时就已经初始化好 了。CommandLine就是我们预设命令行参数的存储地方,其类型为FlagSet

FlagSet

Flagset存储了我们预先设定好的所支持的命令行参数信息,并且在解析过程中动态更 新 :

github.com/docker/docker/pkg/mflag/flag.go :

 type FlagSet struct {
	 Usage func()
	 name          string
	 parsed        bool
	 actual        map[string]*Flag
	 formal        map[string]*Flag
	 args          []string
	 errorHandling ErrorHandling
	 output        io.Writer 
}
  1. Usage : 见名知意,可知是用来输出help信息的。一般是在没输入参数或者参数出错 的时候使用

  2. name : CommandLine将其设为docker(os.Args[0]).
  3. parsed : 是否已经解析完成
  4. actual : 实际解析出来的命令行参数结果.
  5. formal : 存储预先定义好的所支持的命令行参数信息。
  6. args : 输入的命令行参数列表
  7. errorHandling : 解析遇到错误时的处理方式。

每一个命令行参数所对应的结构体为Flag,其定义为 :

github.com/docker/docker/pkg/mflag/flag.go :

 type Flag struct {
	 Names    []string // name as it appears on command line
	 Usage    string   // help message
	 Value    Value    // value as set
	 DefValue string   // default value (as text); for usage message
 }

定义简单明了,Namesstring列表是因为很多参数既有长类型也有短类型,两个名字 都存下来。还有一些个别的情况时将要废弃的参数形式,比如:

 flEnableCors  = flag.Bool([]string{"#api-enable-cors", "-api-enable-cors"}, false, "Enable CORS headers in the remote API")

其名字前有一个#符号。在解析时如果遇到这类参数,会输出一些警告信息 :

github.com/docker/docker/pkg/mflag/flag.go#parseOne() :

for i, n := range flag.Names {
	if n == fmt.Sprintf("#%s", name) {
		replacement := ""
		for j := i; j < len(flag.Names); j++ {
			if flag.Names[j][0] != '#' {
				replacement = flag.Names[j]
				break
			}
		}
		if replacement != "" { // 内容过长,省略部分。
			fmt.Fprintf(f.out(), "Warning: '-%s' is deprecated, ...)
		} else {
			fmt.Fprintf(f.out(), "Warning: '-%s' is deprecated, ...)
		}
    }
}  

效果如下图所示 :

deprecated

设置过程

这里说的设置过程是指在package初始化时所设定的支持的命令行参数的过程,以前面提 到过的daemon参数为例。

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

前面已经提到过,flag大部分的操作都由CommandLine变量执行,调用结果为:

func (f *FlagSet) Bool(names []string, value bool, usage string) *bool {
	p := new(bool)
	f.BoolVar(p, names, value, usage)
	return p
}

基本上还是向下传递参数,只不过多了一个p,p是一个指针,flDaemonp指向同一 个地址,即参数的值。前面说过在解析过程中会动态更新参数的值,用指针既可保证 flDaemon指向的是最后实际解析出来的值。

func BoolVar(p *bool, names []string, value bool, usage string) {
	CommandLine.Var(newBoolValue(value, p), names, usage)
}

type boolValue bool
	
func newBoolValue(val bool, p *bool) *boolValue {
	*p = val
	return (*boolValue)(p)
}

type定义了一个新的类型boolValue,可以猜想处理string会有stringValue,处理in会有intValue…它们都实现了Value接口:

type Value interface {
	String() string
	Set(string) error
}

Value是用于动态存储位于Flag中的值的接口.想想看,在最开始解析命令行参数时,我们需要对不同的类型作分别处理,有了统一的Value接口, 后续的处理就可以统一进行,不用对每种类型都定义处理函数.

int为例,其intValue各接口定义如下:

type intValue int

func newIntValue(val int, p *int) *intValue {
	*p = val
	return (*intValue)(p)
}

func (i *intValue) Set(s string) error {
	v, err := strconv.ParseInt(s, 0, 64)
	*i = intValue(v)
	return err
}

func (i *intValue) Get() interface{} { return int(*i) }

func (i *intValue) String() string { return fmt.Sprintf("%v", *i) }

其他类型与此类似,稍有不同的是bool类型。因为bool类型的参数通常并不需要明确地 指明其值,只要参数出现,即可认为为true。比如-d参数,并不需要写-d=true。针对 这种情况,boolValue提供了额外的IsBoolFlag()函数和boolFlag interface.

func (b *boolValue) IsBoolFlag() bool { return true }

type boolFlag interface {
	Value
	IsBoolFlag() bool
}

再回到原来的处理流程,看看最终的Var函数的实现 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (f *FlagSet) Var(value Value, names []string, usage string) {
	flag := &Flag{names, usage, value, value.String()}
	for _, name := range names {
		name = strings.TrimPrefix(name, "#")
		_, alreadythere := f.formal[name]
		if alreadythere {
			var msg string
			if f.name == "" {
				msg = fmt.Sprintf("flag redefined: %s", name)
			} else {
				msg = fmt.Sprintf("%s flag redefined: %s", f.name, name)
			}
			fmt.Fprintln(f.out(), msg)
			panic(msg) 
		}
		if f.formal == nil {
			f.formal = make(map[string]*Flag)
		}
		f.formal[name] = flag
	}
}

整个逻辑比较简单,先生成相应的Flag变量,然后建立各个参数名(长短名,将要废弃的 名字)对其的映射。各个参数均以此流程设置,最后都存储在FlagSetformal映射表中, 后续的解析便可以对照着处理了。

解析过程

func Parse() {
	CommandLine.Parse(os.Args[1:])
}

解析过程和设定过程一样都是由CommandLine变量来执行的,Parse直接读取全部参数 (除Args[0]docker外)进行处理 :

parse

首先将parsed置为true,然后将所有参数存入CommandLineargs,之后便是逐个处 理参数,在for循环内一直调用parseOne,处理出错的参数,依据errorHandling的设 置来决定是继续还是退出等等。我们先看看parseOne的实现,因为函数代码过长,分段详述:

  1. 先判断是不是一个flag
if len(f.args) == 0 {
	return false, "", nil
}
s := f.args[0]
if len(s) == 0 || s[0] != '-' || len(s) == 1 {
	return false, "", nil
}
if s[1] == '-' && len(s) == 2 { // "--" terminates the flags
	f.args = f.args[1:]
	return false, "", nil
}
name := s[1:]
if len(name) == 0 || name[0] == '=' {
	return false, "", f.failf("bad flag syntax: %s", s)
}

len(f.args) == 0 一般代表解析的终止,没有更多的参数了,结合上述Parse函数中的 判断,此时就会跳出for循环,正常结束解析流程。其他的几种args[0]情况也会导致相同结果:

  • 长度为0或1
  • 不以-开头
  • 值为 --
  • 格式错误,比如-=之类的。

如果确定args[0]是一个flag,其会从f.args去除,以便下一次处理的args[0]是下一 个参数。如果args[0]是形如--debug=false的格式,便需从中取出相应的namevalue

f.args = f.args[1:]
has_value := false
value := ""
if i := strings.Index(name, "="); i != -1 {
	value = trimQuotes(name[i+1:])
	has_value = true
	name = name[:i]
}

有了namevalue后,便可以与之前存在f.formal中的参数列表相对照,看其是否属于 程序所支持的参数:

flag, alreadythere := m[name] // BUG
if !alreadythere {
	if name == "-help" || name == "help" || name == "h" { 
		f.usage()
		return false, "", ErrHelp
	}
	if len(name) > 0 && name[0] == '-' {
		return false, "", f.failf("flag provided but not defined: -%s", name)
	}
	return false, name, ErrRetry
}

前面提到过CommandLineerrorHandlingErrorOnExit,碰到错误会直接退出。如 果没有在formal表中找到相应的记录,有三种情况,一种是需要查看帮助信息, 系统就在打印好帮助信息后退出,另一种是程序不支持的参数,打印错误信息退出。最后一 种是短参数写在了一起,比如-dD,代表--daemon --debug,这种情况需要返回上层继续 处理,我们也可以看到在Parse函数中对ErrRetry作了单独处理,将参数字符串分割为单 个字母,然后分别解析。

之后需要对bool类型的参数做特殊处理,原因前已详述:

if fv, ok := flag.Value.(boolFlag); ok && fv.IsBoolFlag() { 
	if has_value {
		if err := fv.Set(value); err != nil {
			return false, "", f.failf("invalid boolean value %q for  -%s: %v", value, name, err)
		}
	} else {
		fv.Set("true") //默认为true
	}
}

对于其他的类型,则必须解析到其值:

else {
	// It must have a value, which might be the next argument.
	if !has_value && len(f.args) > 0 {
		// value is the next arg
		has_value = true
		value, f.args = f.args[0], f.args[1:]
	}
	if !has_value {
		return false, "", f.failf("flag needs an argument: -%s", name)
	}
	if err := flag.Value.Set(value); err != nil {
		return false, "", f.failf("invalid value %q for flag -%s: %v", value, name, err)
	}
}

一般情况下都是以args列表中的下一个字符串为其值。至此,一个参数解析流程将结束, 后面只要不断重复此过程即可,除了需要对将要废弃的参数打印一些警告信息。当处理结束 时,CommandLineformal映射表中包含了所有预设的参数及更新的值,actual表中只 包含了程序运行时实际使用的参数及其信息。

后续设定

除了参数解析,整个main函数的其他部分就是比较简单地用解析到的值设置各个组件,理解了前者之后,后面的部分就没有什么难点了。

版本号

if *flVersion {
	showVersion()
	return
}

代码其实没什么好说的,这里主要想提及的是docker里设置版本号的方式。在docker根 目录下会有一个VERSION文件,里面记录了程序的版本号,然后在编译脚本中会读取其内 容来进行设置:

github.com/docker/docker/hack/make.sh :

VERSION=$(cat ./VERSION)

日志级别

if *flLogLevel != "" {
	lvl, err := log.ParseLevel(*flLogLevel)
	if err != nil {
		log.Fatalf("Unable to parse logging level: %s", *flLogLevel)
	}
	initLogging(lvl)
} else {
	initLogging(log.InfoLevel)
}

if *flDebug {
	os.Setenv("DEBUG", "1")
	initLogging(log.DebugLevel)
}

有两个设置日志的参数 : --log-level--debug,后者只是为了方便使用,且优先级更高。

sockets

if len(flHosts) == 0 {
	defaultHost := os.Getenv("DOCKER_HOST")
	if defaultHost == "" || *flDaemon {
		// If we do not have a host, default to unix socket
		defaultHost = fmt.Sprintf("unix://%s", api.DEFAULTUNIXSOCKET)
	}
	defaultHost, err := api.ValidateHost(defaultHost)
	if err != nil {
		log.Fatal(err)
	}
	flHosts = append(flHosts, defaultHost)
}

docker daemon可以监听三种类型的socket :

  1. unix

    由上面代码可知,这是默认的形式 ,位于 /var/run/docker.sock

  2. tcp

    远程访问(web api)需要开启tcp socket,默认是不加密和无需认证的。如果需要监 听所有interface,可以设为-H tcp://0.0.0.0:2375,或者可以自己指定特定的IP。

  3. fd

    基于systemd的系统可以用到,便于其他服务通过systemd socket activationdocker daemon交互。详见:sockert activation

flHosts是一个列表,可以多次指定-H参数。

daemon

if *flDaemon {
	mainDaemon()
	return
}

docker并不像redis等程序那样分为serverclient程序,区别即在这里。如果有-d参 数,就以daemon方式启动,没有,就当做是client.,然后继续解析子命令及其参数进行 处理。后面介绍的流程就是只针对client而言。

TLS 认证

即使对TLS的原理不是很了解,通过下面的代码,也很容易理解docker的认证过程:

tls

与此相关的主要有四个参数 :

tls-args

如果--tlsverify或者--tlstrue,则启用TLS认证。通过指定的三个PEM文件,生成 一个tls.Config,用于后面的docker clientdocker daemon的连接。

DockerCli

protoAddrParts := strings.SplitN(flHosts[0], "://", 2)

if *flTls || *flTlsVerify {
	cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, nil, protoAddrParts[0], protoAddrParts[1], &tlsConfig)
} else {
	cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, nil, protoAddrParts[0], protoAddrParts[1], nil)
}

子命令

最后一部分便是docker client子命令的解析与执行,比如doker ps,docker stop <id>等等。具体细节就留待以后解析了。

if err := cli.Cmd(flag.Args()...); err != nil {
	if sterr, ok := err.(*utils.StatusError); ok {
		if sterr.Status != "" {
			log.Println(sterr.Status)
		}
		os.Exit(sterr.StatusCode)
	}
	log.Fatal(err)
}