操作系统虚拟化底层基础之命名空间(namespace)

来源:互联网 发布:舔女生尿道口 知乎 编辑:程序博客网 时间:2024/05/17 01:26

操作系统虚拟化底层基础之命名空间(namespace)

 

黎润(yijunzhu@qq.com)

  

(sina不支持自动word转化,请感兴趣同学从链接下载虚拟化之命名空间)

 

背景

随着公司业务的迅猛发展,大量的机器在线上业务号召下投入了服务于广大网民的神圣职责。不过基于一个不完全统计,我们公司的线上机器平均利用率20%左右,这就意味着70%左右的机器都是可回收或者复用的。

 

出于节约机器,统一管理以及在线迁移的初衷,我们进行了虚拟化计算的研究。经过选型测试以及具体应用场景的研究,我们选择了操作系统虚拟化技术,即LXC。(为什么选择LXC,OpenVZ如何? Xen效果如何等等这些问题请参考其他文档,本文主要讨论LXC的底层实现技术)。LXC本身不是一个具体的技术,它是一个集合技术的代称,我们可以总体上来看,LXC主要有namespace和cgroup两大模块构建而成,本系列主要就是说说这两个技术,本文则专注于namespace。

 

在我们讲述具体的技术之前,先来看看容器模块的整个状态系统,目前主要是IBM,google等公司的团队在负责维护更新。

 

 

 

目前container已经被上有内核所接纳,所以不存在自己维护分支版本的问题。但是这些团队之间合作不是我们想象的和谐,不同利益集团之间是有内核的政治诉求,都想把自家的内容扶位正房,导致我们再看操作系统虚拟化的时候会有不同项目博弈的事迹。

 

 

总览

每一个进程其所包含的命名空间都被抽象层一个nsproxy指针,共享同一个命名空间的进程指向同一个指针,指针的结构通过引用计数(count)来确定使用者数目。当一个进程其所处的用户空间发生变化的时候就发生分裂。通过复制一份老的命名空间数据结构,然后做一些简单的修改,接着赋值给相应的进程。

 

 

看了上面的数据结构,我们就会基本明白,命名空间本身只是一个框架,需要其他实行虚拟化的子系统实现自己的命名空间。这些子系统的对象就不再是全局维护的一份结构了,而是和进程的用户空间数目一致,每一个命名空间都会有对象的一个具体实例。目前Linux系统实现的命名空间子系统主要有UTS、IPC、MNT、PID以及NET网络子模块。我们在下文会针对这些子模块进行进一步的分析。

UTS命名空间子模块

UTS相对而言是一个简单的扁平化命名空间子模块,其不同的命名空间之间没有层次关系。我们先来看一下UTS的数据结构。

 

New_utename结构里面就是我们通过uname –a能够看到的信息。看一下机器上的输出:

 

我通过红色斜线把uname –a的输出分隔开,分别对应上面的new_utsname的结构体。另外内核还把这些信息也通过proc文件系统导出,我们可以通过/proc/sys/kernel目录里面的如下等变量(Ostype/ hostname/osrelease/ version)查看,当然这些变量的值也是可以更改的。

初始的时候,系统默认构造了一个UTS结构,他的值分别如下所述。

 

         当一个新的命名空间创建的时候,copy_utsname会被调用来创建一个UTS的命名空间,主要工作在clone_uts_ns函数里面完成。

 

上面讲述了UTS的代码表示,我们再来只管看一下UTS Namespace和Kref配合使用的场景。

 

 

 

 

 

  上述顺序描述了ustname在容器里面的局部化以及和引用计数配合完成的对象生命周期管理。

IPC命名空间子模块

IPC作为一个常见的进程间通信工具,命名空间对他也进行了部分支持。另外IPC也是一个较为简单的扁平化进程间通信工具,命名空间之间不存在层级。

 

上面罗列的主要是IPC 命名空间里面包含的元素,各个命名空间之间的关系是并列的。

 

IPC Namespace

IPC Namespace

IPC Namespace

……

NSProxy

NSProxy

……

 

 

 

 

 

 


我们直观的给一个图描述资源隔离使用概念图。

 

属于不同命名空间的进程之间是不能访问对方的全局资源的,这儿展示的主要是IPC的SHM,MSG以及SEM,在较新的代码里MQueue也可以被隔离。

MNT命名空间子模块

         虚拟机的一个核心功能就是完成应用的隔离,即业务之间相互不可见。这一块主要通过文件系统的视图来完成,进程创建的时候,每一个进程都有自己的文件挂节点信息。看一下经典的struct task_struct.

