【邮箱:summer_mushroom@163.com】
linux
【原创,转载请注明出处】git
要了解Libcontainer首先要了解linux container所用到的一些基本技术。linux container是一种内核虚拟化技术,能够提供轻量级的虚拟化,以便隔离进程和资源。而这正是docker容器技术和核心,docker正是linux container的一种实现。linux container所用到的基本技术包括namespace、cgroup、chroot、veth、union FS、iptables和netfilter、TC、quota、setrlimit,下面对这些基本技术作一个简要的归纳:github
1. Namespace:用来作资源隔离以实现轻量级虚拟化,包括六种namespace,UTS namespace提供了主机名和域名之间的隔离;IPC namespace提供了进程间通讯的隔离;Network namespace提供了网络的隔离包括网络设备、网络栈、端口等;Mount namespace提供了文件系统的隔离;User namespace提供了用户权限间的隔离。docker
2. Cgroups:实现资源限制,能够限制、记录任务组所使用的物理资源。还可用于优先级分配,经过分配的CPU时间片数量及磁盘IO宽带大小控制任务运行的优先级。用于资源统计,统计系统的资源使用量,如CPU使用时长、内存用量等。用于任务控制,能够对任务执行挂起、控制等操做。json
3. Chroot:更改root目录,用于在container里查看到的文件系统。他有三大优势:增长系统的安全性,限制了用户的权利;创建一个与原系统隔离的系统目录,这一点对容器极为重要;切换系统的根目录位置。 安全
4. Veth:把一个从网络用户空间(network namespace )发出的数据包转发到另外一个用户空间。即实现容器和宿主机之间的通讯。网络
5. Union FS:叠加的文件系统,其中包括aufs一种支持联合挂载的文件系统。函数
6. Iptables,netfilter:主要用来作ip数据包的过滤。源码分析
7. TC:主要用来作流量隔离,带宽的限制。测试
8. Quota:用来作磁盘读写大小的限制,用来限制用户可用空间的大小。
9. Setrlimit:能够限制container中打开的进程数,限制打开的文件个数等。
基于上文对linux container相关技术,docker基本是实现了前五个的技术,用libcontainer作了一层封装。也就是说docker经过libcontainer封装了linux container的部分技术,这样使得Docker具备持续部署与测试、跨云平台支持、环境标准化和版本控制、高资源利用率与隔离、容器跨平台性与镜像、易于理解且易用以及具备应用镜像仓库等优势。Libcontainer本质上是Docker中用于容器管理的包,基于Go语言实现,经过管理namespace、Cgroups、capabilities以及文件系统等来进行容器控制。Libcontainer可用于建立容器并对容器进行生命周期管理。提到Libcontainer就要提到execdriver,execdriver封装了对namespace、cgroups等对OS资源进行操做的全部方法,而Libcontainer是execdriver的默认实现。execdriver经过获得的command信息加载生成容器配置container,而后调用libcontainer加载容器配置container,建立真正的docker容器,完成容器的建立并对容器的生命周期进行管理。
Execdriver的工做流程如图2.1所示:
图2.1 execdriver的工做流程
Execdriver首先获得Docker daemon提交的command信息,提交过来的command信息包含namespace、cgroup等配置容器所需的重要信息。相对应的command结构体源码如图2.2,其中包含了生成容器所需的基本配置,有namespace相关好比UTS可提主机名和域名之间的隔离;IPC提供了进程间通讯的隔离;Network提供了网络的隔离包括网络设备、网络栈、端口等;Mount提供了文件系统的隔离。Resource包含了cgroup相关的信息,ProcessConfig表示容器中运行的进程的信息。
对图2.2中的部分参数作简要解释,其中:ContainerPid表示容器中进程的pid;ID是容器ID,表明容器的惟一标识,很是重要;Mount是namespace的一种用于文件系统的隔离;Network也是namespace的一种用于进行网络的隔离;ProcessConfig描述了容器中运行的进程的信息;Resource提供了cgroup相关的信息,后面会对Resource结构体展开作详细的分析;Rootfs是容器的根目录系统;WorkingDir顾名思义是容器的工做路径;TmpDir是用来存储docker临时文件的目录;
图2.2 command结构体
Cgroups用于实现资源限制,能够限制、记录任务组所使用的物理资源。cgroups相关信息包含在resource里,resource包含了对driver配置的全部资源的信息,resource结构体相关定义如图2.3,其中:memory表示所使用的存储容量,还定义了CPU用量等cgroup所需的信息。
图2.3 resource结构体
ProcessConfig中包含了表示容器中运行的进程的信息,ProcessConfig结构体相关定义源码如图2.4,
图2.4ProcessConfig结构体
图2.1中所示的工做流程相对应的源码在deamon/execdriver/native/driver.go的run函数中,run函数部分截图如图2.5所示,其中container, err := d.createContainer(c, hooks)语句的做用是调用createContainer函数建立容器配置。函数传入的参数c表示execdriver.Command,即上文提到的command结构体,也就是说createContainer函数根据command参数建立相关的容器配置。
图2.5 Run函数部分函数体
上文说到createContainer函数根据command参数建立相关的容器配置,下面咱们看一下createContainer函数的内部结构,如图2.6为createContainer函数的部分结构。其中container = execdriver.InitContainer(c)能够看到调用InitContainer函数经过传入的execdriver.Command参数生成容器配置container。其中一系列的createXXX()方法根据InitContainer函数获得的container填充模板,配置IPC、Pid、network等所需字段。其中createIpc()表示配置Ipc提供提供了进程间通讯的隔离;createPid()表示配置Pid;createUTS()表示配置UTS提供主机名和域名之间的隔离;createNetwork()配置Network提供了网络的隔离包括网络设备、网络栈、端口等。
图2.6 createContainer函数的部分函数体
由createContainer函数的源码的内部结构能够看到在createContainer函数中首先调用InitContainer函数生成了一个叫作container的变量,InitContainer函数经过传入的execdriver.Command参数生成容器配置container,如图2.7是execdriver.InitContainer函数的内部结构。在InitContainer函数中根据command配置container的hostname主机名、cgroup、devices、rootfs等信息,最后返回一个容器配置container,这时候的返回的container实际上是一个Config对象,表示容器配置。后面再由createContainer函数中的createXXX()方法根据InitContainer函数返回的container容器配置,配置相应IPC、Pid、network等所需字段。
图2.7 InitContainer函数的部分函数体
至此咱们已经分析完了deamon/execdriver/native/driver.go的run函数中container, err := d.createContainer(c, hooks)语句,简单的说该语句的结果就是生成了一份container容器配置。接下来在run函数中execdriver调用libcontainer加载已经生成好的容器配置container,建立真正的Docker容器。
在deamon/execdriver/native/driver.go的run函数中,成功生成container容器配置之后,工做就交由libcontainer。libcontainer的主要工做为:
1. 建立libcontainer构建容器所须要使用的进程对象,即Process。对应源码如图3.1所示。Process指定了容器内进程对象的配置和IO,其中有指定若干参数,并对参数赋值。Args表示将要运行的一系列指令;Env指定该进程对象的环境变量;Cwd将进程的工做目录改至容器的rootfs中;User将为容器中的正在运行的进程设置UID和GID。
图3.1 构建Process
2.接下来在run函数中err := setupPipes(container, &c.ProcessConfig, p, pipes);语句调用setupPipes函数设置容器的输出管道。而setupPipes函数即为设置容器输出管道函数,其函数体定义在deamon/execdriver/native/driver.go的setupPipes函数中。setupPipes函数主要经过execdriver.Pipes配置容器的输出管道,其主要做用是将容器的输出成标准输入、标准输出和标准错误。
3. 使用Factory工厂类,用容器ID和容器配置container建立逻辑容器Container,在run函数中对应的源码为:d.factory.Create(c.ID, container),其中c为execdriver.Command,c.ID为容器ID,container即为以前屡次提到的容器配置。在生成逻辑容器的过程当中,容器配置container的各项会填充到逻辑容器Container对像的配置项config里。
4.接下来用启动容器,启动容器对应的语句为cont.Start(p),其中cont为d.factory.Create(c.ID, container)函数生成的Container逻辑容器,而参数p为以前生成的容器所须要使用的进程对象Process。
5. 下面的代码p.Wait()即为process.Wait(),表示等待以前Process的全部工做都完成,直到物理容器建立成功。Processd的Wait函数所对应的源码为图3.2所示。
图3.2 Process的Wait()函数
6. 最后的cont.Destroy()表示Container.Destory(),即在须要的状况下能够销毁容器。
经过上述对libcontainer主要工做分析,咱们发现libcontainer的重点正是Process、Container、Factory这3个逻辑实体的实现。其中Factory用于建立一个逻辑上的容器对象;Container是包含容器配置信息的逻辑容器;Process用于物理容器中进程的配置和IO管理。下面咱们libcontainer中这三个逻辑实体进行详细的解析。
Factory的做用是用给定的容器ID建立一个新的容器,并在该容器中启动初始进程。而且接受的容器ID为只包含字母、数字、下划线组成的字符创,且长度必须在1到1024之间。容器ID不能与已经存在的容器的ID重合,使用同一路径(和文件系统)的Factory建立的容器必须有不一样的标识。最后用一个正在运行的进程返回一个新的容器。
在这个过程当中可能出现的错误有:IdInUse表示容器ID已经被其余容器占用;InvalidIdFormat表示容器ID的格式不正确;ConfigInvalid表示配置信息无用;Systemerror表示系统错误。一但发生错误,那么任何已经建立的容器部分都会被清除,保证了容器建立的原子性,要不所有建立成功,不然所有不建立。
Factory对象中包含三个函数,他们分别为:
1. Create()函数:其传入参数为一个容器ID和一份Config类型的配置参数,而且接受的容器ID为只包含字母、数字、下划线组成的字符创,且长度必须在1到1024之间。容器ID不能与已经存在的容器的ID重合,使用同一路径(和文件系统)的Factory建立的容器必须有不一样的标识。根据传入的这两个参数建立并返回一个Container类,其中包括容器ID、容器工做目录、容器配置、初始化指令和参数、以及Cgroup管理器等信息。在这个函数中Container建立完毕。其中可能出现路径不存在、容器已经中止、系统故障等错误。
2. Load()函数:传入参数为一个已经被成功Create过的容器的容器ID,返回该容器的信息。若是容器已经Create过说明存在id目录,则会从id目录下直接读取state.json来载入容器信息。其中可能出现的错误有管道链接错误和系统故障。
3. StartInitialization()函数:是容器初始化函数,是Libcontainer在容器从新执行期间会调用的内部API。
4. Type()函数:返回容器管理的类型,好比lxc或libcontainer等。
至此,Factory对象完成了容器的建立和初始化。接下来就了解一下包含包含容器配置信息的逻辑容器Container。
Container对象至关因而逻辑容器主要包含了容器配置、控制、状态显示等功能。其中ID表示容器的ID。Status表示容器内进程的状态,容器的状态包括:Running表示容器存在而且正在运行;Pausing表示容器存在而且进程正在被中止;Paused表示容器存在可是全部的进程都被中止了;Checkpointed表示容器存在而且容器状态都已保存至磁盘;Destoryed表示容器不存在。
Container对象中具备一系列容器相关的函数操做,其中包括:
ID():返回容器的ID,表明容器的惟一标识
Status():返回容器内进程的状态,可能为运行状态也多是中止状态。可能抛出的错误为ContainerDestroyed表示容器不存在已经被销毁;Systemerror表示系统错误。
State():返回容器的状态信息,包括容器ID、配置信息、初始进程ID、进程启动时间、cgroup文件路径、namespace路径等。可能出现的错误为Systemerror即系统错误。
Config():返回容器的配置信息
Processes():返回容器的PID,这个PID即为用来调用进程的namespace。有些PID可能不在与容器中的进程相关,除非容器的状态是PAUSED,这样才能保证每个PID都是有效的。
Stats():返回容器统计信息,包括cgroup中的统计以及网卡设备的统计信息。
Set():设置容器的资源配置,例如cgroup各个子系统的文件路径等。
Start():在容器内启动一个进程,若是进程启动失败就返回一个错误。能够根据以往的Process结构追踪进程的生命周期。其中主要工做有两个:建立ParentProcess实例,执行ParentProcess.start()来启动物理容器。ParentProcess是一个接口其具体实现为initProcess对象,initProcess用于建立容器所需的ParentProcess,为建立物理容器作准备。用逻辑容器Container执行initProcess.start(),真正的物理容器即Docker容器就生成了。
Destory():在结束全部的正在运行的进程之后销毁容器。
Process分为两类,一类是Process另一类是ParentProcess。Process用于容器内进程的配置和IO的管理,其参数包括:Args表示将要运行的一系列指令;Env指定该进程对象的环境变量;Cwd将进程的工做目录改至容器的rootfs中;User将为容器中的正在运行的进程设置UID和GID;Stdin io.Reader表示标准输入;Stdout io.Writer表示标准输出;Stderr io.Writer表示标准错误;consolePath表示到容器的控制台的路径;Capabilities表示容器中进程运行所需的权限;ops表示ParentProcess对象。ParentProcess负责处理容器启动工做,包含一系列的函数动做:
pid():返回一个正在运行的进程的pid,能够经过管道从已启动的容器进程中得到。
start():开始容器中的执行进程。
terminate():发送SIGKILL信号结束进程。
StartTime():获取进程启动时间。
signal():发送信号给进程。
wait():等待程序执行结束,返回结束的程序状态。
本文主要是对libcontainer的原理进行分析和探究。首先在第一小节介绍了linux container所用到的一些技术,而这些技术中Docker实现了其中的前五种即:Namespace用来作资源隔离以实现轻量级虚拟化; Cgroups实现资源限制、优先级分配、资源统计、任务控制;Chroot更改root目录,用于在container里查看到的文件系统;Veth实现容器和宿主机之间的通讯;Union FS实现实现叠加的文件系统。根据docker所实现的这五种linux container的技术介绍了libcontainer的本质做用, libcontainer实际上是Docker中用于容器管理的包,对以上这五种技术作了一层封装,以此实现对容器的控制管理。
在第二章节中对execdriver作了分析介绍,其中包括配置信息的介绍和execdriver工做流程的介绍。配置信息主要介绍了command结构体、namespace相关字段、resource结构体和ProcessConfig结构体。Execdriver的工做流程主要包括:execdriver获得Docker daemoe提供的command信息、根据command信息获得容器配置container、调用libcontainer加载容器配置container建立真正的docker容器。后面的章节主要详细介绍了execdriver调用libcontainer的详细步骤,主要为:使用Factory建立逻辑容器Container、启动逻辑容器Container、用逻辑容器建立物理容器。而后还详细分析了Factory、Container及Process对象,分析了这些对象的主要参数及主要方法函数等。
源码分析参考了浙江大学SEL实验室的《Docker容器与容器云》这本书,代码来自github.com/docker/docker/以及github.com/opencontainers/runc/libcontainer/。