Docker基礎技術:Linux Namespace(上)
時下最熱的技術莫過于Docker了,很多人都覺得Docker是個新技術,其實不然,Docker除了其編程語言用go比較新外,其實它還真不是個新東西,也就是個新瓶裝舊酒的東西,所謂的The New “Old Stuff”。Docker和Docker衍生的東西用到了很多很酷的技術,我會用幾篇 文章來把這些技術給大家做個介紹,希望通過這些文章大家可以自己打造一個山寨版的docker。
當然,文章的風格一定會尊重時下的“流行”——我們再也沒有整塊整塊的時間去看書去專研,而我們只有看微博微信那樣的碎片時間(那怕我們有整塊的時間,也被那些在手機上的APP碎片化了)。所以,這些文章的風格必然堅持“馬桶風格”(希望簡單到占用你拉一泡屎就時間,而且你還不用動腦子,并能學到些東西)
廢話少說,我們開始。先從Linux Namespace開始。
簡介
Linux Namespace是Linux提供的一種內核級別環(huán)境隔離的方法。不知道你是否還記得很早以前的Unix有一個叫chroot的系統(tǒng)調用(通過修改根目錄把用戶jail到一個特定目錄下),chroot提供了一種簡單的隔離模式:chroot內部的文件系統(tǒng)無法訪問外部的內容。Linux Namespace在此基礎上,提供了對UTS、IPC、mount、PID、network、User等的隔離機制。
舉個例子,我們都知道,Linux下的超級父親進程的PID是1,所以,同chroot一樣,如果我們可以把用戶的進程空間jail到某個進程分支下,并像chroot那樣讓其下面的進程 看到的那個超級父進程的PID為1,于是就可以達到資源隔離的效果了(不同的PID namespace中的進程無法看到彼此)
šLinux Namespace 有如下種類,官方文檔在這里《Namespace in Operation》
主要是š三個系統(tǒng)調用
- šclone() – 實現(xiàn)線程的系統(tǒng)調用,用來創(chuàng)建一個新的進程,并可以通過設計上述參數達到隔離。
 - šunshare() – 使某進程脫離某個namespace
 - šsetns() – 把某進程加入到某個namespace
 
unshare() 和 setns() 都比較簡單,大家可以自己man,我這里不說了。
下面還是讓我們來看一些示例(以下的測試程序最好在Linux 內核為3.8以上的版本中運行,我用的是ubuntu 14.04)。
clone()系統(tǒng)調用
首先,我們來看一下一個最簡單的clone()系統(tǒng)調用的示例,(后面,我們的程序都會基于這個程序做修改):
- #define _GNU_SOURCE
 - #include <sys/types.h>
 - #include <sys/wait.h>
 - #include <stdio.h>
 - #include <sched.h>
 - #include <signal.h>
 - #include <unistd.h>
 - /* 定義一個給 clone 用的棧,棧大小1M */
 - #define STACK_SIZE (1024 * 1024)
 - static char container_stack[STACK_SIZE];
 - char* const container_args[] = {
 - "/bin/bash",
 - NULL
 - };
 - int container_main(void* arg)
 - {
 - printf("Container - inside the container!\n");
 - /* 直接執(zhí)行一個shell,以便我們觀察這個進程空間里的資源是否被隔離了 */
 - execv(container_args[0], container_args);
 - printf("Something's wrong!\n");
 - return 1;
 - }
 - int main()
 - {
 - printf("Parent - start a container!\n");
 - /* 調用clone函數,其中傳出一個函數,還有一個??臻g的(為什么傳尾指針,因為棧是反著的) */
 - int container_pid = clone(container_main, container_stack+STACK_SIZE, SIGCHLD, NULL);
 - /* 等待子進程結束 */
 - waitpid(container_pid, NULL, 0);
 - printf("Parent - container stopped!\n");
 - return 0;
 - }
 
