# namespace 與 rootfs
## namespace
這段小程序的作用是,在創建子進程時開啟指定的 Namespace。
在 main 函數里,我們通過 clone() 系統調用創建了一個新的子進程 container_main,并且聲明要為它啟用 Mount Namespace(即:CLONE_NEWNS 標志)。而這個子進程執行的,是一個“/bin/bash”程序,也就是一個 shell。所以這個 shell 就運行在了 Mount Namespace 的隔離環境中。
## ns.c
```
#define _GNU_SOURCE
#include <sys/mount.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
"/bin/bash",
NULL
};
// One
// int container_main(void* arg)
// {
// printf("Container - inside the container!\n");
// execv(container_args[0], container_args);
// printf("Something's wrong!\n");
// return 1;
// }
int container_main(void* arg)
{
printf("Container - inside the container!\n");
mount("none", "/tmp", "tmpfs", 0, "");
execv(container_args[0], container_args);
printf("Something's wrong!\n");
return 1;
}
int main()
{
printf("Parent - start a container!\n");
int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS | SIGCHLD , NULL);
waitpid(container_pid, NULL, 0);
printf("Parent - container stopped!\n");
return 0;
}
```
## 編譯測試
```
$ gcc -o ns ns.c
$ ./ns
Parent - start a container!
Container - inside the container!
```
```
root@ubuntu-xenial:~/mkdocker# df -Th | grep tmpfs
none tmpfs 496M 0 496M 0% /tmp
```
> Mount Namespace 跟其他 Namespace 的使用略有不同的地方對容器進程視圖的改變,一定是伴隨著掛載操作(mount)才能生效。
假設,我們現在有一個 目錄,想要把它作為一個 /bin/bash 進程的根目錄。
首先,創建幾個 lib 文件夾:
```
$ mkdir -p $HOME/{bin,lib64,lib/x86_64-linux-gnu}
$ cd $HOME
```
然后,把 bash 命令拷貝到 test 目錄對應的 bin 路徑下:
```
$ cp -v /bin/{bash,ls} $HOME/bin
```
接下來,把 bash 命令需要的所有 so 文件,也拷貝到 test 目錄對應的 lib 路徑下。找到 so 文件可以用 ldd 命令:
```
$ list="$(ldd /bin/ls | egrep -o '/lib.*\.[0-9]')"
$ for i in $list; do cp -v "$i" "${home}${i}"; done
$ list="$(ldd /bin/bash | egrep -o '/lib.*\.[0-9]')"
$ for i in $list; do cp -v "$i" "${home}${i}"; done
```
最后,執行 chroot 命令,告訴操作系統,我們將使用 $HOME/ 目錄作為 /bin/bash 進程的根目錄:
```
$ chroot $HOME /bin/bash
```
這時,你如果執行 "ls /",就會看到,它返回的都是 `$HOME/ `目錄下面的內容,而不是宿主機的內容。更重要的是,對于被 chroot 的進程來說,它并不會感受到自己的根目錄已經被“修改”成$HOME 了。這種視圖被修改的原理,和 Linux Namespace 很類似,實際上,Mount Namespace 正是基于對 chroot 的不斷改良才被發明出來的,它也是 Linux操作系統里的第一個 Namespace。當然,為了能夠讓容器的這個根目錄看起來更“真實”,我們一般會在這個容器的根目錄下掛載一個完整操作系統的文件系統,比如 Ubuntu16.04 的 ISO。
## rootfs
而這個掛載在容器根目錄上、用來為容器進程提供隔離后執行環境的文件系統,就是所謂的“容器鏡像”。它還有一個更為專業的名字,叫作:rootfs(根文件系統)。
所以,一個最常見的 rootfs,或者說容器鏡像,會包括如下所示的一些目錄和文件,比如/bin,/etc,/proc 等等。
對 Docker 項目來說,它最核心的原理實際上有以下三點:
* 啟用 Linux Namespace 配置;
* 設置指定的 Cgroups 參數;
* 切換進程的根目錄(Change Root)
這樣,一個完整的容器就誕生了。不過,Docker 項目在最后一步的切換上會優先使用 pivot_root 系統調用,如果系統不支持,才會使用 chroot。
另外,需要明確的是,rootfs 只是一個操作系統所包含的文件、配置和目錄,并不包括操作系統內核。在 Linux 操作系統中,這兩部分是分開存放的,操作系統只有在開機啟動時才會加載指定版本的內核鏡像。
Docker 公司在實現 Docker 鏡像時并沒有沿用以前制作 rootfs 的標準流程,而是做了一個小小的創新:
當然,這個想法不是憑空臆造出來的,而是用到了一種叫作聯合文件系統(Union FileSystem)的能力。
Union File System 也叫 UnionFS,最主要的功能是將多個不同位置的目錄聯合掛載(union
mount)到同一個目錄下。比如,我現在有兩個目錄 A 和 B,它們分別有兩個文件:
```
$ tree
.
├──A
│ ├── a
│ └── x
└── B
├── b
└── x
```
然后,我使用聯合掛載的方式,將這兩個目錄掛載到一個公共的目錄 C 上:
Docker 在鏡像的設計中,引入了層(layer)的概念。也就是說,用戶制作鏡像的每一步操作,都會生成一個層,也就是一個增量 rootfs。
AuFS 的全稱是 Another UnionFS,后改名為 Alternative UnionFS,再后來干脆改名叫作Advance UnionFS,從這些名字中你應該能看出這樣兩個事實:
對于 AuFS 來說,它最關鍵的目錄結構在 /var/lib/docker 路徑下的 diff 目錄:
```
/var/lib/docker/aufs/diff/<layer_id>
```
而這個目錄的作用,我們不妨通過一個具體例子來看一下。現在,我們啟動一個容器,比如:
```
$ docker run -d ubuntu:latest sleep 3600
$ docker image inspect ubuntu:latest
```
這個所謂的“鏡像”,實際上就是一個 Ubuntu 操作系統的 rootfs,它的內容是 Ubuntu 操作系統的所有文件和目錄。不過,與之前我們講述的 rootfs 稍微不同的是,Docker 鏡像使用的
rootfs,往往由多個“層”組成:
```
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:f49017d4d5ce9c0f544c...",
"sha256:8f2b771487e9d6354080...",
"sha256:ccd4d61916aaa2159429...",
"sha256:c01d74f99de40e097c73...",
"sha256:268a067217b5fe78e000..."
]
}
```
可以看到,這個 Ubuntu 鏡像,實際上由五個層組成。這五個層就是五個增量 rootfs,每一層都是 Ubuntu 操作系統文件與目錄的一部分;而在使用鏡像時,Docker 會把這些增量聯合掛載
在一個統一的掛載點上(等價于前面例子里的“/C”目錄)。這個掛載點就是 /var/lib/docker/aufs/mnt/,比如:
這個目錄里面正是一個完整的 Ubuntu 操作系統:那么,前面提到的五個鏡像層,又是如何被聯合掛載成這樣一個完整的 Ubuntu 文件系統的呢?
這個信息記錄在 AuFS 的系統目錄 /sys/fs/aufs 下面。
```
$ ls /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
```
```
$ cat /proc/mounts| grep aufs
none /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fc... aufs rw,relatime,si=972c6d361e6b32ba
```

