Lxc实现与cgroup关系
LXC 是 Linux Container的缩写。Linux 容器技术是一种内核虚拟化技术,它提供了轻量级的虚拟化技术,可以在单一控制主机上同时提供多个虚拟环境(即容器)以隔离进程和资源,每个虚拟环境拥有自己的进程和独立的命名空间。在基于容器的虚拟化技术中,进程不再是个全局概念,而是从属于某个特定的容器。理想情况下,进程跟容器之间是动态关联的,进程可以在容器之间迁移。在基于容器的虚拟化技术中,容器既是资源容器,也是隔离的命名空间,它能有效地将由单个操作系统管理的资源划分到隔离的组中,以更好地在隔离的组之间平衡有冲突的资源的占用需求。
实现LXC两大技术之一是命名空间。基于容器的虚拟化技术主要通过隔离操作系统内核的对象(例如 PID、UID、系统共享内存、IPC、net等等)来完成安全性的隔离,具体就是运用Namespace名字空间将原来的全局对象(句柄、UID等)隔离到完全不同的名称空间里,不同虚拟机之间是完全不可见的,因此它们也不能访问到名称空间之外的对象。全局对象在每个虚拟机内本地化了,换句话说,全局的对象标记仅仅是在每个虚拟机内部全局。Namespace特性的具体使用方式就是在clone时加入相应的flag(NEWNS NEWPID等等)。LXC正是通过在clone时设定这些flag,为进程创建一个有独立PID,IPC,FS,Network,UTS空间的容器。一个容器就是一个虚拟的运行环境,对容器里的进程是透明的,它会以为自己是直接在一个系统上运行的。
实现LXC另一个技术是cgroups。LXC在资源管理方面依赖与内核的 Cgroups (Control Groups) 子系统,Cgroups 子系统是内核提供的一个基于进程组的资源管理的框架,可以为特定的进程组限定可以使用的资源。
linux内核提供了cgroups控制组(control groups)的功能,最初由google的工程师提出,后来被整合进Linux内核。cgroups适用于多种应用场景,从单个进程的资源控制,到实现操作系统层次的虚拟化提供了以下功能:
1、限制进程组可以使用的资源数量(Resource limiting )。比如:memory子系统可以为进程组设定一个memory使用上限,一旦进程组使用的内存达到限额再申请内存,就会出发OOM(out of memory)。
2、进程组的优先级控制(Prioritization )。比如:可以使用cpu子系统为某个进程组分配特定cpu share。
3、记录进程组使用的资源数量(Accounting )。比如:可以使用cpuacct子系统记录某个进程组使用的cpu时间。
4、进程组隔离(Isolation)。比如:使用ns子系统可以使不同的进程组使用不同的namespace,以达到隔离的目的,不同的进程组有各自的进程、网络、文件系统挂载空间。
5、进程组控制(Control)。比如:使用freezer子系统可以将进程组挂起和恢复。
下面介绍lxc常用的管理接口:
1、lxc-start 创建容器,并在容器中执行给定命令
2、lxc-kill发送信号给容器中的第一个用户进程(容器内部进程号为2的进程)
3、lxc-cgroup用于获取或调整与cgroup相关的参数
简单介绍下命令用法:
lxc-start -n name [-f config_file] [-cconsole_file] [-d] [-s KEY=VAL] [command]
-d 将容器当做守护进程执行
-f 后面跟配置文件
-c 指定一个文件作为容器console的输出,如果不指定,将输出到终端
-s 指定配置
例如:
lxc-start -n foo1 -f ./lxc-cfg -d /bin/bash
这里所说的配置文件提供了cgroups、提供单独的网络、根目录空间等,以P4080DS单板为例的配置文件如下,具体含义有时机再进行分析。
lxc.utsname= foo23
lxc.network.type= macvlan
lxc.network.macvlan.mode= vepa
lxc.network.flags= up
lxc.network.link= br0
lxc.network.name= eth0
lxc.network.ipv4= 192.168.0.23
lxc.network.type= macvlan
lxc.network.macvlan.mode= vepa
lxc.network.flags= up
lxc.network.link= br0
lxc.network.name= eth1
lxc.network.ipv4= 192.168.128.23
lxc.tty= 1
lxc.pts= 1
lxc.cgroup.cpuset.cpus= 0,1
lxc.cgroup.cpu.shares= 1234
lxc.cgroup.devices.deny= a
lxc.cgroup.devices.allow= c 1:3 rw
lxc.cgroup.devices.allow= b 8:0 rw
lxc.rootfs= /var/lib/lxc/temp/rootfs
lxc.mount.entry=/lib/var/lib/lxc/temp/rootfs/lib none ro,bind 0 0
lxc.mount=/var/lib/lxc/temp/fstab
下面从一个Lxc容器的创建lxc-start 的流程图,可以清楚的了解lxc实现与cgroup的关系:
lxc实现流程(与cgroup关系)框图
从代码进行分析,使用lxc 开源0.9.0版本,下载地址http://sourceforge.net/projects/lxc/,目前最新的版本为lxc-0.9.0.tar.gz.
从main函数开始Main->lxc_start->__lxc_start
int __lxc_start(const char *name, struct lxc_conf*conf,
structlxc_operations* ops, void *data, const char *lxcpath)
{
structlxc_handler *handler;
interr = -1;
intstatus;
handler= lxc_init(name, conf, lxcpath);//初始化容器的管理结构lxc_handler结构,返回该结构体,lxc_handler中除了包含容器的名字、属性,更包含char *cgroup成员用来关联对应一个cgroup
if(!handler) {
ERROR("failedto initialize the container");
return-1;
}
handler->ops= ops;
handler->data= data; //应用参数赋值,容器启动后根据该值启动容器进程,上文中的/bin/bash
if (must_drop_cap_sys_boot()){
#ifHAVE_SYS_CAPABILITY_H
DEBUG("Droppingcap_sys_boot\n");
#else
DEBUG("Can'tdrop cap_sys_boot as capabilities aren't supported\n");
#endif
}else {
DEBUG("Notdropping cap_sys_boot or watching utmp\n");
handler->conf->need_utmp_watch= 0;
}
err= lxc_spawn(handler);//核心函数,创建命名空间关联cgroup
if(err) {
ERROR("failedto spawn '%s'", name);
gotoout_fini_nonet;
}
err= lxc_poll(name, handler);//父进程开始一个epoll循环,主要处理console和container的两端转发epoll,以及接收一些外来查询请求的unix套接口epoll。
if(err) {
ERROR("mainloopexited with an error");
gotoout_abort;
}
while(waitpid(handler->pid, &status, 0) < 0 && errno == EINTR)
continue;
... ...
}
创建命名空间关联cgroup 的核心函数lxc_spawn:
int lxc_spawn(struct lxc_handler *handler)
{
intfailed_before_rename = 0;
constchar *name = handler->name;
if(lxc_sync_init(handler))//调用socketpair():创造一对未命名的、相互连接的UNIX域套接字,,后续由lxc_sync_fini()关闭
return-1;
handler->clone_flags= CLONE_NEWUTS|CLONE_NEWPID|CLONE_NEWIPC|CLONE_NEWNS;//容器必备命名空间属性
if(!lxc_list_empty(&handler->conf->id_map)) {
INFO("Cloninga new user namespace");
handler->clone_flags|= CLONE_NEWUSER;
}
if(!lxc_list_empty(&handler->conf->network)) {//设置网络的命名空间
handler->clone_flags|= CLONE_NEWNET;
if(lxc_find_gateway_addresses(handler)) {
ERROR("failedto find gateway addresses");
lxc_sync_fini(handler);
return-1;
}
//容器的网络设备需要在clone之前填充,因为在clone进程中用到
if(lxc_create_network(handler)) {
ERROR("failedto create the network");
lxc_sync_fini(handler);
return-1;
}
}
if(save_phys_nics(handler->conf)) {
ERROR("failedto save physical nic info");
gotoout_abort;
}
/*
* if the rootfs is not a blockdev, prevent thecontainer from
* marking it readonly.
*/
handler->pinfd= pin_rootfs(handler->conf->rootfs.path);//设定指定根文件系统路径
if(handler->pinfd == -1) {
ERROR("failedto pin the container's rootfs");
gotoout_delete_net;
}
handler->pid= lxc_clone(do_start, handler, handler->clone_flags);//根据命名空间clone_flags,使用新namespace创建进程,因此容器的进程是彼此隔离的。
if(handler->pid < 0) {
SYSERROR("failedto fork into a new namespace");
gotoout_delete_net;
}
lxc_sync_fini_child(handler);
if(lxc_sync_wait_child(handler, LXC_SYNC_CONFIGURE))
failed_before_rename= 1;
/*TODO - pass lxc.cgroup.dir (or user's pam cgroup) in for first argument */
if((handler->cgroup = lxc_cgroup_path_create(NULL, name)) == NULL) //创建cgroup容器
gotoout_delete_net;
if(lxc_cgroup_enter(handler->cgroup, handler->pid) < 0)//将任务pid添加至cgroup
gotoout_delete_net;
if(failed_before_rename)
gotoout_delete_net;
/*Create the network configuration */
if(handler->clone_flags & CLONE_NEWNET) {
if(lxc_assign_network(&handler->conf->network, handler->pid)){//netlink_transaction
ERROR("failedto create the configured network");
gotoout_delete_net;
}
}
if(lxc_map_ids(&handler->conf->id_map, handler->pid)) {
ERROR("failedto set up id mapping");
gotoout_delete_net;
}
/*Tell the child to continue its initialization. we'll get
* LXC_SYNC_CGROUP when it is ready for us tosetup cgroups
*/
//当子进程准备好设置cgroups,告知子进程继续初始化
if(lxc_sync_barrier_child(handler, LXC_SYNC_POST_CONFIGURE))
gotoout_delete_net;
//根据配置文件设置cgroup属性
if(setup_cgroup(handler->cgroup, &handler->conf->cgroup)) {
ERROR("failedto setup the cgroups for '%s'", name);
gotoout_delete_net;
}
... ...
}
lxc_cgroup_path_create根据传入的容器名,通过mkdir创建容器,然后lxc_cgroup_enter将所clone的任务pid添加至cgroup下的tasks中。
这里需要注意在lxc创建容器之前要已经配置并mount了cgroup。
子进程由clone创建后执行do_start:
static int do_start(void *data)
{
structlxc_handler *handler = data;
... ...
/*Setup the container, ip, names, utsname, ... */
if(lxc_setup(handler->name, handler->conf)) {//子进程非cgroup属性设置,包含ip、名字空间、根文件系统路径、串口等
ERROR("failedto setup the container");
gotoout_warn_father;
}
if(lxc_sync_barrier_parent(handler, LXC_SYNC_CGROUP)) //询问父进程同步设置cgroups,父进程通过上面说的lxc_sync_barrier_child响应子进程
return-1;
... ...
/*after this call, we are in error because this
* ops should not return as it execs */
handler->ops->start(handler,handler->data);//子进程调用start,通过execve启动参数-d后带的应用程序。
out_warn_father:
/*we want the parent to know something went wrong, so any
* value other than what it expects is ok. */
lxc_sync_wake_parent(handler,LXC_SYNC_POST_CONFIGURE);
return-1;
}
至此一个容器创建完成,并和一个cgroup相关联。对cgroup的 Cpu、Cpuset、Cpuacct、Memory、Devices等子系统的控制与管理,就是对容器相应部分的控制与管理。
对于容器外创建的进程如果使用lxc命令并指定容器名,则该进程属于该容器。如果在容器内部创建的进程则继承父进程的命名空间以及cgroup属性,则属于父容器。