libcontainer的工作流程

execdriver的run方法通过docker daemon提交一份command信息创建了一份可供libcontainer解读的容器配置container,继而创建真正的docker容器。OCI组织成立后,libcontainer进化为runC ,因此从技术上说,未来libcontainer/runC创建的将是符合Open Container Format (OCF)标准容器

这个阶段,execdriver需要借助libcontainer处理一些事情,

创建libcontainer构建容器需要使用的“进程”,进程对象(非真正进程),称为(Process)

设置容器的输出管道,这里使用的就是docker daemon提供给libcontainer的pipes,

使用名为Factory的工厂类,通过factory.Create(<容器>,<容器配置>)创建一个逻辑上的容器,称为Container,在这个过程中,容器配置container会填充到Container对象的config项里,container的使命至此就完成

执行Container.Start (Process)启动物理容器

execdriver执行由Docker daemon 提供的startcallback完成回调动作

execdriver执行Process.Wait, 一直等上Process的所有工作都完成

可以看到。libcontainer对Docker 容器做了一层更高级的抽象,它定义了Process和Container来对应Linux中“进程”和“容器”的关系。一旦“物理”的容器创建成功,其他调用者就可以通过容器ID获取这个逻辑容器Container ,接着使用container.start得到容器的资源使用信息,或者执行Container.Destory来销毁这个容器

综上,libcontainer中最主要的内容是Process、Container以及Factory这三个逻辑实体的实现原理。而execdriver或者其他调用者只要一次执行“使用Factory创建逻辑容器Container”、“启动逻辑容器container”和“逻辑容器创建物理容器”,即可完成docker容器创建

libcontainer实现原理

  用Factory创建逻辑容器Container

libcontainer中Factory存在的意义就是能够创建一个逻辑“容器对象”container,这个逻辑上的“容器对象”并不是一个运行着的docker容器,而包含了容器要运行的指令以及其参数、namespace和cgroups配置参数等。对于docker daemon来说,容器的定义只需要一种就够了,不同的容器只是实例的内容(属性和参数)不一样而已。对于libcontainer来说,由于它需要与底层系统打交道,不同的平台就需要创建出完全异构的“逻辑容器对象”(比Linux容器和Windows容器),这也是解释了为什么这里会使用“工厂模式”:今后libcontainer可以支持更多平台上各种类型的容器的实现,而execdriver调用libcontainer创建容器的方法却不会受到影响

验证容器运行的根目录(默认/var/lib/docker/containers)、容器ID(字母、数字和下化线,长度范围1~1024)和容器配置这三项内容的合法性

验证上述容器ID与现在有的容器不冲突

在根目录下创建以ID为名的容器工作目录(/var/lib/docker/containers/{容器ID})。

返回一个Container对象,其中的信息包括了容器ID、容器工作目录、容器配置、初始化指令和参数(即dockerinit),以及cgroups管理器(这里有直接通过文件操作管理和systemd管理两个选择。默认选第一种)

至此,Container就已经创建和初始化完成了

启动逻辑容器

参与物理容器创建过程的Process一个有两个实例,第一个叫Process,用于物理容器内进程的配置和IO的管理,前面的伪码中创建的Process就是指它,另一个叫ParentProcess负责从物理容器外部处理容器启动工作,与container对象直接进行交互,启动工作完成后,Parent-Process.start()来启动物理容器

创建ParentProcess的过程

(1)创建一个管道(pipe),用于与容器内部未来要运行的进程通信(这个pipe不同于前面的输出流pipe)

(2)根据逻辑容器container中与容器内未来要运行的进程相关的信息创建一个容器内进程启动命令cmd对象,这个对象有Golang语言中的os/exec包进行声明。docker 会调用os/exec包中的内置函数,根据cmd对象来创建一个新的进程,即容器中的第一个进程dockerinit。而cmd对象则需要从Container中获得的属性包括启动命令的路径、命令参数、输入输出、执行命令的根目录以及进程管道pipe等

(3)为cmd添加一个环境变量_LIBCONTAINER_INITTYPE=standard来告诉将来的容器进程(dock-erinit)当前执行的“创建”动作。设置这个标志是因为libcontainer还可以进入已有的容器执行子进程,即docker exec指令执行效果

(4)将容器需要配置的namespace添加到cmd的Cloneflags中,表示将来这个cmd要运行在上述namespace中。若需要加入user namespace,还要针对配置项进行用户映射,默认映射到宿主机的root用户

(5)将Container中的容器配置和Process中的Entrypoint信息合并为一份容器配置加入ParentProcess当中

实际上,ParentProcess是一个接口,上述过程真正创建的是一个称谓initProcess的具体实现对象,cmd、pipe、cgroup管理器和容器配置这4部分共同组成了一个initProcess。这个对象是用来“创建容器”所需的ParentProcess,这主要是为了同setnsProcess区分,后者作用是进入已有的容器。逻辑容器Container启动的过程实际上是initProcess对象的构建过程,而构建initProcess则是为了创建容器做准备。接下逻辑容器Container执行initProcess.start(),真正的Docker容器就诞生了

