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>>