《自己動手寫Docker》書摘之一: Linux Namespace__Linux

來源:互聯網
上載者:User
Linux Namespace 介紹

我們經常聽到說Docker 是一個使用了Linux Namespace 和 Cgroups 的虛擬化工具,但是什麼是Linux Namespace 它在Docker內是怎麼被使用的,說到這裡很多人就會迷茫,下面我們就先介紹一下Linux Namespace 以及它們是如何在容器裡面使用的。 概念

Linux Namespace 是kernel 的一個功能,它可以隔離一系列系統的資源,比如PID(Process ID),User ID, Network等等。一般看到這裡,很多人會想到一個命令chroot,就像chroot允許把目前的目錄變成根目錄一樣(被隔離開來的),Namesapce也可以在一些資源上,將進程隔離起來,這些資源套件括進程樹,網路介面,掛載點等等。

比如一家公司向外界出售自己的計算資源。公司有一台效能還不錯的伺服器,每個使用者買到一個tomcat執行個體用來運行它們自己的應用。有些調皮的客戶可能不小心進入了別人的tomcat執行個體,修改或者關閉了其中的某些資源,這樣就會導致各個客戶之間互相干擾。也許你會說,我們可以限制不同使用者的許可權,讓使用者只能訪問自己名下的tomcat,但是有些操作可能需要系統層級的許可權,比如root。我們不可能給每個使用者都授予root許可權,也不可能給每個使用者都提供一台全新的物理主機讓他們互相隔離,因此這裡Linux Namespace就派上了用場。使用Namespace, 我們就可以做到UID層級的隔離,也就是說,我們可以以UID為n的使用者,虛擬化出來一個namespace,在這個namespace裡面,使用者是具有root許可權的。但是在真實的物理機器上,他還是那個UID為n的使用者,這樣就解決了使用者之間隔離的問題。當然這個只是Namespace其中一個簡單的功能。

除了User Namespace ,PID也是可以被虛擬。命名空間建立系統的不同視圖, 對於每一個命名空間,從使用者看起來,應該像一台單獨的Linux電腦一樣,有自己的init進程(PID為1),其他進程的PID依次遞增,A和B空間都有PID為1的init進程,子容器的進程映射到父容器的進程上,父容器可以知道每一個子容器的運行狀態,而子容器與子容器之間是隔離的。從圖中我們可以看到,進程3在父命名空間裡面PID 為3,但是在子命名空間內,他就是1.也就是說使用者從子命名空間 A 內看進程3就像 init 進程一樣,以為這個進程是自己的初始化進程,但是從整個 host 來看,他其實只是3號進程虛擬化出來的一個空間而已。

當前Linux一共實現六種不同類型的namespace。

Namespace類型 系統調用參數 核心版本
Mount namespaces CLONE_NEWNS 2.4.19
UTS namespaces CLONE_NEWUTS 2.6.19
IPC namespaces CLONE_NEWIPC 2.6.19
PID namespaces CLONE_NEWPID 2.6.24
Network namespaces CLONE_NEWNET 2.6.29
User namespaces CLONE_NEWUSER 3.8

Namesapce 的API主要使用三個系統調用 clone() - 建立新進程。根據系統調用參數來判斷哪種類型的namespace被建立,而且它們的子進程也會被包含到namespace中 unshare() - 將進程移出某個namespace setns() - 將進程加入到namesp中 UTS Namespace

UTS namespace 主要隔離nodename和domainname兩個系統標識。在UTS namespace裡面,每個 namespace 允許有自己的hostname。

下面我們將使用Go來做一個UTS Namespace 的例子。其實對於 Namespace 這種系統調用,使用 C 語言來描述是最好的,但是本書的目的是去實現 docker,由於 docker 就是使用 Go 開發的,那麼我們就整體使用 Go 來講解。先來看一下代碼,非常簡單:

package mainimport (    "os/exec"    "syscall"    "os"    "log")func main() {    cmd := exec.Command("sh")    cmd.SysProcAttr = &syscall.SysProcAttr{        Cloneflags: syscall.CLONE_NEWUTS,    }    cmd.Stdin = os.Stdin    cmd.Stdout = os.Stdout    cmd.Stderr = os.Stderr    if err := cmd.Run(); err != nil {        log.Fatal(err)    }}

解釋一下代碼,exec.Command('sh') 是去指定當前命令的執行環境,我們預設使用sh來執行。下面的就是設定系統調用參數,像我們前面講到的一樣,使用CLONE_NEWUTS這個標識符去建立一個UTS Namespace。Go幫我們封裝了對於clone()函數的調用,這個代碼執行後就會進入到一個sh 運行環境中。