從上面的程序,我們可以看到,這和pthread基本上是一樣的玩法。但是,對于上面的程序,父子進程的進程空間是沒有什么差別的,父進程能訪問到的子進程也能。
下面, 讓我們來看幾個例子看看,Linux的Namespace是什么樣的。
UTS Namespace
下面的代碼,我略去了上面那些頭文件和數據結構的定義,只有最重要的部分。
- int container_main(void* arg)
 - {
 - printf("Container - inside the container!\n");
 - sethostname("container",10); /* 設置hostname */
 - 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_NEWUTS | SIGCHLD, NULL); /*啟用CLONE_NEWUTS Namespace隔離 */
 - waitpid(container_pid, NULL, 0);
 - printf("Parent - container stopped!\n");
 - return 0;
 - }
 
運行上面的程序你會發(fā)現(xiàn)(需要root權限),子進程的hostname變成了 container。
- hchen@ubuntu:~$ sudo ./uts
 - Parent - start a container!
 - Container - inside the container!
 - root@container:~# hostname
 - container
 - root@container:~# uname -n
 - container
 
IPC Namespace
IPC全稱 Inter-Process Communication,是Unix/Linux下進程間通信的一種方式,IPC有共享內存、信號量、消息隊列等方法。所以,為了隔離,我們也需要把IPC給隔離開來,這樣,只有在同一個Namespace下的進程才能相互通信。如果你熟悉IPC的原理的話,你會知道,IPC需要有一個全局的ID,即然是全局的,那么就意味著我們的Namespace需要對這個ID隔離,不能讓別的Namespace的進程看到。
要啟動IPC隔離,我們只需要在調用clone時加上CLONE_NEWIPC參數就可以了。
- int container_pid = clone(container_main, container_stack+STACK_SIZE,
 - CLONE_NEWUTS | CLONE_NEWIPC | SIGCHLD, NULL);
 
首先,我們先創(chuàng)建一個IPC的Queue(如下所示,全局的Queue ID是0)
- hchen@ubuntu:~$ ipcmk -Q
 - Message queue id: 0
 - hchen@ubuntu:~$ ipcs -q
 - ------ Message Queues --------
 - key msqid owner perms used-bytes messages
 - 0xd0d56eb2 0 hchen 644 0 0
 
如果我們運行沒有CLONE_NEWIPC的程序,我們會看到,在子進程中還是能看到這個全啟的IPC Queue。
- hchen@ubuntu:~$ sudo ./uts
 - Parent - start a container!
 - Container - inside the container!
 - root@container:~# ipcs -q
 - ------ Message Queues --------
 - key msqid owner perms used-bytes messages
 - 0xd0d56eb2 0 hchen 644 0 0
 
但是,如果我們運行加上了CLONE_NEWIPC的程序,我們就會下面的結果:
- root@ubuntu:~$ sudo./ipc
 - Parent - start a container!
 - Container - inside the container!
 - root@container:~/linux_namespace# ipcs -q
 - ------ Message Queues --------
 - key msqid owner perms used-bytes messages
 
我們可以看到IPC已經被隔離了。
PID Namespace
我們繼續(xù)修改上面的程序:
- int container_main(void* arg)
 - {
 - /* 查看子進程的PID,我們可以看到其輸出子進程的 pid 為 1 */
 - printf("Container [%5d] - inside the container!\n", getpid());
 - sethostname("container",10);
 - execv(container_args[0], container_args);
 - printf("Something's wrong!\n");
 - return 1;
 - }
 - int main()
 - {
 - printf("Parent [%5d] - start a container!\n", getpid());
 - /*啟用PID namespace - CLONE_NEWPID*/
 - int container_pid = clone(container_main, container_stack+STACK_SIZE,
 - CLONE_NEWUTS | CLONE_NEWPID | SIGCHLD, NULL);
 - waitpid(container_pid, NULL, 0);
 - printf("Parent - container stopped!\n");
 - return 0;
 - }
 
運行結果如下(我們可以看到,子進程的pid是1了):
- hchen@ubuntu:~$ sudo ./pid
 - Parent [ 3474] - start a container!
 - Container [ 1] - inside the container!
 - root@container:~# echo $$
 - 1
 
