docker源码分析之Libcontainer

【作者:王潭(浙江大学软件学院硕士)

【邮箱:summer_mushroom@163.com】

【原创,转载请注明出处】


1. Libcontainer简介

1.1 linux container相关技术

要了解Libcontainer首先要了解linux container所用到的一些基本技术。linux container是一种内核虚拟化技术,可以提供轻量级的虚拟化,以便隔离进程和资源。而这正是docker容器技术和核心,docker正是linux container的一种实现。linux container所用到的基本技术包括namespace、cgroup、chroot、veth、union FS、iptables和netfilter、TC、quota、setrlimit,下面对这些基本技术做一个简要的概括:

1. Namespace:用来做资源隔离以实现轻量级虚拟化,包括六种namespace,UTS namespace提供了主机名和域名之间的隔离;IPC namespace提供了进程间通信的隔离;Network namespace提供了网络的隔离包括网络设备、网络栈、端口等;Mount namespace提供了文件系统的隔离;User namespace提供了用户权限间的隔离。

2. Cgroups:实现资源限制,可以限制、记录任务组所使用的物理资源。还可用于优先级分配,通过分配的CPU时间片数量及磁盘IO宽带大小控制任务运行的优先级。用于资源统计,统计系统的资源使用量,如CPU使用时长、内存用量等。用于任务控制,可以对任务执行挂起、控制等操作。

3. Chroot:更改root目录,用于在container里查看到的文件系统。他有三大优点:增加系统的安全性,限制了用户的权利;建立一个与原系统隔离的系统目录,这一点对容器极为重要;切换系统的根目录位置。

4. Veth:把一个从网络用户空间(network namespace )发出的数据包转发到另一个用户空间。即实现容器和宿主机之间的通信。

5. Union FS:叠加的文件系统,其中包括aufs一种支持联合挂载的文件系统。

6. Iptables,netfilter:主要用来做ip数据包的过滤。

7. TC:主要用来做流量隔离,带宽的限制。

8. Quota:用来做磁盘读写大小的限制,用来限制用户可用空间的大小。

9. Setrlimit:可以限制container中打开的进程数,限制打开的文件个数等。

1.2 Libcontainer简介

       基于上文对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容器,完成容器的创建并对容器的生命周期进行管理。

2. execdriver工作流程

       Execdriver的工作流程如图2.1所示:

204348_7xBu_2600761.png


2.1 execdriver的工作流程

2.1 配置信息简介

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临时文件的目录;

204651_cgU4_2600761.png

204651_s9uu_2600761.png


2.2 command结构体

Cgroups用于实现资源限制,可以限制、记录任务组所使用的物理资源。cgroups相关信息包含在resource里,resource包含了对driver配置的所有资源的信息,resource结构体相关定义如图2.3,其中:memory表示所使用的存储容量,还定义了CPU用量等cgroup所需的信息。

204747_s8Dj_2600761.png

204747_qwmI_2600761.png



2.3 resource结构体

       ProcessConfig中包含了表示容器中运行的进程的信息,ProcessConfig结构体相关定义源码如图2.4,

204813_low2_2600761.png

204813_HrJl_2600761.png


2.4ProcessConfig结构体

2.2 主要流程分析

       图2.1中所示的工作流程相对应的源码在deamon/execdriver/native/driver.go的run函数中,run函数部分截图如图2.5所示,其中container, err := d.createContainer(c, hooks)语句的作用是调用createContainer函数创建容器配置。函数传入的参数c表示execdriver.Command,即上文提到的command结构体,也就是说createContainer函数根据command参数创建相关的容器配置。

204837_MjdC_2600761.png

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提供了网络的隔离包括网络设备、网络栈、端口等。

204855_Hkwe_2600761.png

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等所需字段。

204911_5TMZ_2600761.png

2.7 InitContainer函数的部分函数体

    至此我们已经分析完了deamon/execdriver/native/driver.go的run函数中container, err := d.createContainer(c, hooks)语句,简单的说该语句的结果就是生成了一份container容器配置。接下来在run函数中execdriver调用libcontainer加载已经生成好的容器配置container,创建真正的Docker容器。

3. Libcontainer实现原理

       在deamon/execdriver/native/driver.go的run函数中,成功生成container容器配置以后,工作就交由libcontainer。libcontainer的主要工作为:

1. 创建libcontainer构建容器所需要使用的进程对象,即Process。对应源码如图3.1所示。Process指定了容器内进程对象的配置和IO,其中有指定若干参数,并对参数赋值。Args表示将要运行的一系列指令;Env指定该进程对象的环境变量;Cwd将进程的工作目录改至容器的rootfs中;User将为容器中的正在运行的进程设置UID和GID。

204932_sPpr_2600761.png

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所示。

204949_m8z1_2600761.png

3.2 ProcessWait()函数

6. 最后的cont.Destroy()表示Container.Destory(),即在需要的情况下可以销毁容器。

通过上述对libcontainer主要工作分析,我们发现libcontainer的重点正是Process、Container、Factory这3个逻辑实体的实现。其中Factory用于创建一个逻辑上的容器对象;Container是包含容器配置信息的逻辑容器;Process用于物理容器中进程的配置和IO管理。下面我们libcontainer中这三个逻辑实体进行详细的解析。

3.1 Factory创建逻辑容器

       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。

3.2 逻辑容器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():在结束所有的正在运行的进程以后销毁容器。

3.3  Process对象

    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():等待程序执行结束,返回结束的程序状态。

4. 总结

本文主要是对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/。


转载于:https://my.oschina.net/u/2600761/blog/638026

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值