我們在ubuntu 14.04上運行這個程式,kernel版本3.13.0-65-generic,go 版本1.7.3,執行go run main.go,我們在這個互動式環境裡面使用pstree -pl查看一下系統中進程之間的關係

|-sshd(19820)---bash(19839)---go(19901)-+-main(19912)-+-sh(19915)---    pstree(19916)   

然後我們輸出一下當前的 PID

# echo $$19915

驗證一下我們的父進程和子進程是否不在同一個UTS namespace

# readlink /proc/19912/ns/utsuts:[4026531838]# readlink /proc/19915/ns/utsuts:[4026532193]

可以看到他們確實不在同一個UTS namespace。由於UTS Namespace是對hostname做了隔離,那麼我們在這個環境內修改hostname應該不影響外部主機,下面我們來做一下實驗。

在這個sh環境內執行

修改hostname 為bird然後列印出來 # hostname -b bird# hostnamebird    

我們另外啟動一個shell在宿主機上運行一下hostname看一下效果

root@iZ254rt8xf1Z:~# hostnameiZ254rt8xf1Z

可以看到外部的 hostname 並沒有被內部的修改所影響,由此就瞭解了UTS Namespace的作用。 IPC Namespace

IPC Namespace 是用來隔離 System V IPC 和POSIX message queues.每一個IPC Namespace都有他們自己的System V IPC 和POSIX message queue。

我們在上一版本的基礎上稍微改動了一下代碼

package mainimport (    "log"    "os"    "os/exec"    "syscall")func main() {    cmd := exec.Command("sh")    cmd.SysProcAttr = &syscall.SysProcAttr{        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC,    }    cmd.Stdin = os.Stdin    cmd.Stdout = os.Stdout    cmd.Stderr = os.Stderr    if err := cmd.Run(); err != nil {        log.Fatal(err)    }}

可以看到我們僅僅增加syscall.CLONE_NEWIPC代表我們希望建立IPC Namespace。下面我們需要開啟兩個shell 來示範隔離的效果。

首先在宿主機上開啟一個 shell

查看現有的ipc Message Queuesroot@iZ254rt8xf1Z:~# ipcs -q------ Message Queues --------key        msqid      owner      perms      used-bytes   messages下面我們建立一個message queueroot@iZ254rt8xf1Z:~# ipcmk -QMessage queue id: 0然後再查看一下 root@iZ254rt8xf1Z:~# ipcs -q------ Message Queues --------key        msqid      owner      perms      used-bytes   messages0x5e8f3f1e 0          root       644        0            0

這裡我們發現是可以看到一個queue了。下面我們使用另外一個shell去運行我們的程式。

root@iZ254rt8xf1Z:~/gocode/src/book# go run main.go# ipcs -q------ Message Queues --------key        msqid      owner      perms      used-bytes   messages

通過這裡我們可以發現,在新建立的Namespace裡面,我們看不到宿主機上已經建立的message queue,說明我們的 IPC Namespace 建立成功,IPC 已經被隔離。 PID Namesapce

PID namespace是用來隔離進程 id。同樣的一個進程在不同的 PID Namespace 裡面可以擁有不同的 PID。這樣就可以理解,在 docker container 裡面,我們使用ps -ef 經常能發現,容器內在前台跑著的那個進程的 PID 是1,但是我們在容器外,使用ps -ef會發現同樣的進程卻有不同的 PID,這就是PID namespace 乾的事情。

再前面的代碼基礎之上,我們再修改一下代碼,添加了一個syscall.CLONE_NEWPID

package mainimport (    "log"    "os"    "os/exec"    "syscall")func main() {    cmd := exec.Command("sh")    cmd.SysProcAttr = &syscall.SysProcAttr{        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID,    }    cmd.Stdin = os.Stdin    cmd.Stdout = os.Stdout    cmd.Stderr = os.Stderr    if err := cmd.Run(); err != nil {        log.Fatal(err)    }}

我們需要開啟兩個 shell,首先我們在宿主機上看一下進程樹,找一下我們的進程的真實 PID

root@iZ254rt8xf1Z:~# pstree -pl |-sshd(894)-+-sshd(9455)---bash(9475)---bash(19619)    |           |-sshd(19715)---bash(19734)    |           |-sshd(19853)---bash(19872)---go(20179)-+-main(20190)-+-sh(20193)    |           |                                       |             |-{main}(20191)    |           |                                       |             `-{main}(20192)    |           |                                       |-{go}(20180)    |           |                                       |-{go}(20181)    |           |                                       |-{go}(20182)    |           |                                       `-{go}(20186)    |           `-sshd(20124)---bash(20144)---pstree(20196)

可以看到,我們的go main 函數啟動並執行pid為 20190。下面我們開啟另外一個 shell 運行一下我們的代碼