### 第一部分,只讀層
它是這個容器的 rootfs 最下面的五層,對應的正是 ubuntu:latest 鏡像的五層。可以看到,它們的掛載方式都是只讀的(ro+wh,即 readonly+whiteout)。這時,我們可以分別查看一下這些層的內容。可以看到,這些層,都以增量的方式分別包含了 Ubuntu 操作系統的一部分。
```
$ ls /var/lib/docker/aufs/diff/72b0744e06247c7d0...
etc sbin usr var
$ ls /var/lib/docker/aufs/diff/32e8e20064858c0f2...
run
$ ls /var/lib/docker/aufs/diff/a524a729adadedb900...
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
```
### 第二部分,可讀寫層
它是這個容器的 rootfs 最上面的一層(6e3be5d2ecccae7cc),它的掛載方式為:rw,即read write。在沒有寫入文件之前,這個目錄是空的。而一旦在容器里做了寫操作,你修改產生的內容就會以增量的方式出現在這個層中。可是,你有沒有想到這樣一個問題:如果我現在要做的,是刪除只讀層里的一個文件呢?為了實現這樣的刪除操作,AuFS 會在可讀寫層創建一個 whiteout 文件,把只讀層里的文件“遮擋”起來。
比如,你要刪除只讀層里一個名叫 foo 的文件,那么這個刪除操作實際上是在可讀寫層創建了一個名叫.wh.foo 的文件。這樣,當這兩個層被聯合掛載之后,foo 文件就會被.wh.foo 文件“遮擋”起來,“消失”了。這個功能,就是“ro+wh”的掛載方式,即只讀 +whiteout 的含義。我喜歡把 whiteout 形象地翻譯為:“白障”。
所以,最上面這個可讀寫層的作用,就是專門用來存放你修改 rootfs 后產生的增量,無論是增、刪、改,都發生在這里。而當我們使用完了這個被修改過的容器之后,還可以使用 docker commit 和 push 指令,保存這個被修改過的可讀寫層,并上傳到 Docker Hub 上,供其他人使用;而與此同時,原先的只讀層里的內容則不會有任何變化。這,就是增量 rootfs 的好處。、
### 第三部分,Init 層
它是一個以“-init”結尾的層,夾在只讀層和讀寫層之間。Init 層是 Docker 項目單獨生成的一個內部層,專門用來存放 `/etc/hosts、/etc/resolv.conf `等信息。需要這樣一層的原因是,這些文件本來屬于只讀的 Ubuntu 鏡像的一部分,但是用戶往往需要在啟動容器時寫入一些指定的值比如 hostname,所以就需要在可讀寫層對它們進行修改。可是,這些修改往往只對當前的容器有效,我們并不希望執行 docker commit 時,把這些信息連同可讀寫層一起提交掉。
所以,Docker 做法是,在修改了這些文件之后,以一個單獨的層掛載了出來。而用戶執行 docker commit 只會提交可讀寫層,所以是不包含這些內容的。
最終,這 7 個層都被聯合掛載到 /var/lib/docker/aufs/mnt 目錄下,表現為一個完整的Ubuntu 操作系統供容器使用。