docker-libcontainer Dear 丶 2022-03-09 07:54 108阅读 0赞 #### libcontainer的工作方式 #### execdriver如何调用libcontainer加载容器配置container,继而创建真正的Docker容器? * 创建libcontaine构建容器需要使用的“进程”,进程对象(非真正进程),称为Process; * 设置容器的输出管道,这里使用的就是Docker daemon提供给libcontainer的pipes; * 使用名为Factory的工厂类,通过factory.Create(<容器ID>,<容器配置container>)创建一个“逻辑”上的容器,称为Container,在这个过程中,容器配置container会填充到Container对象的config项里,container的使命至此就完成了; * 执行Container.Start(Process)启动物理的容器; * execdriver执行由Docker daemon提供的startCallback完成回调动作; * execdriver执行Process.Wait,一直等上述Process的所有工作都完成。 可以看到,libcontainer对Docker容器做了一层更高级的抽象,它定义了Process和Container来对应Linux中“进程”与“容器”的关系。一旦“物理”的容器创建成功,其他调用者就可以通过容器ID获取这个逻辑容器Container,接着使用Container.Stats得到容器的资源使用信息,或者执行Container.Destory来销毁这个容器。 综上,libcontainer中最主要的内容是Process, Container以及Factory这3个逻辑实体的实现原理,而execdriver或者其他调用者只要依次执行“使用Factory创建逻辑容器Container",“启动逻辑容器Container”和“用逻辑容器创建物理容器”,即可完成Docker容器的创建。 #### libcontainer实现原理 #### 1. 用FactoryL\}l建逻辑容器Container libcontainer中Factory存在的意义就是能够创建一个逻辑上的“容器对象”Container,这个逻辑上的“容器对象”并不是一个运行着的Docker容器,而是包含了容器要运行的指令及其参数、namespace和cgroups配置参数等。 Factory的Create操作具体做的事情: * 验证容器运行的根目录(默认/var/lib/docker/containers)、容器ID(字母、数字和下划 线构成,长度范围为1~1024)和容器配置这三项内容的合法性。 * 验证上述容器ID与现有的容器不冲突。 * 在根目录下创建以ID为名的容器工作目录(/var/lib/docker/containers/\{容器ID\} ). * 返回一个Container对象,其中的信息包括了容器ID、容器工作目录、容器配置、初始化指令和参数(dockerinit),以及cgroups管理器(这里有直接通过文件操作管理和systemd管理两个选择,默认选第一种)。 2. 启动逻辑容器Container 参与物理容器创建过程的Process一共有两个实例,第一个叫Process,用于物理容器内进程的配置和IO的管理;另一个叫ParentProcess,负责从物理容器外部处理物理容器启动工作,与Container对象直接进行交互。启动工作完成后,ParentProcess负责执行等待、发信号、获得容器内进程pid等管理工作。 创建ParentProcess的过程如下: (1)创建一个管道(pipe ),用来与容器内未来要运行的进程通信(这个pipe不同于前面的输出流pipes,后面会做解释)。 (2)根据逻辑容器Container中与容器内未来要运行的进程相关的信息创建一个容器内进程启动命令cmd对象,这个对象由Golang语言中的os/exec包进行声明,Docker会调用os/exec包中的内置函数,根据cmd对象来创建一个新的进程,即容器中的第一个进程dockerinit。而cmd对象则需要从Container中获得的属性包括启动命令的路径、命令参数、输人输出、执行命令的根目录以及进程管道pipe等。 (3)为cmd添加一个环境变量\_LIBCONTAINER\_ INITTYPE=standard来告诉将来的容器进程(dockerinit )当前执行的是“创建”动作。设置这个标志是因为libcontainer还可以进人已有的容器执行子进程,即docker exec指令执行的效果。 (4)将容器需要配置的namespace添加到。and的Cloneflags中,表示将来这个cmd要运行在上述namespace中。若需要加人user namespace,还要针对配置项进行用户映射,默认映射到宿主机的root用户。 (5)将Container中的容器配置和Process中的Entrypoint信息合并为一份容器配置加人到ParentProcess当中。 实际上,ParentProcess是一个接口,上述过程真正创建的是一个称为initProcess的具体实现对象。cmd, pipe, cgroup管理器和容器配置这4部分共同组成了一个initProcess。这个对象是用来“创建容器”所需的ParentProcess,这主要是为了同setnsProcess区分,后者的作用是进入已有容器。逻辑容器Container启动的过程实际上就是initProcess对象的构建过程,而构建initProcess则是为创建物理容器做准备。 3. 用逻辑容器创建物理容器 逻辑容器Container通过initProcess.start()方法新建物理容器的过程如下: (1) Docker daemon利用Golang的exec包执行initProcess.cmd,其效果等价于创建一个新的进程并为它设置namespace。这个cmd里指定的命令就是容器诞生时的第一个进程。对于libcontainer来说,这个命令来自于execdriver新建容器时加载daemon的initPath,即 Docked作目录下的/var/lib/docker/init/dockerinit-\{version\}文件。dockerinit进程\}h在的namespace即用户为最终的Docker容器指定的namespace. (2)把容器进程dockerinit的PID加入到cgroup中管理。至此我们可以说dockerinit的容器隔离环境已经初步创建完成。 (3)创建容器内部的网络设备,包括to和veth。 (4)通过管道发送容器配置给容器内进程dockerinit. (5)通过管道等待dockerinit根据上述配置完成所有的初始化工作,或者出错返回。 dockerinit进程只有一个功能,那就是执行reexec.init(),该init方法做什么工作,是由对应的execdrive注册到reexe。当中的具体实现来决定的。对于libcontainer来说,这里要注册执行的是Factory当中的StartInitialization()。接下来的所有动作都发生在容器内部: * 创建pipe管道所需的文件描述符。 * 通过管道获取ParentProcess传来的容器配置,如namespace、网络等信息。 * 从配置信息中获取并设置容器内的环境变量,如区别新建容器和在已存在容器中执行命令的环境变量——LIBCONTAINER\_INITTYPE. * 如果用户在docker run中指定了一ipc、-pid、-uts参数,则dockerinit还需要把自己加人到用户指定的上述namespace中。 * 初始化网络设备,这些网络设备正是在ParentProcess中创建出来的lo和veth。这里的初始化工作包括:修改名称、分配MAC地址、设置MTU、添加IP地址和配置默认网关等。 * 设置路由和RLIMIT参数。 * 创建mount namespace,为挂载文件系统做准备。 * 在上述mount namespace中设置挂载点,挂载rootfs和各类文件设备,比如/proc。然后通过 pivot\_root切换进程根路径到rootfs的根路径。 * 写人hostname等,加载profile信息。 * 比较当前进程的父进程ID与初始化进程一开始记录下来的父进程ID,如果不相同,说明父进程异常退出过,终止这个初始化进程。否则执行最后一步。 * 最后一步,使用execv系统调用执行容器配置中的Args指定的命令。 至此,容器的创建和启动过程宣告结束,上述全部过程可以通过图3-9来描述。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Z5X2xvbmc_size_16_color_FFFFFF_t_70] [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Z5X2xvbmc_size_16_color_FFFFFF_t_70]: /images/20220309/3c39abb861f04579a22852e7e861c6ae.png
还没有评论,来说两句吧...