在一个系统启动的时候,0号进程就设置好了自己所在的根目录以及当前目录。在创建子进程的时候,通过CLONE_FS来指明父子之间的共享信息,如果设置了两者共享同一个结构(指针加上引用计数),没有设置标记的话,子进程创建一个新的拷贝,两者之间互不影响。如果设置了CLONE_FS,接下来通过chroot(2),chdir(2), or umask(2)的调用结果两者之间会相互影响,反之两者是独立的。

     下面这张图清晰明了的刻画了进程内部的文件系统信息以及文件描述符的位置,同时还可以看到一个文件的主要组成部分。

 

通过文字以及代码描述还是比较枯燥而且不方便直观,下面我们通过图形的方式来看一下进程的文件系统映射情况。

 

最初我们在系统(system)目录里面创建了一个container目录,然后在这个目录里面为每一个虚拟机创建了独立的目录,例如1和2(本例)。在目录1和2里面分别创建相应虚拟机的根目录文件系统。这样虚拟机启动的时候,我们chroot到1或者2里面,看到的文件系统试图就如下所示。

 

在这种使用方式下,虚拟机1和虚拟机2之间的文件系统是互不可见的,而且虚拟机也看不到除了根目录之外的其他文件目录。为了和系统或者其他虚拟机部分共享文件,我们可以映射特定目录到虚拟机的根文件系统,达到部分隔离以及共享的效果,下图。

 

PID命名空间子模块

         PID是虚拟化命名空间里面较复杂的模块,因为前面的命名空间基本都是扁平的,没有层次结构。但是PID命名空间是有层次的,在高层次命名空间能够看到所有的低层次命名空间信息,反之则不行。

         先直观来看看层次化的命名空间结构以及进程的数字变化。需要指出的是,对于命名空间里面的进程,我们看到好像有多个,其实是一一对应的,即进程只有一个,但是在不同的命名空间里面有不同的数据表示,获取一个进程信息需要进程号加上空间信息才能唯一确定一个进程。

 

看完了PID命名空间的组织后,我们来看看他的代码实现。

struct pid_namespace {

         struct kref kref;

         struct pidmap pidmap[PIDMAP_ENTRIES];

         int last_pid;

         struct task_struct *child_reaper;

         struct kmem_cache *pid_cachep;

         unsigned int level;

         struct pid_namespace *parent;

    ……

};

上图里面重要的一些字段通过红色标注了出来,child_reaper指向的进程作用相当于全局命名空间的init进程,其中一个目的是对孤儿进程进行回收。Level则表明自己所处的命名空间在系统命名空间里面的深度,这是一个重要的标记,因为层次高的命名空间可以看到低级别的所有信息。系统的命名空间从0开始技术,然后累加。命名空间的层次结构通过parent来关联。

了解完PID命名空间的信息后,我们再来看看PID为了支持命名空间所需要做的修改。以前的PID命名空间是全局唯一的,现在则必须是命名空间局部化,有一个可见的命名空间就必须有一个PID变量。

         来看看PID的内核表示,系统对于每一个PID都有一个PID结构体来表示,但是在每一个命名空间里面的upid表示具体的数值。    上面的PID就是我们在系统中内核的表示,一个PID可能对应多个task_struct,所以在上面的表示里面通过一个task数组来表示。接着numbers数字分别表示在不同命名空间里面可以看到的pid数值,因为numbers在最后一个位置,所有本质上来说相当于一个指针,增加命名空间的时候,再增加一个numbers即可。

 

         上述的upid则是具体的命名空间内数值表示,nr表示数字,ns则指向关联的命名空间。当然系统的所有upid通过pid_chain挂在同一个全局链表里。

 

 

这张表格和上图一起结合起来我们理解PID的管理结构。一个task_struct通过pid_link的hlist_node挂接到struct pid的链表上面去。同时task_struct又是用过pid_link找到pid,通过pid遍历tasks链表又能够得到所有的任务,当然也可以读取numbers数字获取每一个命名空间里面的数字信息。

为了在pid和upid之间转换,系统提供了很多内部转换接口,我们首先来了解一些基本指导性原则。

..._nr()

通过nr结尾的函数就是获取以前所谓的全局PID,这个全局PID和我们在以前系统里面所见的PID是一致的。例如pid_nr(pid)就返回给定pid的全局PID数值。这些数值往往只有在本机有效,例如一些通过PID获取进程结构的代码。但是在这种情况下,保存pid结构往往比全局PID更有意义,因为全局PID不能随意迁移。

 

..._vnr()

Vnr结尾的函数主要和局部pid打交道,例如一个进程可见的局部ID。来看一个例子,task_pid_vnr(tsk)就返回它能够看到任务PID。需要注意的是,这个数字仅仅在本命名空间内有效。

 

..._nr_ns()

以nr_ns结尾的函数能够获取到特定命名空间课间的PID数值,如果你想得到一些任务的PID数值,你就可以通过task_pid_nr_ns(tsk, current->nsproxy->pid_ns)调用得到数字,接着通过find_task_by_pid_ns(pid, current->nsproxy->pid_ns)反过来找到任务结构。当一个用户请求过来的时候,基本上都是调用这组函数,因为这种情况下一个任务可能需要得到另外一个命名空间的信息。

 

NET命名空间子模块

NET的命名空间隔离做的工作相对而言是最多的,但是整体思路还是一致的,即把全局的资源局部化,在每一个命名空间里面保留自己的控制信息。例如在目前的内核里面路由表,arp表,netfilter表,设备等等都已经空间化了,其所做的后续操作都要首先关联到特定的命名空间,然后再取出里面的数据进行后面的分析。

 

首先来看看net命名空间的结构体

struct net {

         atomic_t           count;

#ifdef NETNS_REFCNT_DEBUG

         atomic_t           use_count;      

#endif

         struct list_head        list;           

         struct work_struct  work;       

         struct proc_dir_entry     *proc_net;

         struct proc_dir_entry     *proc_net_stat;

#ifdef CONFIG_SYSCTL

         struct ctl_table_set         sysctls;

#endif

         struct net_device       *loopback_dev;         

         struct list_head      dev_base_head;

         struct hlist_head    *dev_name_head;

         struct hlist_head     *dev_index_head;

        

         struct list_head        rules_ops;

         spinlock_t                  rules_mod_lock;

         struct sock               *rtnl;                          

 

         struct netns_core    core;

         struct netns_mib     mib;

         struct netns_packet         packet;

         struct netns_unix    unx;

         struct netns_ipv4    ipv4;

#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)

         struct netns_ipv6    ipv6;

#endif

#if defined(CONFIG_IP_DCCP) || defined(CONFIG_IP_DCCP_MODULE)

         struct netns_dccp   dccp;

#endif

#ifdef CONFIG_NETFILTER

         struct netns_xt                  xt;

#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)

         struct netns_ct                  ct;

#endif

#endif

#ifdef CONFIG_XFRM

         struct netns_xfrm   xfrm;

#endif

         struct net_generic  *gen;

};

上面这个结构实在较大,您在需要深入了解的时候慢慢了解每个子模块吧。在一个命名空间创建的时候,会做一些初始化。所有系统定义了一个回调函数,让感兴趣的模块注册。结构如下:

注册接口如下

      一个新的用户空间被创建的时候,注册模块的init结构被创建。同理,一个空间销毁的时候,exit函数也会被调用。

那么现在有了很多命名空间,而且命名空间之间是隔离的,那么他们之间怎么通信呢。这儿需要注意的是引入了一个新的概念,就做网络设备对。一个设备对即A设备接收到的时间自动发送到B设备,反之亦然。

我们先来从直观上看一下网络概念图。

 

接下来的问题就是如何通信?其实可以通过二层或者三层来实现网络转发,本质上就是通过桥接还是路由。我们下面以桥接为例来说明数据报文是如何转发的。

对于容器1来说,veth1需要配置一个IP地址,但是veth0和eth0配置在同一个桥接设备上。Veth0和veth1是网络设备对。

    是一个实际网络环境里面的虚拟化配置,veth0和eth0是通过桥接来完成转发,但是veth0和veth1之间是通过设备对来完成数据转发,其他概念都没有太多变化,除了增加一个独立的命名空间,这儿我们来看看网络设备对是如何工作的。

创建过程不再讨论,就是每次创建一对,A和B的对端分别指向彼此。

创建完了后,彼此关联。

我们还是来看看数据通道,他们的数据是如何透传的。

     过eth_type_trans替换设备指针,接着就通过netif_rx送上起,设备已经属于一个特定的命名空间了,接着就在特定的命名空间里面完成这个报文的应用层处理。但是不管怎么样,通过这个小函数我们就能够轻松的把一个报文从一个命名空间发送到另外一个命名空间里面。

 

总结

终于把内核的命名空间大致模块都看了一遍,其实我们只要在心理面把握住重要的一点就可以了。所有虚拟化的资源,在获取资源的时候,必须首先通过nsproxy获取到合适的命名空间,然后再进行接下来的操作。另外命名空间虽然用户空间程序不是很多,但是在内核里面很早就引入了,而且一堆人活跃的维护着。

 

 

0 0
原创粉丝点击