root@iZ254rt8xf1Z:~/gocode/src/book# go run main.go# echo $$1

可以看到,我們列印了當前namespace的pid,發現是1,也就是說。這個20190 PID 被映射到 namesapce 裡面的 PID 為1.這裡還不能使用ps 來查看,因為ps 和 top 等命令會使用/proc內容,我們會在下面的mount namesapce講解。 Mount Namespace

mount namespace 是用來隔離各個進程看到的掛載點視圖。在不同namespace中的進程看到的檔案系統層次是不一樣的。在mount namespace 中調用mount()和umount()僅僅只會影響當前namespace內的檔案系統,而對全域的檔案系統是沒有影響的。

看到這裡,也許就會想到chroot()。它也是將某一個子目錄變成根節點。但是mount namespace不僅能實現這個功能,而且能以更加靈活和安全的方式實現。

mount namespace是Linux 第一個實現的namesapce 類型,因此它的系統調用參數是NEWNS(new namespace 的縮寫)。貌似當時人們沒有意識到,以後還會有很多類型的namespace加入Linux大家庭。

我們針對上面的代碼做了一點改動,增加了NEWNS 標識。

package mainimport (    "log"    "os"    "os/exec"    "syscall")func main() {    cmd := exec.Command("sh")    cmd.SysProcAttr = &syscall.SysProcAttr{        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,    }    cmd.Stdin = os.Stdin    cmd.Stdout = os.Stdout    cmd.Stderr = os.Stderr    if err := cmd.Run(); err != nil {        log.Fatal(err)    }}

首先我們運行代碼後,查看一下/proc的檔案內容。proc 是一個檔案系統,它提供額外的機制可以從核心和核心模組將資訊發送給進程。

# ls /proc1     14     19872  23  34   43  55   739  865        bus      filesystems  kpagecount     pagetypeinfo   sysvipc10    145    2      24  348  44  57   75   866        cgroups      fs       kpageflags     partitions     timer_list100   1472   20     25  35   45  58   76   869        cmdline      interrupts   latency_stats  sched_debug    timer_stats11    1475   20124  26  353  47  59   77   894        consoles     iomem    loadavg        schedstat      tty1174  15     20129  27  36   48  6    776  9          cpuinfo      ioports  locks          scsi       uptime1192  154    20144  28  37   49  60   78   937        crypto       ipmi     mdstat         self       version12    155    20215  29  38   5   607  796  945        devices      irq      meminfo        slabinfo       version_signature1255  16     20226  3   39   50  61   8    9460       diskstats    kallsyms misc           softirqs       vmallocinfo1277  17     20229  30  391  51  62   827  967        dma      kcore    modules        stat       vmstat1296  18     20231  31  40   52  63   836  99         driver       key-users    mounts         swaps          xen13    19     21     32  41   53  7    860  acpi       execdomains  keys     mtrr           sys        zoneinfo1309  19853  22     33  42   54  733  862  buddyinfo  fb       kmsg     net        sysrq-trigger

因為這裡的/proc還是宿主機的,所以我們看到裡面會比較亂,下面我們將/proc mount到我們自己的namesapce下面來。

# mount -t proc proc /proc# ls /proc1      consoles   execdomains  ipmi       kpagecount     misc      sched_debug  swaps          uptime5      cpuinfo    fb       irq        kpageflags     modules       schedstat    sys        versionacpi       crypto     filesystems  kallsyms   latency_stats  mounts    scsi     sysrq-trigger  version_signaturebuddyinfo  devices    fs       kcore      loadavg        mtrr      self     sysvipc        vmallocinfobus    diskstats  interrupts   key-users  locks      net       slabinfo timer_list     vmstatcgroups    dma        iomem    keys       mdstat         pagetypeinfo  softirqs timer_stats    xencmdline    driver     ioports      kmsg       meminfo        partitions    stat     tty        zoneinfo

可以看到,瞬間少了好多命令。下面我們就可以使用 ps 來查看系統的進程了。

# ps -efUID        PID  PPID  C STIME TTY          TIME CMDroot         1     0  0 20:15 pts/4    00:00:00 shroot         6     1  0 20:19 pts/4    00:00:00 ps -ef

可以看到,在當前namesapce裡面,我們的sh 進程是PID 為1 的進程。這裡就說明,我們當前的Mount namesapce 裡面的mount 和外部空間是隔離的,mount 操作並沒有影響到外部。Docker volume 也是利用了這個特性。 User Namesapce