你可能會問,PID為1有個毛用啊?我們知道,在傳統(tǒng)的UNIX系統(tǒng)中,PID為1的進程是init,地位非常特殊。他作為所有進程的父進程,有很多特權(比如:屏蔽信號等),另外,其還會為檢查所有進程的狀態(tài),我們知道,如果某個子進程脫離了父進程(父進程沒有wait它),那么init就會負責回收資源并結束這個子進程。所以,要做到進程空間的隔離,首先要創(chuàng)建出PID為1的進程,最好就像chroot那樣,把子進程的PID在容器內變成1。
但是,我們會發(fā)現(xiàn),在子進程的shell里輸入ps,top等命令,我們還是可以看得到所有進程。說明并沒有完全隔離。這是因為,像ps, top這些命令會去讀/proc文件系統(tǒng),所以,因為/proc文件系統(tǒng)在父進程和子進程都是一樣的,所以這些命令顯示的東西都是一樣的。
所以,我們還需要對文件系統(tǒng)進行隔離。
Mount Namespace
下面的例程中,我們在啟用了mount namespace并在子進程中重新mount了/proc文件系統(tǒng)。
- int container_main(void* arg)
 - {
 - printf("Container [%5d] - inside the container!\n", getpid());
 - sethostname("container",10);
 - /* 重新mount proc文件系統(tǒng)到 /proc下 */
 - system("mount -t proc proc /proc");
 - execv(container_args[0], container_args);
 - printf("Something's wrong!\n");
 - return 1;
 - }
 - int main()
 - {
 - printf("Parent [%5d] - start a container!\n", getpid());
 - /* 啟用Mount Namespace - 增加CLONE_NEWNS參數 */
 - int container_pid = clone(container_main, container_stack+STACK_SIZE,
 - CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
 - waitpid(container_pid, NULL, 0);
 - printf("Parent - container stopped!\n");
 - return 0;
 - }
 
運行結果如下:
- hchen@ubuntu:~$ sudo ./pid.mnt
 - Parent [ 3502] - start a container!
 - Container [ 1] - inside the container!
 - root@container:~# ps -elf
 - F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
 - 4 S root 1 0 0 80 0 - 6917 wait 19:55 pts/2 00:00:00 /bin/bash
 - 0 R root 14 1 0 80 0 - 5671 - 19:56 pts/2 00:00:00 ps -elf
 
上面,我們可以看到只有兩個進程 ,而且pid=1的進程是我們的/bin/bash。我們還可以看到/proc目錄下也干凈了很多:
- root@container:~# ls /proc
 - 1 dma key-users net sysvipc
 - 16 driver kmsg pagetypeinfo timer_list
 - acpi execdomains kpagecount partitions timer_stats
 - asound fb kpageflags sched_debug tty
 - buddyinfo filesystems loadavg schedstat uptime
 - bus fs locks scsi version
 - cgroups interrupts mdstat self version_signature
 - cmdline iomem meminfo slabinfo vmallocinfo
 - consoles ioports misc softirqs vmstat
 - cpuinfo irq modules stat zoneinfo
 - crypto kallsyms mounts swaps
 - devices kcore mpt sys
 - diskstats keys mtrr sysrq-trigger
 
下圖,我們也可以看到在子進程中的top命令只看得到兩個進程了。

這里,多說一下。在通過CLONE_NEWNS創(chuàng)建mount namespace后,父進程會把自己的文件結構復制給子進程中。而子進程中新的namespace中的所有mount操作都只影響自身的文件系統(tǒng),而不對外界產生任何影響。這樣可以做到比較嚴格地隔離。
你可能會問,我們是不是還有別的一些文件系統(tǒng)也需要這樣mount? 是的。
Docker的 Mount Namespace
下面我將向演示一個“山寨鏡像”,其模仿了Docker的Mount Namespace。
首先,我們需要一個rootfs,也就是我們需要把我們要做的鏡像中的那些命令什么的copy到一個rootfs的目錄下,我們模仿Linux構建如下的目錄:
- hchen@ubuntu:~/rootfs$ ls
 - bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var
 
然后,我們把一些我們需要的命令copy到 rootfs/bin目錄中(sh命令必需要copy進去,不然我們無法 chroot )
- hchen@ubuntu:~/rootfs$ ls ./bin ./usr/bin
 - ./bin:
 - bash chown gzip less mount netstat rm tabs tee top tty
 - cat cp hostname ln mountpoint ping sed tac test touch umount
 - chgrp echo ip ls mv ps sh tail timeout tr uname
 - chmod grep kill more nc pwd sleep tar toe truncate which
 - ./usr/bin:
 - awk env groups head id mesg sort strace tail top uniq vi wc xargs
 
注:你可以使用ldd命令把這些命令相關的那些so文件copy到對應的目錄:
- hchen@ubuntu:~/rootfs/bin$ ldd bash
 - linux-vdso.so.1 => (0x00007fffd33fc000)
 - libtinfo.so.5 => /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007f4bd42c2000)
 - libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f4bd40be000)
 - libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4bd3cf8000)
 - /lib64/ld-linux-x86-64.so.2 (0x00007f4bd4504000)
 
