8

docker 源码分析(2) -- 主程序及命令行参数解析

 2 years ago
source link: https://yanhang.me/post/2015-01-30-docker-source-code-part2-start-and-args/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

研究一个大项目的源码,最好是从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看下:

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

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

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, ...)
		}
    }
}  

效果如下图所示 :

这里说的设置过程是指在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函数的实现 :

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外)进行处理 :

首先将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. 由上面代码可知,这是默认的形式 ,位于 /var/run/docker.sock

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

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

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

daemon

if *flDaemon {
	mainDaemon()
	return
}

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

TLS 认证

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

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

如果--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)
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK