您好,登录后才能下订单哦!
这篇文章主要讲解了“docker源码分析Libcontainer”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“docker源码分析Libcontainer”吧!
要了解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中打开的进程数,限制打开的文件个数等。
基于上文对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():等待程序执行结束,返回结束的程序状态。
感谢各位的阅读,以上就是“docker源码分析Libcontainer”的内容了,经过本文的学习后,相信大家对docker源码分析Libcontainer这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是亿速云,小编将为大家推送更多相关知识点的文章,欢迎关注!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。