用逻辑容器创建物理容器

逻辑容器container 通过initProcess.start()方法新建物理容器的过程如下

(1)docker daemon 利用Golang的exec包执行initProcess.cmd,其效果等价于创建一个新的进程并称为它设置namespace。这个cmd里指定的命令就是容器诞生时第一个进程,对于libcontainer来说,这个命令来自于execdriver新建容器时加载daemon的initPath,即docker工作目录下的/var/lib/docker/init/dockerinit-{version}文件,dockerinit进程所在的namespace即用户为最终docker容器指定的namespace

(2)把容器进程dockerinit的PID加入cgroups中管理,至此我们可以说dockerinit的容器隔离已经初步完成

(3)创建容器内部网络设备,包括lo和veth。这一部分涉及netlink等

(4)通过管道发送容器配置给容器内进程dockerinit

(5)通过管道等待dockerinit根据上述配置完成初始化工作,或者返回错误。

综上所述。ParentProcess即(initProcess)启动一个子进程dockerinit作为容器内初始进程,接着,ParentProcess作为父进程通过pipe在容器外面对dockerinit管理和维护,在容器里,dockerinit进程只有一个功能,就是执行reexec.init(),该init方法做什么工作是由对应的execdriver注册到reexec当中的具体实现来决定的,对于libcontainer来说这里注册执行的是Factory当中的StartInitialization()。接下来所有动作都只在容器内完成

(1)创建pipe管道所需的文件描述符

(2)通过管道获取ParentProcess传来的容器配置,namespace网络等信息

(3)从配置信息中获取并设置容器内环境变量

(4)如果用户在docker run 指定了-ipc、-pid、-uts参数,则dockerinit还需要把自己加入到用户指定的上述namespace中

(5)初始化网络设备,这些网络设备正是在ParentProcess中创建的lo和veth。初始化工作包括:修改名称、分配MAC地址、添加IP地址等和默认配置网关

(6) 创建mount namespace,为挂载文件系统做准备

(7)在mount namespace中设置挂载点,挂载rootfs和各类文件设备、/proc.然后通过pivot_root切换进程根路径到rootfs的根路径

(8)写入hostname等,加载profile

(9)比较当前进程的父进程ID与初始化进程一开始记录下来的父进程ID,如果相同,说明父进程异常退出,终止初始化进程。否则执行最后一步

(10)最后一步,使用execv系统调用执行容器配置中的Args指令。

Args[0]正是用户指定的Entrypoint,Args[1,2,3...]则是该指令后面的运行参数,所有当容器创建后,它里面运行的进程已经从dockerinit变成了用户指定的命令Entrypoint(如果不知道。默认运行时/bin/sh)。execv调用就是为了保证这个“替换”发生后的Entrypoint指令继续使用原先dockerinit的PID信息等

dockerdaemon 通过容器创建所需的配置和用户需要启动的命令交给libcontainer,后者根据这些信息创建逻辑容器和父进程(步骤1),接下来父进程执行Cmd.start,才真正创建(clone)出了容器的namespace环境,并通过dockerinit以及管道来管理完成整个容器初始化过程,这个过程3个阶段

(1)docker daemon进程进行“用Factory创建出逻辑容器Container”“启动逻辑容器Container”等工作,构建ParentProcess对象,然后利用他创建容器内第一个进程dockerinit

(2)dockerinit利用reexec.init()执行starrtlnitialization()。这里dockerinit会将自己加入到用户指定的namespace(如果指定的话),然后开始进行容器内各种初始化工作

(3)starrtlnitialization()使用execv系统调用执行容器配置中的Arges指定的命令,即Entry-Point和docker run的参数

docker daemon 与容器间通信方式

容器进程启动需要做初始化工作,就使用namespace隔离后的两个进程间通信。我们负责创建容器的进程称为父进程,容器进程称为子进程,父进程克隆出子进程后,依旧是共享内存的。让子进程感知内存中写入了新数据依旧是个问题,一般有4中方式:信号发送(signal)、对内存轮询访问(poll memory)、sockets通信(sockets)、文件和文件描述符(files and files-description)

对于signal而言,本身包含的信息有限,需要额外记录,namespace带来的上下文变化使其操作更复杂,并不是最佳选择。显然通过轮询内存的方式来沟通是一个非常低效的方法,因为docker 会加入network namespace ,实际上初始化网络栈也是完全隔离的,所有socket方式并不可行

docker最终选择的方式是管道—文件和文件描述符的方式。在Linux中,通过pipe(intfd[2])系统调用就可以创建管道,参数是一个包含两个整数的数组。调用完成后,在fd[1]端写入数据,就可以从fd[0]端读取

调用pipe函数后,创建子进程会嵌入这个打开的文件描述符,对fd[0]写入数据后可在fd[0]端读取。通过管道,父子进程之间就可以通信,这个通信完成的标志就在EOF信号的传递。众所周知,当打开的文件描述符都是关闭时,才能读到EOF信号。因此libcontainer中父进程在通过pipe向子进程发送完毕初始化信息后,先关闭自己这一端的管道,然后等待子进程关闭另一端的管道文件描述符,传来EOF表示子进程已经完成这些初始化工作。

