简单的对linux container 技术做一个总结

linux中称谓的container在内核层面由两个独立的机制保证,一个保证资源的隔离性,名为namespace;一个进行资源的控制,名为cgroup。

 

 

1、先说一下namespace

Namespace是对全局系统资源的一种封装隔离,使得处于不同namespace的进程拥有独立的全局系统资源,改变一个namespace中的系统资源只会影响当前namespace里的进程,对其他namespace中的进程没有影响。

 

inux现有的namespace有6种: uts, pid, ipc, mnt, net和user 。可以通过命令 ls /proc/[pid]/ns 看到pid下ns状况

 

.1 uts

    最简单的主机名,一个namespace结构绑定这样一个字符串,uname的时候去current->nsproxy->uts_namespace下面取字符串就好了

1.2 ipc

ipc维护的是一个id到struct的映射关系,这种映射关系在内核由公用设施idr提供。所谓ipc的namespace就是每个namespace都有自己独立的idr,即独立的key->value的映射集合,不同的namespace通过key找value会在不同的idr中寻找,内核数据结构是公共可见的,隔离的仅仅是索引关系。

1.3 mnt

每个mnt_namespace有自己独立的vfsmount *root, 即根挂载点是互相独立的,同时由vfsmount->mnt_child串接起来的子mnt链表,以及继续往下都是彼此独立的,产生的外在效果就是某个mnt_namespace中的mount, umount不会 对其他namespace产生影响,因为整个mount树是每个namespace各有一份,彼此间无干扰, path lookup也在各自的mount树中进行。这里和chroot之类的又不一样,chroot改变的只是 task_struct相关的fs_struct中的root,影响的是path lookup的起始点,对整个mount树并无关系.

1.4 user

    user_namespace的实现方式和ipc类似,每个namespace各自维护一个uid到user_struct的映射,用hash表实现。但是uid会在两个地方体现,一个是user_struct中的uid,还有一个是cred中的uid。user_namespace影响范围局限在user_struct中,虽然clone(NEWUSER)时会把task_struct的cred中的uid,gid都设成0,然后这种关系又可以通过fork等传递下去,但是终究user_namespace并没有影响到cred里面数据,而且vfs里面inode是只有uid的,

不会有user_struct信息,因此某个具体的文件其uid是固定的,具体在某个namespace中如何显示用户名则不关内核层的事了,由/etc/passwd中定义的映射关系决定。

    另外还有个问题,比如跨两个namespace的unix套接字通信,有个选项叫PEERCRED,是拿对方节点的ucred结构,因为不同namespace,因此拿到的uid,gid都要进行user_namespace的重映射。这里重映射的策略就是:

    1)同user_namespace,OK。不需要

2)不同,则由于每个user_namespace都会记录创建自己的那个user_struct,因此一层层往上索引到init_user_ns,如果发现需要remap的那个user_struct是我们的祖先创建者,则map为0,否则则返回一个表示不存在的MAGIC NUMBER

 

1.5 pid

pid_namespace是每个namespace有一个单独的pid_map作为bitmap进行pid的分配,因此各个pid namespace的pid互不干扰,独立分配。同一个task_struct会从init_ns开始,到最终它所在的namespace,每一层都会有个单独的pid(也就是深层次的task_struct创建,在每一个层次的namespace都会进行pid的alloc),而所有这些pid信息都是栈式保存在struct pid结构中。

pid是唯一一个底层namespace会把上层namespace信息都保留下来的namespace, pid信息保存在struct pid中,而具体的(pid, ns)信息对则保存在upid中,pid会根据自己的namespace深度扩展一个upid的栈,在这个pid结构中,该task_struct从init_ns到实际所处的namespace整个树路径上的(pid,ns)信息都记录了,于是上面所说的跨namesapce unix socket通信取PEERCRED对pid的remap就很简单了,有父子关系,直接从pid的不同深度取另一个值就行了;如果没父子关系,则MAGIC NUMBER。

同时pid的upid栈中每个upid都会连入对应pid namespace的hash表中,那么该task_struct在每个namespace层次中都是可见的(可在ns对应hash表中找到),且pid号互不相关(看到的是对应栈层次上的upid->nr)。

由于历史因素,task_struct中除了用pid索引pid, ppid, pgid,sid,结构体本身还有pid,tgid等,这里的数据都是 取的init_ns中的对应数值.

 

1.6 net

net_ns是非常复杂的一块。mainline都做了几个版本来稳定它,主要是各个关键数据结构都加上了net信息,比如sock, 路由查找的fib, netfilter的rules,net_device等, net对于不同net的数据隔离和前面几种每个namespace自己建索引表不同,net的一般都在同一个索引表中,只是数据多加一维net信息,在查询时多加上对这一维的比较。相应的网络层关键处理函数都会多带一个net_namespace的参数. 在net_namespace结构体内部,主要保存了网络各层关键的sysctl信息,用于实现对不同net namespace可以进行不同的内核参数配置,以及不同的proc entry导出。

 

 

那namespace 是如何创建的呢

 