User namespace 主要是隔離使用者的使用者組ID。也就是說,一個進程的User ID 和Group ID 在User namespace 內外可以是不同的。比較常用的是,在宿主機上以一個非root使用者運行建立一個User namespace,然後在User namespace裡面卻映射成root 使用者。這樣意味著,這個進程在User namespace裡面有root許可權,但是在User namespace外面卻沒有root的許可權。從Linux kernel 3.8開始,非root進程也可以建立User namespace ,並且此進程在namespace裡面可以被映射成 root並且在 namespace內有root許可權。

下面我們繼續以一個例子來描述.

package mainimport (    "log"    "os"    "os/exec"    "syscall")func main() {    cmd := exec.Command("sh")    cmd.SysProcAttr = &syscall.SysProcAttr{        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |            syscall.CLONE_NEWUSER,    }    cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(1), Gid: uint32(1)}    cmd.Stdin = os.Stdin    cmd.Stdout = os.Stdout    cmd.Stderr = os.Stderr    if err := cmd.Run(); err != nil {        log.Fatal(err)    }    os.Exit(-1)}

我們在原來的基礎上增加了syscall.CLONE_NEWUSER。首先我們以root來運行這個程式,運行前在宿主機上我們看一下目前使用者和使用者組

root@iZ254rt8xf1Z:~/gocode/src/book# iduid=0(root) gid=0(root) groups=0(root)

可以看到我們是root 使用者,我們運行一下程式

root@iZ254rt8xf1Z:~/gocode/src/book# go run main.go$ iduid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
Network Namespace

Network namespace 是用來隔離網路裝置,IP地址連接埠等網路棧的namespace。Network namespace 可以讓每個容器擁有自己獨立的網路裝置(虛擬),而且容器內的應用可以綁定到自己的連接埠,每個 namesapce 內的連接埠都不會互相衝突。在宿主機上搭建橋接器後,就能很方便的實現容器之間的通訊,而且每個容器內的應用都可以使用相同的連接埠。

同樣,我們在原來的代碼上增加一點。我們增加了syscall.CLONE_NEWNET 這裡標識符。

package mainimport (    "log"    "os"    "os/exec"    "syscall")func main() {    cmd := exec.Command("sh")    cmd.SysProcAttr = &syscall.SysProcAttr{        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |            syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET,    }    cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(1), Gid: uint32(1)}    cmd.Stdin = os.Stdin    cmd.Stdout = os.Stdout    cmd.Stderr = os.Stderr    if err := cmd.Run(); err != nil {        log.Fatal(err)    }    os.Exit(-1)}

首先我們在宿主機上查看一下自己的網路裝置。

root@iZ254rt8xf1Z:~/gocode/src/book# ifconfigdocker0   Link encap:Ethernet  HWaddr 02:42:d7:5d:c3:b9          inet addr:192.168.0.1  Bcast:0.0.0.0  Mask:255.255.240.0          UP BROADCAST MULTICAST  MTU:1500  Metric:1          RX packets:0 errors:0 dropped:0 overruns:0 frame:0          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:0          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)eth0      Link encap:Ethernet  HWaddr 00:16:3e:00:38:cc          inet addr:10.170.174.187  Bcast:10.170.175.255  Mask:255.255.248.0          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1          RX packets:5605 errors:0 dropped:0 overruns:0 frame:0          TX packets:1819 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:7129227 (7.1 MB)  TX bytes:159780 (159.7 KB)eth1      Link encap:Ethernet  HWaddr 00:16:3e:00:6d:4d          inet addr:101.200.126.205  Bcast:101.200.127.255  Mask:255.255.252.0          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1          RX packets:15433 errors:0 dropped:0 overruns:0 frame:0          TX packets:6888 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:1000          RX bytes:13287762 (13.2 MB)  TX bytes:1787482 (1.7 MB)lo        Link encap:Local Loopback          inet addr:127.0.0.1  Mask:255.0.0.0          UP LOOPBACK RUNNING  MTU:65536  Metric:1          RX packets:0 errors:0 dropped:0 overruns:0 frame:0          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0          collisions:0 txqueuelen:0          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

可以看到我們宿主機上有lo, eth0, eth1 等網路裝置,下面我們運行一下程式去Network namespce 裡面去看看。

root@iZ254rt8xf1Z:~/gocode/src/book# go run main.go$ ifconfig$

我們發現,在Namespace 裡面什麼網路裝置都沒有。這樣就能展現 Network namespace 與宿主機之間的網路隔離。 小結

本節我們主要介紹了Linux Namespace,一共有六種類別的Namespace, 我們分別簡單的介紹了一下,然後以 Go 語言為例子做了一下 demo,讓大家方便有一個直觀的認識,我們會在後面的章節中使用到這些知識,而且對於這些namespace的應用,後面章節會有更加複雜的例子等待著大家。

相關圖書推薦<<自己動手寫 docker>>

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.