docker之启动创建容器流程的更多相关文章

  1. docker run启动的容器挂掉了,数据怎么办

    情景描述 在某个系统中,功能性的服务使用 docker stack deploy xxx 启动,某个国产数据库的服务单独使用 docker run xxx 启动,数据库服务没有将存储的位置挂载出来: ...

  2. 【快学Docker】快速创建容器,容器常用命令

    前言 容器是Docker的三大核心概念之一.简单地说,容器是独立运行的一个或一组应用,以及它们的运行态环境.对应的,虚拟机可以理解为模拟运行的一整套操作系统(提供了运行态环境和其他系统环境)和跑在上面 ...

  3. Docker修改已创建容器端口映射

    修改已创建容器端口映射. 通过编辑 hostconfig.json 文件来修改 Docker 容器的端口映射 该文件地址:/var/lib/docker/containers/[hash_of_the ...

  4. Docker 系列三(容器管理).

    一.运行容器 1.基于镜像新建一个容器并启动 : tomcat:8.0 -i:交互式操作 -t:终端 -rm:容器退出后随之将其删除,可以避免浪费空间 -p :端口映射 -d :容器在后台运行 指明了 ...

  5. Docker系列教程05 容器常用命令

    https://mp.weixin.qq.com/s?__biz=MzI4ODQ3NjE2OA==&mid=2247483890&idx=1&sn=2721f08624e6de ...

  6. Docker学习第二天-容器

    Docker 容器 容器是 Docker 又一核心概念. 简单的说,容器是独立运行的一个或一组应用,以及它们的运行态环境.对应的,虚拟机可以理解为模拟运行的一整套操作系统(提供了运行态环境和其他系统环 ...

  7. docker安装启动、配置MySql

    1.安装mysql镜像 docker pull mysql/mysql-server 2.docker中启动Mysql容器 docker run --name mysql01 -d -p 3306:3 ...

  8. Kubernetes将弃用Docker!与 containerd容器引擎

    时间戳:2022-06-07 20:32:19 星期二 撰写文档参考:(阿良-腾讯课堂)Kubernetes将弃用Docker 参考博客k8s入坑之路(3)containerd容器 container ...

  9. 使用docker简单启动springboot项目

    1.搭建docker环境 需要linux系统必须是centOS7以上 执行一下命令: yum install epel-release –y yum clean all yum list 2.安装 y ...

随机推荐

  1. BZOJ1879 Bill的挑战

    题目:http://www.lydsy.com/JudgeOnline/problem.php?id=1879 本来是一道水题(~~~~(>_<)~~~~). 开始SB了,敲了个AC自动机 ...

  2. View Controller Programming Guide for iOS---(一)---About View Controllers

    About View Controllers View controllers are a vital link between an app’s data and its visual appear ...

  3. IntentService使用以及源码分析

    一 概述 我们知道,在Android开发中,遇到耗时的任务操作时,都是放到子线程去做,或者放到Service中去做,在Service中开一个子线程来执行耗时操作. 那么,在Service里面我们需要自 ...

  4. P3007 [USACO11JAN]大陆议会The Continental Cowngress(2-SAT)

    简述:给出 n 个法案, m 头牛的意见, 每头牛有两个表决 格式为 “支持或反对某法案”, 每头牛需要至少满足一个表决, 不可能成立的话输出 IMPOSSIBLE, 否则输出方案, Y代表能, N代 ...

  5. 莫队初探(不带修/例题极少)By cellur925

    因为今天考到莫队裸题了嘤嘤嘤...而我这样的蒟蒻肯定不会这样的高端算法啊QAQ.于是暴力水了40分qwq. 正如上文所说,我实在太菜了,于是学习莫队也只是学习了最简单的不带修普通莫队,如果我能苟到省选 ...

  6. .Net开发人员必备工具下载

    .Net开发人员必备工具下载   本人亲测下载地址: Win8.1破解工具下载: http://pan.baidu.com/s/1eQf2UiQ 可激活版本 Windows Vista Busines ...

  7. python之迷宫BFS

    # @File: maze_queue_bfs from collections import deque maze = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0 ...

  8. Reduce实现

    Reduce实现 参考 第一版 Array.prototype.fakeReduce = function (fn, base) { // this 指向原数组 // 拷贝数据, 更改指针方向 var ...

  9. C. Arcade dp二维费用背包 + 滚动数组 玄学

    http://codeforces.com/gym/101257/problem/C 询问从左上角走到右下角,每次只能向右或者向左,捡起三种物品算作一个logo,求最多能得到多少个logo. 设dp[ ...

  10. 在solr客户端删除库中的数据

    1.在solr客户端,访问你的索引库(我认为最方便的方法) 1)documents type 选择 XML 2)documents 输入下面语句<delete><query>* ...