IPC CLONE_NEWIPC System V IPC, POSIX message queues (since Linux 2.6.19) Network CLONE_NEWNET Network devices, stacks, ports, etc. (since Linux 2.6.24) Mount CLONE_NEWNS Mount points (since Linux 2.4.19) PID CLONE_NEWPID Process IDs (since Linux 2.6.24) User CLONE_NEWUSER User and group IDs (started in Linux 2.6.23 and completed in Linux 3.8) UTS CLONE_NEWUTS Hostname and NIS domain name (since Linux 2.6.19)

 

pid = clone(fn, stack + stack_size, clone_flags | SIGCHLD, arg);

 

2、什么事cgroup

 

cgroup是Linux下的一种将进程按组进行管理的机制,在用户层看来,cgroup技术就是把系统中的所有进程组织成一颗一颗独立的树,每棵树都包含系统的所有进程,树的每个节点是一个进程组,而每颗树又和一个或者多个subsystem关联,树的作用是将进程分组,而subsystem的作用就是对这些组进行操作。cgroup主要包括下面两部分:

2.1 subsystem 一个subsystem就是一个内核模块,他被关联到一颗cgroup树之后,就会在树的每个节点(进程组)上做具体的操作。subsystem经常被称作"resource controller",因为它主要被用来调度或者限制每个进程组的资源,但是这个说法不完全准确,因为有时我们将进程分组只是为了做一些监控,观察一下他们的状态,比如perf_event subsystem。到目前为止,Linux支持12种subsystem,比如限制CPU的使用时间,限制使用的内存,统计CPU的使用情况,冻结和恢复一组进程等,后续会对它们一一进行介绍。

 

2.2 hierarchy 一个hierarchy可以理解为一棵cgroup树,树的每个节点就是一个进程组,每棵树都会与零到多个subsystem关联。在一颗树里面,会包含Linux系统中的所有进程,但每个进程只能属于一个节点(进程组)。系统中可以有很多颗cgroup树,每棵树都和不同的subsystem关联,一个进程可以属于多颗树,即一个进程可以属于多个进程组,只是这些进程组和不同的subsystem关联。目前Linux支持12种subsystem,如果不考虑不与任何subsystem关联的情况(systemd就属于这种情况),Linux里面最多可以建12颗cgroup树,每棵树关联一个subsystem,当然也可以只建一棵树,然后让这棵树关联所有的subsystem。当一颗cgroup树不和任何subsystem关联的时候,意味着这棵树只是将进程进行分组,至于要在分组的基础上做些什么,将由应用程序自己决定,systemd就是一个这样的例子。

 

 

那么cgroup 是如何使用的呢

cgroup相关的所有操作都是基于内核中的cgroup virtual filesystem,使用cgroup很简单,挂载这个文件系统就可以了。一般情况下都是挂载到/sys/fs/cgroup目录下

 

 

xxx@ubuntu:~$ mount | grep cgroup

tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)

cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd)

cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)

cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)

cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)

cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)

cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)

cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)

cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)

cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)

cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)

cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)

 

 

 

  • 挂载一颗和所有subsystem关联的cgroup树到/sys/fs/cgroup

mount -t cgroup xxx /sys/fs/cgroup

  • 挂载一颗和cpuset subsystem关联的cgroup树到/sys/fs/cgroup/cpuset

mkdir /sys/fs/cgroup/cpuset mount -t cgroup -o cpuset xxx /sys/fs/cgroup/cpuset

  • 挂载一颗与cpu和cpuacct subsystem关联的cgroup树到/sys/fs/cgroup/cpu,cpuacct

mkdir /sys/fs/cgroup/cpu,cpuacct mount -t cgroup -o cpu,cpuacct xxx /sys/fs/cgroup/cpu,cpuacct

  • 挂载一棵cgroup树,但不关联任何subsystem,下面就是systemd所用到的方式

mkdir /sys/fs/cgroup/systemd mount -t cgroup -o none,name=systemd xxx /sys/fs/cgroup/systemd

 

 

 

现在我们知道namespace 和 cgroup ,如何用实现一个container 系统呢?

 

int cgroup_create(name ,pid)

{

……

/*在相应的cgroup[cpu memory 等] 下写入task*/

mkdir("/sys/fs/cpu/name")

 

/*把相应的pid 写入到新task文件里*/

write_to_file(/sys/fs/cpu/name,pid )

……

}

 

int container_create(char *name)

{

size_t stack_size = sysconf(_SC_PAGESIZE);

void *stack = alloca(stack_size);

pid_t pid;

 

int clone_flags = CLONE_NEWPID | CLONE_NEWNS;

clone_flags |= CLONE_NEWIPC;

clone_flags |= CLONE_NEWUTS;

clone_flags |= CLONE_NEWNET;

/*创建一个新的 namespace*/

pid = clone(fn, stack + stack_size, flags | SIGCHLD, arg);

 

cgroup_create(name ,pid);

 

}

 

可以通过命令查看

xxx@ubuntu:~$ ls /sys/fs/cgroup/cpu/docker/tasks

/sys/fs/cgroup/cpu/docker/tasks

这样我们就把我们创建的namespace,通过cgroup 管理起来

 

这样我们就创建了一个namespace ,并且通过cgroup 实现对这个namespace的控制,实现了一个简单container

 

上面的操作也可以通过安装libcgroup 包,通过命令实现。

发布了18 篇原创文章 · 获赞 3 · 访问量 3944
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览