自己動(dòng)手寫docker筆記(1)Linux NameSpace

書中提到,docker最核心的就是通過Linux NameSpace,Cgroups以及Union FS構(gòu)造的。這里首先記錄一下NameSpace。

Linux NameSpace

Linux Namespace 是kernel 的一個(gè)功能,它可以隔離一系列系統(tǒng)的資源,比如PID(Process ID),User ID, Network等等。
類似與chroot允許把當(dāng)前目錄變成根目錄一樣(被隔離開來的),Namesapce也可以在一些資源上,將進(jìn)程隔離起來,這些資源包括進(jìn)程樹,網(wǎng)絡(luò)接口,掛載點(diǎn)等等。
書中提到一個(gè)生動(dòng)的例子。一家公司向外界出售自己的計(jì)算資源,公司有一臺(tái)性能還不錯(cuò)的服務(wù)器,每個(gè)用戶買到一個(gè)tomcat實(shí)例用來運(yùn)行它們自己的應(yīng)用。有些調(diào)皮的客戶可能不小心進(jìn)入了別人的tomcat實(shí)例,修改或者關(guān)閉了其中的某些資源,這樣就會(huì)導(dǎo)致各個(gè)客戶之間互相干擾。
為此,使用Namespace,
我們就可以做到UID級(jí)別的隔離,也就是說,我們可以以UID為n的用戶,虛擬化出來一個(gè)namespace,在這個(gè)namespace里面,用戶是具有root權(quán)限的。但是在真實(shí)的物理機(jī)器上,
他還是那個(gè)UID為n的用戶。這只是NameSpace的一個(gè)子功能。

除了User Namespace ,PID也是可以被虛擬的。命名空間建立系統(tǒng)的不同視圖, 對(duì)于每一個(gè)命名空間,從用戶看起來,應(yīng)該像一臺(tái)單獨(dú)的Linux計(jì)算機(jī)一樣,有自己的init進(jìn)程(PID為1),
其他進(jìn)程的PID依次遞增,A和B空間都有PID為1的init進(jìn)程,子容器的進(jìn)程映射到父容器的進(jìn)程上,父容器可以知道每一個(gè)子容器的運(yùn)行狀態(tài),而子容器與子容器之間是隔離的。從圖中我們可以看到,進(jìn)程3在父命名空間里面PID 為3,但是在子命名空間內(nèi),他就是1.也就是說用戶從子命名空間 A 內(nèi)看進(jìn)程3就像 init 進(jìn)程一樣,以為這個(gè)進(jìn)程是自己的初始化進(jìn)程,但是從整個(gè) host 來看,他其實(shí)只是3號(hào)進(jìn)程虛擬化出來的一個(gè)空間而已。

當(dāng)前Linux一共實(shí)現(xiàn)六種不同類型的namespace。

Namespace類型 系統(tǒng)調(diào)用參數(shù) 內(nèi)核版本
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主要使用三個(gè)系統(tǒng)調(diào)用

  • clone() - 創(chuàng)建新進(jìn)程。根據(jù)系統(tǒng)調(diào)用參數(shù)來判斷哪種類型的namespace被創(chuàng)建,而且它們的子進(jìn)程也會(huì)被包含到namespace中
  • unshare() - 將進(jìn)程移出某個(gè)namespace
  • setns() - 將進(jìn)程加入到namespacef中

UTS NameSpace

下面我們將使用Go來做一個(gè)UTS Namespace 的例子。

package main

import (
    "os/exec"
    "syscall"
    "os"
    "log"
)

func main() {
    cmd := exec.Command("sh")//指定被fork()出來的新進(jìn)程內(nèi)的初始化進(jìn)程
    cmd.SysProcAttr = &syscall.SysProcAttr{
        //已經(jīng)封裝clone,直接進(jìn)行調(diào)用就好
        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)
    }
}
Result1
Result2
Result3

IPC NameSpace

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


添加代碼

可以看到我們僅僅增加syscall.CLONE_NEWIPC代表我們希望創(chuàng)建IPC Namespace。

package main

import (
    "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)
    }
}

下面我們需要打開兩個(gè)shell 來演示隔離的效果。

查看現(xiàn)有的ipc Message Queues

root@taroballs-PC:/home/taroballs/GoglandProjects/awesomeDockerProject/IPC# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

下面我們創(chuàng)建一個(gè)message queue 然后再查看一下

root@taroballs-PC:/home/taroballs/GoglandProjects/awesomeDockerProject/IPC# ipcmk -Q
Message queue id: 0
root@taroballs-PC:/home/taroballs/GoglandProjects/awesomeDockerProject/IPC# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0xb06f1d25 0          root       644        0            0           

發(fā)現(xiàn)是可以看到一個(gè)queue了。下面我們使用另外一個(gè)shell去運(yùn)行我們的程序。


成功隔離

通過這里我們可以發(fā)現(xiàn),在新創(chuàng)建的Namespace里面,我們看不到宿主機(jī)上已經(jīng)創(chuàng)建的message queue,說明我們的 IPC Namespace 創(chuàng)建成功,IPC 已經(jīng)被隔離。

PID NameSpacce

PID namespace是用來隔離進(jìn)程 id。同樣的一個(gè)進(jìn)程在不同的 PID Namespace 里面可以擁有不同的 PID。

可以這樣理解,在 docker container 里面,我們使用ps -ef 發(fā)現(xiàn),容器內(nèi)在前臺(tái)跑著的那個(gè)init進(jìn)程的 PID 是1,但是我們?cè)谌萜魍猓褂?code>ps -ef會(huì)發(fā)現(xiàn)同樣的進(jìn)程卻有不同的 PID,這就是PID namespace 干的事情。
同上,我們添加了一個(gè)syscall.CLONE_NEWPID