下面是我的rootfs中的一些so文件:
- hchen@ubuntu:~/rootfs$ ls ./lib64 ./lib/x86_64-linux-gnu/
 - ./lib64:
 - ld-linux-x86-64.so.2
 - ./lib/x86_64-linux-gnu/:
 - libacl.so.1 libmemusage.so libnss_files-2.19.so libpython3.4m.so.1
 - libacl.so.1.1.0 libmount.so.1 libnss_files.so.2 libpython3.4m.so.1.0
 - libattr.so.1 libmount.so.1.1.0 libnss_hesiod-2.19.so libresolv-2.19.so
 - libblkid.so.1 libm.so.6 libnss_hesiod.so.2 libresolv.so.2
 - libc-2.19.so libncurses.so.5 libnss_nis-2.19.so libselinux.so.1
 - libcap.a libncurses.so.5.9 libnss_nisplus-2.19.so libtinfo.so.5
 - libcap.so libncursesw.so.5 libnss_nisplus.so.2 libtinfo.so.5.9
 - libcap.so.2 libncursesw.so.5.9 libnss_nis.so.2 libutil-2.19.so
 - libcap.so.2.24 libnsl-2.19.so libpcre.so.3 libutil.so.1
 - libc.so.6 libnsl.so.1 libprocps.so.3 libuuid.so.1
 - libdl-2.19.so libnss_compat-2.19.so libpthread-2.19.so libz.so.1
 - libdl.so.2 libnss_compat.so.2 libpthread.so.0
 - libgpm.so.2 libnss_dns-2.19.so libpython2.7.so.1
 - libm-2.19.so libnss_dns.so.2 libpython2.7.so.1.0
 
包括這些命令依賴的一些配置文件:
- hchen@ubuntu:~/rootfs$ ls ./etc
 - bash.bashrc group hostname hosts ld.so.cache nsswitch.conf passwd profile
 - resolv.conf shadow
 
你現(xiàn)在會說,我靠,有些配置我希望是在容器起動時給他設置的,而不是hard code在鏡像中的。比如:/etc/hosts,/etc/hostname,還有DNS的/etc/resolv.conf文件。好的。那我們在rootfs外面,我們再創(chuàng)建一個conf目錄,把這些文件放到這個目錄中。
- hchen@ubuntu:~$ ls ./conf
 - hostname hosts resolv.conf
 
