一、LXC概述
LXC(LinuxContainer)是来自于Sourceforge网站上的开源项目,LXC给Linux用户提供了用户空间的工具集,用户可以通过LXC创建和管理容器,在容器中创建运行操作系统就可以有效的隔离多个操作系统,实现操作系统级的虚拟化。最初的Docker容器技术基于LXC进行构建,后来Docker在自己的内核中刨除了LXC。
二、LXC命令介绍
从Linux内核2.6.27版本开始已经支持LXC,只需要在Linux用户态安装相应的用户态工具liblxc即可。下表3-1是LXC的常用命令接口:
三、LXC容器隔离实现机制
从前面的介绍中我们可以了解到,LXC能够创建容器用于Linux系统的虚拟化,而LXC作为用户层管理工具主要提供了管理容器的接口,对实现容器的机制进行了封装隐藏,本文将对LXC容器的实现机制进行分析。
LXC内部采用了Linux内核Namespace和Cgroup两个特性,下面我们将对这两种机制进行介绍。
3.1、Namespaee命名空间实现机制
Namespace命名空间机制给虚拟化提供了轻量级形式,即操作系统级虚拟化。该机制和FreeBSD的jail机制和OpenVZ类似。传统上,Linux系统中所有进程通过PID进行标识,内核只需要管理一个PID列表,而且用户通过uname系统调用获取的系统相关信息也全部相同。在Linux系统中,用户的管理方式通过UID编号,即通过全局唯一的UID列表进行标识。全局ID能够使内核很好的管理系统,选择允许或拒绝某些特权。如UID为0的root用户能够允许做任何事情,但是其他UID的用户就会受到限制;对于用户X无法杀死另一个用户Y的进程,但是用户X可以看到用户Y的活动状态,而这种状态并不适用于一些场景,比如对于隐私性要求较高的服务。
为了解决上述类似的问题,Namespace机制为进程ID,用户ID等系列资源的隔离提供了一种占用资源极少的解决方案,而其它虚拟化方案一般需要一台物理机运行多个内核进行上述资源的隔离,命名空间可以在一台物理机上运行一个内核,通过命名空间机制将上述全局资源进行抽象。它可以使一组进程放置于容器中,而各个容器间彼此隔离,也可以设置容器之间进行共享。从用户的角度看,命名空间将全局资源控制进行了分割放置到相应的容器中,在容器中进程只能看到本容器中的成员而无法看到其他容器的成员。
命名空间层次关系
在上图中,我们可以看到三个命名空间,父命名空间记录管理了6个PID值,而两个子命名空间父命名空间知道子命名空间的存在,而两个子命名空间不知道对方的存在,对于进程各个子命名空间中属性的改变无法将影响传播到其它命名空间(包括父命名空间),这样在两个子命名空间中运行Linux内核就可以很好的隔离两个系统。
- 命名空间在Linux内核中的表示
命名空间分别对UTS,进程间通信(IPC),文件系统视图,进程PID,UID和网络六个属性进行封装。
在每个Task任务结构中包含了一个nsproxy结构,这样对于每个创建的进程都有对应的命名空间,通过该结构可以建立进程与命名空间之间的关系,
对于命名空间的支持,需要在Linux内核编译的时候进行启动设置。如果没有设置,会使用系统默认的命名空间即整个内核就一个命名空间,全局可见。
对于每个具体的子命名空间实现形式,在此不再具体展开。
- 命名空间在用户空间的表示
命名空间机制在用户层通过调用系统调用clone实现,clone系统调用与fIork系统调用的最大区别就是通过传入众多的参数选择性的继承父进程的资源,而flork系统调用就会复制父进程的资源。
通过设置flags参数就可以创建新的命名空间,选择性的继承父进程的资源。
flags设定为CLONE NEWPID时会创建一个新的PID命名空间,即该新的命名空间为进程提供了一个新的独立的P1D环境,调用clone系统的进程的ID号为1,就成为该命名空间的第一个进程,相当于Linux系统的lnit进程,由于是起始PID命名空间中的祖先进程,如果该进程结束在这个命名空间中的所有进程都会被结束。PID的命名空间具有层次性,在父命名空间中的进程可以创建出子命名空间,而子命名空间对父命名空间可见。
flags设定为CLONE—NEWIPC时会创建一个新的IPC命名空间,如果该标记没有设定,进程会根据父进程的参数进程设置。针对system V对象和POSIX信号队列的进程间通信机制,IPC命名空间可以进行隔离,即当前进程的通信对象或队列在本命名空间中对其它进程可见,而在不同命名空间中的进程进行通信类似于不同Linux系统间的通信。
flags设定为CLONE NEWUTS时会创建一个新的UTS命名空间,初始结构会以调用进程的UTS进行初始化,通过调用setdomainname函数和sethostname函数可以分别设置域名和主机名,调用uname函数可以获得UTS信息。
flags设定为CLONE NEWNS时会创建新的mount挂载点命名空间,建立的挂载点命名空间就是进程可见的文件结构视图,所以通过挂载命名空间可以很好的进行文件系统的隔离。
flags设定为CLONE NEWNET时会创建一个新的网络命名空间,网络命名空间对网络栈进行了视图上的隔离,包括IPV4,IPV6,栈协议,ip路由表,防火墙规则等。一个物理网络设备只能一次对应一个网络命名空间,但通过创建虚拟网络设备创建的隧道可以与实际的物理网络设备进行通信,这样就可以实现多个网络设备的通信。
以上所述的flags标识可以组合使用,LXC调用该接口可以很方便的创建独立的运行环境,设定相应的参数,完成操作系统级的虚拟化隔离。
3.2、Cgroup实现机制
LXC通过命名空间机制实现了资源的隔离,而对于物理资源的限制则通过cgroup(control group)机制实现。
cgroup系统定义了以下的概念:
1)控制群(control group):控制群就是一组进程组,是cgroup系统中的控制的基本单位。
2)子系统:子系统是一类资源控制系统,比如CPU,是对c铲oup中进程的控制具体方法,通过子系统可以针对每个cgroup进行限制。目前cgroup机制支持的子系统如下表所示:
3)层级:层级是各个控制群以树形的方式进行排列,子节点的控制权继承父节点的树形。linux系统中一个进程在其中的一个控制群中,同时也在其中的一个层级下。一个层级通过cgroup系统的虚拟文件系统相对应。
它们之间的层级关系如下图所示:
cgroup层级关系
在Linux系统中第一个被用户建立的cgroup是根cgroup,包含了系统中所有的进程,位于第一层级。在上图中根cgroup又被分为两个子的cgroup系统,即cgroup0和cgroupl,位于cgroup系统的第二层。一个子系统只能在一个层级上,即在第一层级的cpuset子系统不同加入第二层级中的cgroup0和cgroupl,因为前面已经说过层级中子节点需要继承父节点的属性,如果两个层级中都有则会出现重复。每个层级可以有多个子系统,如第二层级有cpu和memo巧子系统。对于进程来说,可以位于不同层级的cgroup中。
- cgroup在linux内核层的表示
cgroup相关的数据结构和命名空间类似,也是从进程管理结构开始。如果需要使用cgroup机制需要在Linux内核编译的时候开启CONFIG_CGROUPS宏。在css_set结构中存储了与进程相关cgroups信息,在cg_list将在同一个css_set的进程组链接起来。Css_set结构如下
css_set结构中最重要成员就是cgroup_subsys_state指针数组,在进程和cgroup之间没有直接的联系,需要通过cgroup_subsys_state间接指向cgroup结构,这样做主要是因为有的时候读取子系统状态会比较频繁而对cgroup赋值等操作较少,struct cgroup中有一个成员cgroupfsroot结构,就是用于我们前面所说的层级关系的具体实现。
对于子系统的管理,则通过cgroup_subsys数据结构进行管理。在该结构中定义了一组操作的接口函数指针,相当于c++的基类,给出了接口函数具体的行为需要针对每个子系统的特点进行自行定义,比如cgroup subsys_state接口每个子系统返回的信息是不同的,需要不同的实现,这样的设计就可以很好的完成多种类型的兼容。
- cgroup在Linux用户层的表示
cgroup在linux用户层通过cgroup文件系统方式表示出来。比如在用户空间可以通过执行如下命令:mount -t cgroup -o cpu,memory cpu_mem/mycgroup/cpu_mem就可以创建名字为cpu_mem的层级,这个层级中有cpu和memory两个子系统,这两个系统可以是挂载至lJ/mycgroup/cpu_mem文件下。这一个创建cgroup层级的过程就如同创建新的文件夹的过程,只是创建的是cgroup特殊文件系统。如果在cpu_mem系统中想要创建一个新的cgroup,只要进入cpu_mem文件目录下,执行命令mkdir newcgroup就可以创建一个叫newcgroup的控制群,这样也表示新创建的cgroup是属于cpu_mem这一个层级。进入newcgroup文件目录后,会有相关子系统的控制文件,可以读取或者修改这些控制文件,比如向的tasks文件中写入当前系统中某个进程的pid号就相当于将进程加入了新创建的cgroup控制群中。