package main

import (
    "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)
    }
}

可以看到,我們打印了當(dāng)前namespace的pid,發(fā)現(xiàn)是1,也就是說。這個(gè)20190 PID 被映射到 namesapce 里面的 PID 為1.這里還不能使用ps 來查看,因?yàn)閜s 和 top 等命令會(huì)使用/proc內(nèi)容

Mount NameSpace

上一小節(jié)講到,暫時(shí)不能使用top和ps查看,因?yàn)槠鋾?huì)使用/proc內(nèi)容

  • 何為/proc文件呢

Linux系統(tǒng)上的/proc目錄是一種文件系統(tǒng),即proc文件系統(tǒng)。與其它常見的文件系統(tǒng)不同的是,/proc是一種偽文件系統(tǒng)(也即虛擬文件系統(tǒng)),存儲(chǔ)的是當(dāng)前內(nèi)核運(yùn)行狀態(tài)的一系列特殊文件,用戶可以通過這些文件查看有關(guān)系統(tǒng)硬件及當(dāng)前正在運(yùn)行進(jìn)程的信息,甚至可以通過更改其中某些文件來改變內(nèi)核的運(yùn)行狀態(tài)。
基于/proc文件系統(tǒng)如上所述的特殊性,其內(nèi)的文件也常被稱作虛擬文件

所謂Mount NameSpace隔離,是用來隔離各個(gè)進(jìn)程看到的掛載點(diǎn)視圖。
在不同namespace中的進(jìn)程看到的文件系統(tǒng)層次是不一樣的。在mount namespace 中調(diào)用mount()umount()僅僅只會(huì)影響當(dāng)前namespace內(nèi)的文件系統(tǒng),而對(duì)全局的文件系統(tǒng)是沒有影響的。
看到這里,也許就會(huì)想到chroot()。它也是將某一個(gè)子目錄變成根節(jié)點(diǎn)。但是mount namespace不僅能實(shí)現(xiàn)這個(gè)功能,而且能以更加靈活和安全的方式實(shí)現(xiàn)。

我們針對(duì)上面的代碼做了一點(diǎn)改動(dòng),代碼中增加了NEWNS 標(biāo)識(shí)。


package main

import (
    "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)
    }
}

首先我們運(yùn)行代碼后,查看一下/proc的文件內(nèi)容。proc 是一個(gè)文件系統(tǒng),它提供額外的機(jī)制可以從內(nèi)核和內(nèi)核模塊將信息發(fā)送給進(jìn)程。


下面我們將/proc mount到我們自己的namesapce下面來。


可以看到,在當(dāng)前namesapce里面,我們的sh 進(jìn)程是PID 為1 的進(jìn)程。這里就說明,我們當(dāng)前的Mount namesapce 里面的mount 和外部空間是隔離的,mount 操作并沒有影響到外部。Docker volume 也是利用了這個(gè)特性。

User NameSpace

User namespace 主要是隔離用戶的用戶組ID。也就是說,一個(gè)進(jìn)程的User ID 和Group ID 在User namespace 內(nèi)外可以是不同的。
比較常用的是,在宿主機(jī)上以一個(gè)非root用戶運(yùn)行創(chuàng)建一個(gè)User namespace,然后在User namespace里面卻映射成root 用戶。
這個(gè)進(jìn)程在User namespace里面有root權(quán)限,但是在User namespace外面卻沒有root的權(quán)限。

從Linux kernel 3.8開始,非root進(jìn)程也可以創(chuàng)建User namespace ,并且此進(jìn)程在namespace里面可以被映射成 root并且在 namespace內(nèi)有root權(quán)限。
繼續(xù)改動(dòng)了我們的代碼


package main

import (
    "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)
}

在原來的基礎(chǔ)上增加了syscall.CLONE_NEWUSER。首先我們以root來運(yùn)行這個(gè)程序,運(yùn)行前在宿主機(jī)上我們看一下當(dāng)前用戶和用戶組。

可以看到。它們的UID是不同的,亦即User NameSpace生效了.

NetWork NameSpace

Network namespace 是用來隔離網(wǎng)絡(luò)設(shè)備,IP地址端口等網(wǎng)絡(luò)棧的namespace。Network namespace 可以讓每個(gè)容器擁有自己獨(dú)立的網(wǎng)絡(luò)設(shè)備(虛擬的),而且容器內(nèi)的應(yīng)用可以綁定到自己的端口,每個(gè) namesapce 內(nèi)的端口都不會(huì)互相沖突。在宿主機(jī)上搭建網(wǎng)橋后,就能很方便的實(shí)現(xiàn)容器之間的通信,而且每個(gè)容器內(nèi)的應(yīng)用都可以使用相同的端口。

同樣,我們?cè)谠瓉淼拇a上增加一點(diǎn)。我們?cè)黾恿?code>syscall.CLONE_NEWNET 這里標(biāo)識(shí)符。

package main

import (
    "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)
}

首先我們?cè)谒拗鳈C(jī)上查看一下自己的網(wǎng)絡(luò)設(shè)備。


可以看到宿主機(jī)上有l(wèi)o, eth0, eth1 等網(wǎng)絡(luò)設(shè)備,而在Namespace 里面什么網(wǎng)絡(luò)設(shè)備都沒有。這樣就能展現(xiàn) Network namespace 與宿主機(jī)之間的網(wǎng)絡(luò)隔離。

可以發(fā)現(xiàn),實(shí)現(xiàn)LinuxNameSpace隔離就是如此簡單。如有勘誤,歡迎斧正~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容