DOCKER基礎技術:LINUX NAMESPACE(上)

時下最熱的技術莫過於Docker了,不少人都以爲Docker是個新技術,其實否則,Docker除了其編程語言用go比較新外,其實它還真不是個新東西,也就是個新瓶裝舊酒的東西,所謂的The New 「Old Stuff」。Docker和Docker衍生的東西用到了不少很酷的技術,我會用幾篇 文章來把這些技術給你們作個介紹,但願經過這些文章你們能夠本身打造一個山寨版的docker。php

固然,文章的風格必定會尊重時下的「流行」——咱們再也沒有整塊整塊的時間去看書去專研,而咱們只有看微博微信那樣的碎片時間(那怕咱們有整塊的時間,也被那些在手機上的APP碎片化了)。因此,這些文章的風格必然堅持「馬桶風格」(但願簡單到佔用你拉一泡屎就時間,並且你還不用動腦子,並能學到些東西)html

廢話少說,咱們開始。先從Linux Namespace開始。node

 簡介

Linux Namespace是Linux提供的一種內核級別環境隔離的方法。不知道你是否還記得很早之前的Unix有一個叫chroot的系統調用(經過修改根目錄把用戶jail到一個特定目錄下),chroot提供了一種簡單的隔離模式:chroot內部的文件系統沒法訪問外部的內容。Linux Namespace在此基礎上,提供了對UTS、IPC、mount、PID、network、User等的隔離機制。python

 

舉個例子,咱們都知道,Linux下的超級父親進程的PID是1,因此,同chroot同樣,若是咱們能夠把用戶的進程空間jail到某個進程分支下,並像chroot那樣讓其下面的進程 看到的那個超級父進程的PID爲1,因而就能夠達到資源隔離的效果了(不一樣的PID namespace中的進程沒法看到彼此)linux

šLinux Namespace 有以下種類,官方文檔在這裏《Namespace in Operationdocker

分類 系統調用參數 相關內核版本
Mount namespaces CLONE_NEWNS Linux 2.4.19
šUTS namespaces CLONE_NEWUTS Linux 2.6.19
IPC namespaces CLONE_NEWIPC Linux 2.6.19
PID namespaces CLONE_NEWPID Linux 2.6.24
Network namespaces CLONE_NEWNET 始於Linux 2.6.24 完成於 Linux 2.6.29
User namespaces CLONE_NEWUSER 始於 Linux 2.6.23 完成於 Linux 3.8)

主要是š三個系統調用shell

  • šclone() – 實現線程的系統調用,用來建立一個新的進程,並能夠經過設計上述參數達到隔離。
  • šunshare() – 使某進程脫離某個namespace
  • šsetns() – 把某進程加入到某個namespace

unshare() 和 setns() 都比較簡單,你們能夠本身man,我這裏不說了。編程

下面仍是讓咱們來看一些示例(如下的測試程序最好在Linux 內核爲3.8以上的版本中運行,我用的是ubuntu 14.04)。bootstrap

clone()系統調用

首先,咱們來看一下一個最簡單的clone()系統調用的示例,(後面,咱們的程序都會基於這個程序作修改):ubuntu

#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");
    /* 直接執行一個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 ./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);

首先,咱們先建立一個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

咱們繼續修改上面的程序:

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有個毛用啊?咱們知道,在傳統的UNIX系統中,PID爲1的進程是init,地位很是特殊。他做爲全部進程的父進程,有不少特權(好比:屏蔽信號等),另外,其還會爲檢查全部進程的狀態,咱們知道,若是某個子進程脫離了父進程(父進程沒有wait它),那麼init就會負責回收資源並結束這個子進程。因此,要作到進程空間的隔離,首先要建立出PID爲1的進程,最好就像chroot那樣,把子進程的PID在容器內變成1。

可是,咱們會發現,在子進程的shell裏輸入ps,top等命令,咱們仍是能夠看獲得全部進程。說明並無徹底隔離。這是由於,像ps, top這些命令會去讀/proc文件系統,因此,由於/proc文件系統在父進程和子進程都是同樣的,因此這些命令顯示的東西都是同樣的。

因此,咱們還須要對文件系統進行隔離。

Mount Namespace

下面的例程中,咱們在啓用了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.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建立mount namespace後,父進程會把本身的文件結構複製給子進程中。而子進程中新的namespace中的全部mount操做都隻影響自身的文件系統,而不對外界產生任何影響。這樣能夠作到比較嚴格地隔離。

 

你可能會問,咱們是否是還有別的一些文件系統也須要這樣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

你如今會說,我靠,有些配置我但願是在容器起動時給他設置的,而不是hard code在鏡像中的。好比:/etc/hosts,/etc/hostname,還有DNS的/etc/resolv.conf文件。好的。那咱們在rootfs外面,咱們再建立一個conf目錄,把這些文件放到這個目錄中。

hchen@ubuntu:~$ ls ./conf
hostname     hosts     resolv.conf

這樣,咱們的父進程就能夠動態地設置容器須要的這些文件的配置, 而後再把他們mount進容器,這樣,容器的鏡像中的配置就比較靈活了。

好了,終於到了咱們的程序。

 

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,你能夠順着連接去看看(英文的哦)

接下來的事情,你能夠本身玩了,我相信你的想像力 。:)

在下一篇,我將向你介紹User Namespace、Network Namespace以及Namespace的其它東西。

 

摘自:https://coolshell.cn/articles/17010.html

相關文章
相關標籤/搜索