Linux Namespace是Linux提供的一種內核級別環境隔離的方法。不知道你是否還記得很早以前的Unix有一個叫chroot的系統調用(通過修改根目錄把用戶到一個特定目錄下),chroot提供了一種簡單的隔離模式:chroot內部的文件系統無法訪問外部的內容。Linux Namespace在此基礎上,提供了對UTS、IPC、mount、PID、network、User等的隔離機制。
舉個例子,我們都知道,Linux下的超級父親進程的PID是1,所以,同chroot一樣,如果我們可以把用戶的進程空間jail到某個進程分支下,並像chroot那樣讓其下面的進程 看到的那個超級父進程的PID為1,於是就可以達到資源隔離的效果了(不同的PID namespace中的進程無法看到彼此)
Linux Namespace 有如下種類,
主要是三個系統調用主要是三個系統調用
clone() – 實現線程的系統調用,用來創建一個新的進程,並可以通過設計上述參數達到隔離。
unshare() – 使某進程脫離某個namespace
setns() – 把某進程加入到某個namespace
unshare() 和 setns() 都比較簡單,大家可以自己man,我這裡不說了。
下面還是讓我們來看一些示例(以下的測試程序最好在Linux 內核為3.8以上的版本中運行,我用的是ubuntu 14.04)。
clone()系統調用首先,我們來看一下一個最簡單的clone()系統調用的示例,(後面,我們的程序都會基於這個程序做修改):
#define _GNU_SOURCE#include #include #include #include #include #include /* 定義一個給 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"); /* 直接執行一個shell,以便我們觀察這個進程空間裡的資源是否被隔離了 */ execv(container_args[0], container_args); printf("Something's wrong!/n"); return 1;}int main(){ printf("Parent - start a container!/n"); /* 調用clone函數,其中傳出一個函數,還有一個棧空間的(為什麼傳尾指針,因為棧是反著的) */ 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;}
運行上面的程序你會發現(需要root權限),子進程的hostname變成了 container。
hchen@ubuntu:~$ sudo ./utsParent - start a container!Container - inside the container!root@container:~# hostnamecontainerroot@container:~# uname -ncontainerIPC 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);
首先,我們先創建一個IPC的Queue(如下所示,全局的Queue ID是0)
hchen@ubuntu:~$ ipcmk -QMessage queue id: 0hchen@ubuntu:~$ ipcs -q------ Message Queues --------key msqid owner perms used-bytes messages0xd0d56eb2 0 hchen 644 0 0
如果我們運行沒有CLONE_NEWIPC的程序,我們會看到,在子進程中還是能看到這個全啟的IPC Queue。
hchen@ubuntu:~$ sudo ./utsParent - start a container!Container - inside the container!root@container:~# ipcs -q------ Message Queues --------key msqid owner perms used-bytes messages0xd0d56eb2 0 hchen 644 0 0
但是,如果我們運行加上了CLONE_NEWIPC的程序,我們就會下面的結果:
root@ubuntu:~$ sudo./ipcParent - 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我們繼續修改上面的程序:
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 ./pidParent [ 3474] - start a container!Container [ 1] - inside the container!root@container:~# echo $$
PID為1,在傳統的UNIX系統中,PID為1的進程是init,地位非常特殊。他作為所有進程的父進程,有很多特權(比如:屏蔽信號等),另外,其還會為檢查所有進程的狀態,我們知道,如果某個子進程脫離了父進程(父進程沒有wait它),那麼init就會負責回收資源並結束這個子進程。所以,要做到進程空間的隔離,首先要創建出PID為1的進程,最好就像chroot那樣,把子進程的PID在容器內變成1。
但是,我們會發現,在子進程的shell裡輸入ps,top等命令,我們還是可以看得到所有進程。說明並沒有完全隔離。這是因為,像ps, top這些命令會去讀/proc文件系統,所以,因為/proc文件系統在父進程和子進程都是一樣的,所以這些命令顯示的東西都是一樣的。
所以,我們還需要對文件系統進行隔離。
下面的例程中,我們在啟用了mount namespace並在子進程中重新mount了/proc文件系統。
int container_main(void* arg){ printf("Container [%5d] - inside the container!/n", getpid()); sethostname("container",10); /* 重新mount proc文件系統到 /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.mntParent [ 3502] - start a container!Container [ 1] - inside the container!root@container:~# ps -elfF S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD4 S root 1 0 0 80 0 - 6917 wait 19:55 pts/2 00:00:00 /bin/bash0 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 /proc1 dma key-users net sysvipc16 driver kmsg pagetypeinfo timer_listacpi execdomains kpagecount partitions timer_statsasound fb kpageflags sched_debug ttybuddyinfo filesystems loadavg schedstat uptimebus fs locks scsi versioncgroups interrupts mdstat self version_signaturecmdline iomem meminfo slabinfo vmallocinfoconsoles ioports misc softirqs vmstatcpuinfo irq modules stat zoneinfocrypto kallsyms mounts swapsdevices kcore mpt sysdiskstats keys mtrr sysrq-trigger
下圖,我們也可以看到在子進程中的top命令只看得到兩個進程了。
這裡,多說一下。在通過CLONE_NEWNS創建mount namespace後,父進程會把自己的文件結構復制給子進程中。而子進程中新的namespace中的所有mount操作都只影響自身的文件系統,而不對外界產生任何影響。這樣可以做到比較嚴格地隔離。
你可能會問,我們是不是還有別的一些文件系統也需要這樣mount? 是的。
Docker的 Mount Namespace下面我將向演示一個“山寨鏡像”,其模仿了Docker的Mount Namespace。
首先,我們需要一個rootfs,也就是我們需要把我們要做的鏡像中的那些命令什麼的copy到一個rootfs的目錄下,我們模仿Linux構建如下的目錄:
hchen@ubuntu:~/rootfs$ lsbin 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 ttycat cp hostname ln mountpoint ping sed tac test touch umountchgrp echo ip ls mv ps sh tail timeout tr unamechmod 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.1libacl.so.1.1.0 libmount.so.1 libnss_files.so.2 libpython3.4m.so.1.0libattr.so.1 libmount.so.1.1.0 libnss_hesiod-2.19.so libresolv-2.19.solibblkid.so.1 libm.so.6 libnss_hesiod.so.2 libresolv.so.2libc-2.19.so libncurses.so.5 libnss_nis-2.19.so libselinux.so.1libcap.a libncurses.so.5.9 libnss_nisplus-2.19.so libtinfo.so.5libcap.so libncursesw.so.5 libnss_nisplus.so.2 libtinfo.so.5.9libcap.so.2 libncursesw.so.5.9 libnss_nis.so.2 libutil-2.19.solibcap.so.2.24 libnsl-2.19.so libpcre.so.3 libutil.so.1libc.so.6 libnsl.so.1 libprocps.so.3 libuuid.so.1libdl-2.19.so libnss_compat-2.19.so libpthread-2.19.so libz.so.1libdl.so.2 libnss_compat.so.2 libpthread.so.0libgpm.so.2 libnss_dns-2.19.so libpython2.7.so.1libm-2.19.so libnss_dns.so.2 libpython2.7.so.1.0
包括這些命令依賴的一些配置文件:
hchen@ubuntu:~/rootfs$ ls ./etcbash.bashrc group hostname hosts ld.so.cache nsswitch.conf passwd profileresolv.conf shadow
你現在會說,我靠,有些配置我希望是在容器起動時給他設置的,而不是hard code在鏡像中的。比如:/etc/hosts,/etc/hostname,還有DNS的/etc/resolv.conf文件。好的。那我們在rootfs外面,我們再創建一個conf目錄,把這些文件放到這個目錄中。
hchen@ubuntu:~$ ls ./confhostname hosts resolv.conf
這樣,我們的父進程就可以動態地設置容器需要的這些文件的配置, 然後再把他們mount進容器,這樣,容器的鏡像中的配置就比較靈活了。
好了,終於到了我們的程序。
#define _GNU_SOURCE#include #include #include #include #include #include #include #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//目錄, * 你會看到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 ./mountParent [ 4517] - start a container!Container [ 1] - inside the container!root@container:/# mountproc 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 unamecat chown grep ip ln mount nc ps sed tabs tar timeout touch tty whichchgrp 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的其它東西。
原文來自:http://os.51cto.com/art/201609/517640.htm
本文地址:http://www.linuxprobe.com/docker-linux-namespace-1.html
http://xxxxxx/Linuxjc/1184709.html TechArticle