歡迎您光臨本站 註冊首頁

操作系統虛擬化底層基礎之命名空間(namespace)

←手機掃碼閱讀     火星人 @ 2014-03-04 , reply:0

操作系統虛擬化底層基礎之命名空間(namespace)

:lol:

操作系統虛擬化底層基礎之命名空間(namespace)

黎潤(yijunzhu@qq.com)


背景
隨著公司業務的迅猛發展,大量的機器在線上業務號召下投入了服務於廣大網民的神聖職責。不過基於一個不完全統計,我們公司的線上機器平均利用率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的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;
        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;/* To decided when the network namespace should be freed. */
#ifdef NETNS_REFCNT_DEBUG
        atomic_t                use_count;        /* To track references we destroy on demand*/
#endif
        struct list_head        list;                /* list of network namespaces */
        struct work_struct        work;        /* work struct for freeing */
        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;          /* The loopback */
        struct list_head         dev_base_head;
        struct hlist_head         *dev_name_head;
        struct hlist_head        *dev_index_head;
        /* core fib_rules */
        struct list_head        rules_ops;
        spinlock_t                rules_mod_lock;
        struct sock                 *rtnl;                        /* rtnetlink socket */

        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獲取到合適的命名空間,然後再進行接下來的操作。另外命名空間雖然用戶空間程序不是很多,但是在內核裡面很早就引入了,而且一堆人活躍的維護著。
《解決方案》

LXC 主頁: http://lxc.sourceforge.net/

不知道和OPENVZ比有啥區別? 和XEN就不用比了,本來就是不同的東西。。。
《解決方案》

回復 2# accessory


    根據openvz一個技術負責人的觀點是:
1.LXC充分利用了openvz的成果
2.openvz還會維護一段時間(最新一個開發分支基於2.6.32)
3.LXC已經被合併入內核主線版本,而openvz則在苦苦支撐
4.現在IBM以及google等大公司都在LXC方面做了很多努力
5.目前LXC功能還沒有openvz多和穩定,但是長期來看,LXC可能成為主流
《解決方案》

了解。多謝。。。

第三點是關鍵。。。
《解決方案》

所有虛擬化的資源,在獲取資源的時候,必須首先通過nsproxy獲取到合適的命名空間,然後再進行接下來的操作。

[火星人 ] 操作系統虛擬化底層基礎之命名空間(namespace)已經有791次圍觀

http://coctec.com/docs/service/show-post-5239.html