這樣,我們的父進程就可以動態(tài)地設置容器需要的這些文件的配置, 然后再把他們mount進容器,這樣,容器的鏡像中的配置就比較靈活了。
好了,終于到了我們的程序。
- #define _GNU_SOURCE
 - #include <sys/types.h>
 - #include <sys/wait.h>
 - #include <sys/mount.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",
 - "-l",
 - NULL
 - };
 - int container_main(void* arg)
 - {
 - printf("Container [%5d] - inside the container!\n", getpid());
 - //set hostname
 - sethostname("container",10);
 - //remount "/proc" to make sure the "top" and "ps" show container's information
 - if (mount("proc", "rootfs/proc", "proc", 0, NULL) !=0 ) {
 - perror("proc");
 - }
 - if (mount("sysfs", "rootfs/sys", "sysfs", 0, NULL)!=0) {
 - perror("sys");
 - }
 - if (mount("none", "rootfs/tmp", "tmpfs", 0, NULL)!=0) {
 - perror("tmp");
 - }
 - if (mount("udev", "rootfs/dev", "devtmpfs", 0, NULL)!=0) {
 - perror("dev");
 - }
 - if (mount("devpts", "rootfs/dev/pts", "devpts", 0, NULL)!=0) {
 - perror("dev/pts");
 - }
 - if (mount("shm", "rootfs/dev/shm", "tmpfs", 0, NULL)!=0) {
 - perror("dev/shm");
 - }
 - if (mount("tmpfs", "rootfs/run", "tmpfs", 0, NULL)!=0) {
 - perror("run");
 - }
 - /*
 - * 模仿Docker的從外向容器里mount相關的配置文件
 - * 你可以查看:/var/lib/docker/containers/<container_id>/目錄,
 - * 你會看到docker的這些文件的。
 - */
 - if (mount("conf/hosts", "rootfs/etc/hosts", "none", MS_BIND, NULL)!=0 ||
 - mount("conf/hostname", "rootfs/etc/hostname", "none", MS_BIND, NULL)!=0 ||
 - mount("conf/resolv.conf", "rootfs/etc/resolv.conf", "none", MS_BIND, NULL)!=0 ) {
 - perror("conf");
 - }
 - /* 模仿docker run命令中的 -v, --volume=[] 參數干的事 */
 - if (mount("/tmp/t1", "rootfs/mnt", "none", MS_BIND, NULL)!=0) {
 - perror("mnt");
 - }
 - /* chroot 隔離目錄 */
 - if ( chdir("./rootfs") != 0 || chroot("./") != 0 ){
 - perror("chdir/chroot");
 - }
 - execv(container_args[0], container_args);
 - perror("exec");
 - printf("Something's wrong!\n");
 - return 1;
 - }
 - int main()
 - {
 - printf("Parent [%5d] - start a container!\n", getpid());
 - int container_pid = clone(container_main, container_stack+STACK_SIZE,
 - CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
 - waitpid(container_pid, NULL, 0);
 - printf("Parent - container stopped!\n");
 - return 0;
 - }
 
sudo運行上面的程序,你會看到下面的掛載信息以及一個所謂的“鏡像”:
- hchen@ubuntu:~$ sudo ./mount
 - Parent [ 4517] - start a container!
 - Container [ 1] - inside the container!
 - root@container:/# mount
 - proc on /proc type proc (rw,relatime)
 - sysfs on /sys type sysfs (rw,relatime)
 - none on /tmp type tmpfs (rw,relatime)
 - udev on /dev type devtmpfs (rw,relatime,size=493976k,nr_inodes=123494,mode=755)
 - devpts on /dev/pts type devpts (rw,relatime,mode=600,ptmxmode=000)
 - tmpfs on /run type tmpfs (rw,relatime)
 - /dev/disk/by-uuid/18086e3b-d805-4515-9e91-7efb2fe5c0e2 on /etc/hosts type ext4 (rw,relatime,errors=remount-ro,data=ordered)
 - /dev/disk/by-uuid/18086e3b-d805-4515-9e91-7efb2fe5c0e2 on /etc/hostname type ext4 (rw,relatime,errors=remount-ro,data=ordered)
 - /dev/disk/by-uuid/18086e3b-d805-4515-9e91-7efb2fe5c0e2 on /etc/resolv.conf type ext4 (rw,relatime,errors=remount-ro,data=ordered)
 - root@container:/# ls /bin /usr/bin
 - /bin:
 - bash chmod echo hostname less more mv ping rm sleep tail test top truncate uname
 - cat chown grep ip ln mount nc ps sed tabs tar timeout touch tty which
 - chgrp cp gzip kill ls mountpoint netstat pwd sh tac tee toe tr umount
 - /usr/bin:
 - awk env groups head id mesg sort strace tail top uniq vi wc xargs
 
關于如何做一個chroot的目錄,這里有個工具叫DebootstrapChroot,你可以順著鏈接去看看(英文的哦)
接下來的事情,你可以自己玩了,我相信你的想像力 。:)
今天的內容就介紹到這里,在Docker 基礎技術:Linux Namespace(下篇)中,我將向你介紹User Namespace、Network Namespace以及Namespace的其它東西。
















 
 
 







 
 
 
 