这篇文章给大家分享的是有关docker中run的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。
通过在/components/cli/command/commands.go里,抽象出各种命令的初始化操作。
使用第三方库"github.com/spf13/cobra"
docker run 初始化命令行终端解析参数,最终生成 APIclient发出REQUEST请求给docker daemon.
docker daemon的初始化,设置了server的监听地址,初始化化routerSwapper, registryService 以及layStore、imageStore、volumeStore等各种存储 。
docker run的命令解析为 docker container create 和 container start 两次请求:
其中container create 不涉及底层containerd的调用,首先将host.config 、networkingConfig和AdjustCPUShares等组装成一个客户端请求,发送到docker daemon注册该容器。该请求会完成拉取image, 以及初始化 baseContainer的RWlayer, config文件等,之后daemon就可以通过containerid来使用该容器。
container start 命令的核心是调用了daemon的containerStart(),它会完成
调用containerd进行create容器,调用libcontainerd模块 clnt *client 的初始化,
设置容器文件系统,挂载点: /var/lib/docker/overlay/{container.RWLayer.mountID}/merged
设置容器的网络模式,调用libnetwork ,CNM模型(sandbox, endpoint,network)
创建/proc /dev等spec文件,对容器所特有的属性进行设置,
调用containerd进行create容器
1)获取libcontainerd模块中的containers
2)获取gid和uid
3)创建state目录,配置文件路径。
4)创建一个containercommon对象,创建容器目录,以及配置文件路径,根据spec创建配置文件。
1) 读取spec对象的配置文件
2) 创建一个fifo的pipe
3) 定义containerd的请求对象,grpc调用containerd模块。
ctr.client.remote.apiClient.CreateContainer(context.Background(), r)
4)启动成功后,更新容器状态。
cmd/dockerd/daemon.go 中存在libcontainerd初始化的流程。
包括启动grpc服务器,对套接字进行监听。
通过grpc.dail 与grpc server建立连接conn, 根据该链接建立apiclient对象,发送json请求。
runContainerdDaemon
通过docker-containerd二进制与grpc server进行通信,
docker-containerd -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd --shim docker-containerd-shim --runtime docker-runc
执行结果的输入输出流重定向到docker daemon。
runc把state.json文件保存在容器运行时的状态信息,默认存放在/run/runc/{containerID}/state.json。
type Supervisor struct {
// stateDir is the directory on the system to store container runtime state information.
stateDir string
// name of the OCI compatible runtime used to execute containers
runtime string
runtimeArgs []string
shim string
containers map[string]*containerInfo
startTasks chan *startTask //这是containerd到runc的桥梁,由func (w *worker) Start()消费
// we need a lock around the subscribers map only because additions and deletions from
// the map are via the API so we cannot really control the concurrency
subscriberLock sync.RWMutex
subscribers map[chan Event]struct{}
machine Machine
tasks chan Task //所有来自于docker-daemon的request都会转化为event存放到这,由func (s *Supervisor) Start()消费
monitor *Monitor
eventLog []Event
eventLock sync.Mutex
timeout time.Duration
}
type startTask struct {
Container runtime.Container
CheckpointPath string
Stdin string
Stdout string
Stderr string
Err chan error
StartResponse chan StartResponse
}
我们知道containerd作为docker daemon的grpc server端,通过接收 apiclient request转化成对应的events,在不同的子系统distribution , bundles , runtime 中进行数据的流转,包括镜像上传下载,镜像打包和解压,运行时的创建销毁等。
其中containerd 核心组件包括 supervisor 和executor, 数据流如下:
docker-daemon
--->tasks chan Task
--->func (s *Supervisor) Start()消费
--->存放到startTasks chan *startTask
-->func (w *worker) Start()消费
docker-containerd初始化包括 新建Supervisor对象:
该对象会启动10个worker,负责处理创建新容器的任务(task)。
supervisor的初始化,包括startTask chan初始化,启动监控容器进程的monitor
一个worker包含一个supervisor和sync.waitgroup,wg用于实现容器启动。
supervisor的start,消费tasks,把task中的container数据组装成runtime.container, 封装到type startTask struct,发送到startTask chan队列。
启动grpc server(startServer),用来接收dockerd的request请求。
func daemon(context *cli.Context) error {
s := make(chan os.Signal, 2048)
signal.Notify(s, syscall.SIGTERM, syscall.SIGINT)
/*
新建一个supervisor,这个是containerd的核心部件
==>/supervisor/supervisor.go
==>func New
*/
sv, err := supervisor.New(
context.String("state-dir"),
context.String("runtime"),
context.String("shim"),
context.StringSlice("runtime-args"),
context.Duration("start-timeout"),
context.Int("retain-count"))
if err != nil {
return err
}
wg := &sync.WaitGroup{}
/*
supervisor 启动10个worker
==>/supervisor/worker.go
*/
for i := 0; i < 10; i++ {
wg.Add(1)
w := supervisor.NewWorker(sv, wg)
go w.Start()
}
//启动supervisor
if err := sv.Start(); err != nil {
return err
}
// Split the listen string of the form proto://addr
/*
根据参数获取监听器
listenSpec的值为 unix:///var/run/docker/libcontainerd/docker-containerd.sock
*/
listenSpec := context.String("listen")
listenParts := strings.SplitN(listenSpec, "://", 2)
if len(listenParts) != 2 {
return fmt.Errorf("bad listen address format %s, expected proto://address", listenSpec)
}
/*
启动grpc server端
*/
server, err := startServer(listenParts[0], listenParts[1], sv)
if err != nil {
return err
}
其中startServer负责启动grpc server,监听docker-containerd.sock,声明注册路由handler。
当CreateContainer handler接收到一个Request之后,会把其转化成type startTask struct,将其转化为一个StartTask 事件,其中存放创建容器的request信息。
通过s.sv.SendTask(e)将该事件发送给supervosior 主循环。
// SendTask sends the provided event to the the supervisors main event loop
/*
SendTask将evt Task发送给 the supervisors main event loop
所有来自于docker-daemon的request都会转化为event存放到这,生产者
*/
func (s *Supervisor) SendTask(evt Task) {
TasksCounter.Inc(1) //任务数+1
s.tasks <- evt
}
等待woker.Start()消费处理结果后,将StartResponse返回给docker-daemon。
负责将每一个request转化成特定的task类型,通过一个goroutine遍历task中所有的任务并进行处理。消费tasks,把task中的container数据组装成runtime.container, 封装到type startTask struct,发送到startTask chan队列。
负责调用containerd-shim, 监控容器中的进程,并把结果返回给StartResponse chan队列。
其中,
container.Start() 通过containerd-shim 调用runc create {containerID}
创建容器。
process, err := t.Container.Start(t.CheckpointPath, runtime.NewStdio(t.Stdin, t.Stdout, t.Stderr))
其中值得注意的是,container.start 和container.exec均是调用createcmd,exec 命令则是通过process.json中的相关属性来判断是Start()还是Exec(),最后组装成containerd-shim的调用命令。
当具体容器内进程pid生成(由runc生成)后,createCmd会启动一个go routine来等待shim命令的结束。 shim命令一般不会退出。 当shim发生退出时,如果容器内的进程仍在运行,则需要把该进程杀死;如果容器内进程已经不存在,则无需清理工作。
process.Start() 通过调用runc start {containerID}
命令启动容器的init进程
root@idc-gz:/var/run/docker/libcontainerd# tree -L 2 eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/
eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/
├── 95de4070f528e1d68c80142f679013815a2d1a00da7858c390ad4895b8f8991b-stdin
├── 95de4070f528e1d68c80142f679013815a2d1a00da7858c390ad4895b8f8991b-stdout
├── config.json
├── dc172589265f782a476af1ed302d3178887d078c737ff3d18b930cbc143e5fd5-stdin
├── dc172589265f782a476af1ed302d3178887d078c737ff3d18b930cbc143e5fd5-stdout
├── ef00cfa54bf014e3f732af3bda1f667c9b0f79c0d865f099b1bee014f0834844-stdin
├── ef00cfa54bf014e3f732af3bda1f667c9b0f79c0d865f099b1bee014f0834844-stdout
├── init-stdin
└── init-stdout
root@idc-gz:/var/run/docker/libcontainerdcontainerd# tree -L 2 eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/
eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/
├── dc172589265f782a476af1ed302d3178887d078c737ff3d18b930cbc143e5fd5
│ ├── control
│ ├── exit
│ ├── log.json
│ ├── pid
│ ├── process.json
│ ├── shim-log.json
│ └── starttime
├── ef00cfa54bf014e3f732af3bda1f667c9b0f79c0d865f099b1bee014f0834844
│ ├── control
│ ├── exit
│ ├── log.json
│ ├── pid
│ ├── process.json
│ ├── shim-log.json
│ └── starttime
├── init
│ ├── control
│ ├── exit
│ ├── log.json
│ ├── pid
│ ├── process.json
│ ├── shim-log.json
│ └── starttime
└── state.json
在源码create.go中,首先会加载config.json的配置,然后调用startContainer函数,其流程包括:
createContainer, 生成libcontainer.Container对象,状态处于stopped、destoryed。
调用loadFactory方法, 生成一个libcontainer.Factory对象。
调用factory.Create()方法,生成libcontainer.Container
把libcontainer.Container封装到type runner struct对象中。
runner.run负责将config.json设置将来在容器中启动的process,设置iopipe和tty
runc create ,调用container.Start(process)
linuxContainer.newParentPorcess组装要执行的parent命令, 组装出来的命令是/proc/self/exe init, 通过匿名管道让runc create 和runc init进行通信。
parent.start()会根据parent的类型来选择对应的start(),自此之后,将进入/proc/self/exe init,也就是runc init
将容器状态持久化到state.json,此时容器状态为created.
runc start,调用container.Run(process)
// LinuxFactory implements the default factory interface for linux based systems.
type LinuxFactory struct {
// Root directory for the factory to store state.
/*
factory 存放数据的根目录 默认是 /run/runc
而/run/runc/{containerID} 目录下,会有两个文件:
一个是管道exec.fifo
一个是state.json
*/
Root string
// InitArgs are arguments for calling the init responsibilities for spawning
// a container.
/*
用于设置 init命令 ,固定是 InitArgs: []string{"/proc/self/exe", "init"},
*/
InitArgs []string
// CriuPath is the path to the criu binary used for checkpoint and restore of
// containers.
// 用于checkpoint and restore
CriuPath string
// Validator provides validation to container configurations.
Validator validate.Validator
// NewCgroupsManager returns an initialized cgroups manager for a single container.
// 初始化一个针对单个容器的cgroups manager
NewCgroupsManager func(config *configs.Cgroup, paths map[string]string) cgroups.Manager
}
// 一个容器负责对应一个runner
type runner struct {
enableSubreaper bool
shouldDestroy bool
detach bool
listenFDs []*os.File
pidFile string
console string
container libcontainer.Container
create bool
}
runc create clone出一个子进程,namespace与父进程隔离,子进程中调用/proc/self/exe init进行初始化。
runc init的过程如下:
调用factory.StartInitialization();
配置容器内部网络,路由,初始化mount namespace, 调用setupRootfs在新的mount namespaces中配置设备、挂载点以及文件系统。
配置hostname, apparmor,processLabel,sysctl, readyonlyPath, maskPath.
获取父进程的退出信号,通过管道与父进程同步,先发出procReady再等待procRun
恢复parent进程的death信号量并检查当前父进程pid是否为我们原来记录的不是的话,kill ourself。
与父进程之间的同步已经完成,关闭pipe。
"只写" 方式打开fifo管道并写入0,会一直保持阻塞。等待runc start
以只读的方式打开FIFO管道,阻塞才会消除。之后本进程才会继续执行。
调用syscall.Exec,执行用户真正希望执行的命令。用来覆盖掉PID为1的Init进程。至此,在容器内部PID为1的进程才是用户希望一直在前台执行的进程。
init进程通过匿名管理读取父进程的信息,initType以及config信息。
调用func newContainerInit(),生成一个type linuxStandardInit struct对象
执行linuxStandardInit.Init(),Init进程会根据config配置初始化seccomp,并调用syscall.Exec执行cmd。
runc start的逻辑比较简单,分为两步:
从context中获取libcontainer.container对象。
通过判断container 的状态为created,执行linuxContainer.exec()。
以“只读”的方式打开FIFO管道,读取内容。这同时也恢复之前处于阻塞状态的`runc Init`进程,Init进程会执行最后调用用户期待的cmd部分。
如果读取到的data长度大于0,则读取到Create流程中最后写入的“0”,则删除FIFO管道文件。
感谢各位的阅读!关于“docker中run的示例分析”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。
原文链接:https://my.oschina.net/markz0928/blog/3164872