![自己动手写Docker](https://wfqqreader-1252317822.image.myqcloud.com/cover/589/35537589/b_35537589.jpg)
第2章 基础技术
2.1 Linux Namespace介绍
我们经常听到,Docker是一个使用了Linux Namespace和Cgroups的虚拟化工具。但是,什么是Linux Namespace,它在Docker内是怎么被使用的?说到这里,很多人就会迷茫。下面就先来介绍一下Linux Namespace及它们是如何在容器中使用的。
2.1.1 概念
Linux Namespace是Kernel的一个功能,它可以隔离一系列的系统资源,比如PID(Process ID)、User ID、Network等。一般看到这里,很多人会想到一个命令chroot,就像chroot允许把当前目录变成根目录一样(被隔离开来的),Namespace也可以在一些资源上,将进程隔离起来,这些资源包括进程树、网络接口、挂载点等。
比如,一家公司向外界出售自己的计算资源。公司有一台性能还不错的服务器,每个用户买到一个tomcat实例用来运行它们自己的应用。有些调皮的客户可能不小心进入了别人的tomcat实例,修改或关闭了其中的某些资源,这样就会导致各个客户之间互相干扰。也许你会说,我们可以限制不同用户的权限,让用户只能访问自己名下的tomcat实例,但是,有些操作可能需要系统级别的权限,比如root权限。我们不可能给每个用户都授予root权限,也不可能给每个用户都提供一台全新的物理主机让他们互相隔离。因此,Linux Namespace在这里就派上了用场。使用Namespace,就可以做到UID级别的隔离,也就是说,可以以UID为n的用户,虚拟化出来一个Namespace,在这个Namespace里面,用户是具有root权限的。但是,在真实的物理机器上,他还是那个以UID为n的用户,这样就解决了用户之间隔离的问题。当然这只是Namespace其中的一个简单功能。
除了User Namespace,PID也是可以被虚拟的。命名空间建立系统的不同视图,从用户的角度来看,每一个命名空间应该像一台单独的Linux计算机一样,有自己的init进程(PID为1),其他进程的PID依次递增,A和B空间都有PID为1的init进程,子命名空间的进程映射到父命名空间的进程上,父命名空间可以知道每一个子命名空间的运行状态,而子命名空间与子命名空间之间是隔离的。从图2.1所示的PID映射关系图中可以看到,进程3在父命名空间中的PID为3,但是在子命名空间内,它的PID就是1。也就是说用户从子命名空间A内看进程3就像init进程一样,以为这个进程是自己的初始化进程,但是从整个host来看,它其实只是3号进程虚拟化出来的一个空间而已。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/26_1.jpg?sign=1739303512-qY9JMrFf88Ks21kO2qeT19i1k7ImuD52-0-7be7e2b56a3e144df0cbe415fa4695cd)
图2.1
当前Linux一共实现了6种不同类型的Namespace。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/26_2.jpg?sign=1739303512-t1TzgennIUuc4RiidF31TFjVdvBROleu-0-42c154b62d7b32718d51b1feb1128367)
Namespace的API主要使用如下3个系统调用。
clone()创建新进程。根据系统调用参数来判断哪些类型的Namespace被创建,而且它们的子进程也会被包含到这些Namespace中。
unshare()将进程移出某个Namespace。
setns()将进程加入到Namespace中。
2.1.2 UTS Namespace
UTS Namespace主要用来隔离nodename和domainname两个系统标识。在UTS Namespace里面,每个Namespace允许有自己的hostname。
下面将使用Go来做一个UTS Namespace的例子。其实对于Namespace这种系统调用,使用C语言来描述是最好的,但是本书的目的是去实现Docker,由于 Docker就是使用Go开发的,所以就整体使用Go来讲解。先来看一下如下代码,非常简单。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/27_1.jpg?sign=1739303512-mOVrXFwAnoMdDPOZ58LUAQKhY7cq7NdW-0-7368ad37c7d7db69d421648203cec225)
解释一下代码,exec.Command("sh")用来指定被fork出来的新进程内的初始命令,默认使用sh来执行。下面就是设置系统调用参数,像2.1.1小节中讲到的一样,使用CLONE_NEWUTS这个标识符去创建一个UTS Namespace。Go帮我们封装了对clone()函数的调用,这段代码执行后就会进入到一个sh运行环境中。
在Ubuntu 14.04上运行这个程序,Kernel版本为3.13.0-65-generic,Go版本为1.7.3,执行go run main.go命令,在这个交互式环境里,使用pstree-pl查看一下系统中进程之间的关系,如下。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/28_1.jpg?sign=1739303512-NJXVZLz8ZaB1uGuDRZ0aSJ5vJVCS5GWa-0-3dfeaeda7671f634731ffabd50443430)
然后,输出一下当前的PID,代码如下。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/28_2.jpg?sign=1739303512-nuAKi2wBVjCZK5RLW9jUFa4kyPZRll9I-0-574fe9f0af4ab5451bda511b4e684416)
验证一下父进程和子进程是否不在同一个UTS Namespace中,验证代码如下。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/28_3.jpg?sign=1739303512-hElKzKM1xAGVIJVlGsq8J6xtJtuxrVlP-0-261f9105d05b1b90f49e513282a8192d)
可以看到它们确实不在同一个UTS Namespace中。由于UTS Namespace对hostname做了隔离,所以在这个环境内修改hostname应该不影响外部主机,下面来做一下实验。
在这个sh环境内执行如下代码示例。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/28_4.jpg?sign=1739303512-Z10wrmUXrQQYU9YQ1tiNHrRyrQjdiWU3-0-6bd614780cb065c954153c41f05d96d2)
另外启动一个shell,在宿主机上运行hostname,看一下效果。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/28_5.jpg?sign=1739303512-b1mOBgzNcspcsJVL6kso3frnsZxR7chD-0-5cb81108031246d70a81a4d4c3da92a3)
可以看到,外部的hostname并没有被内部的修改所影响,由此可了解UTS Namespace的作用。
2.1.3 IPC Namespace
IPC Namespace用来隔离System V IPC和POSIX message queues。每一个IPC Namespace都有自己的System V IPC和POSIX message queue。
在上一版本的基础上稍微改动了一下代码。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/29_1.jpg?sign=1739303512-EUtTKwsGUQzbQQxCdo8lGpxUAcZmHANF-0-5f301997078b80065c16b7599c10abce)
可以看到,仅仅增加syscall.CLONE_NEWIPC代表我们希望创建IPC Namespace。下面,需要打开两个shell来演示隔离的效果。
首先在宿主机上打开一个shell。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/29_2.jpg?sign=1739303512-eOLaVxoC4ApP6tasz8fAR2BX4UBMivHE-0-8b5999bad40b61347a5ba123d540b3ae)
这里,能够发现可以看到一个queue了。下面,使用另外一个shell去运行程序。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/30_1.jpg?sign=1739303512-v79OMPE83VigYkimTnWqVKJPmpnEAvsX-0-4f5f1f3c5f987de0398b79544b2fe04d)
通过以上实验,可以发现,在新创建的Namespace里,看不到宿主机上已经创建的message queue,说明IPC Namespace创建成功,IPC已经被隔离。
2.1.4 PID Namespace
PID Namespace是用来隔离进程ID的。同样一个进程在不同的PID Namespace 里可以拥有不同的PID。这样就可以理解,在docker container 里面,使用ps-ef经常会发现,在容器内,前台运行的那个进程PID是1,但是在容器外,使用ps-ef会发现同样的进程却有不同的PID,这就是PID Namespace做的事情。
在2.1.3小节中代码的基础上,再修改一下代码,添加一个syscall.CLONE_NEWPID,代表为fork出来的子进程创建自己的PID Namespace。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/30_2.jpg?sign=1739303512-4nefedTbSiKMnBkGus39bNb98hBbk4gY-0-405e069d75e957cb9e8ffec1b1113ff9)
我们需要打开两个shell。首先在宿主机上看一下进程树,找一下进程的真实PID。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/31_1.jpg?sign=1739303512-nJvUVhbt0fNg5y5Qq7RpAHwwcIczWyxS-0-df332b6ed31dfdf3cd1f80e5c88a12f7)
可以看到,go main函数运行的PID为20190。下面,打开另外一个shell运行一下如下代码。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/31_2.jpg?sign=1739303512-6EXv0uRi4serLYZFcEHoV90FfSy5cixT-0-abe2b9d524ba87d463259baee4a39ae3)
可以看到,该操作打印了当前Namespace的PID,其值为1。也就是说,这个20190的PID被映射到Namespace里后PID 为1。这里还不能使用ps来查看,因为ps和top等命令会使用/proc内容,具体内容在下面的Mount Namespace部分会进行讲解。
2.1.5 Mount Namespace
Mount Namespace用来隔离各个进程看到的挂载点视图。在不同Namespace的进程中,看到的文件系统层次是不一样的。在Mount Namespace中调用mount()和umount()仅仅只会影响当前Namespace内的文件系统,而对全局的文件系统是没有影响的。
看到这里,也许就会想到chroot()。它也是将某一个子目录变成根节点。但是,Mount Namespace不仅能实现这个功能,而且能以更加灵活和安全的方式实现。
Mount Namespace是Linux 第一个实现的Namespace 类型,因此,它的系统调用参数是NEWNS(New Namespace 的缩写)。当时人们貌似没有意识到,以后还会有很多类型的Namespace加入Linux大家庭。
针对2.1.4小节中的代码做了一点改动,增加了NEWNS标识,如下。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/31_3.jpg?sign=1739303512-WYTQMucZBgenqPbVsZqVjJ5uN70gD2SW-0-73e188b5a6a982d38e44b88a0537e965)
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/32_1.jpg?sign=1739303512-bPMw0y6a28o7kDT508U4LeqaR4FbTE9h-0-7a08b73cc4976034159c4e03e92be7f4)
首先,运行代码,然后查看一下/proc的文件内容。proc是一个文件系统,提供额外的机制,可以通过内核和内核模块将信息发送给进程。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/32_2.jpg?sign=1739303512-I1YBsa009KHtf2a4qeIIR9kSJ1psSTwy-0-3c8d46dfffd602375a550bcd8cd16f40)
因为这里的/proc还是宿主机的,所以看到里面会比较乱,下面,将/proc mount到我们自己的Namespace下面来。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/33_1.jpg?sign=1739303512-mzvpzBVPMYJPuU0kSjeUvSj728BuAnlI-0-85e7599b73188839c9f910c634a50700)
可以看到,瞬间少了好多文件。下面就可以使用ps来查看系统的进程了。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/33_2.jpg?sign=1739303512-wX5VjZcIW9nIr9Vcff8Q1os56RWqZyAG-0-9de7cadb5723ece4199493073c7cbea6)
可以看到,在当前的Namespace中,sh 进程是PID 为1 的进程。这就说明,当前的Mount Namespace 中的mount 和外部空间是隔离的,mount 操作并没有影响到外部。Docker volume也是利用了这个特性。
2.1.6 User Namespace
User Namespace 主要是隔离用户的用户组ID。也就是说,一个进程的User ID 和Group ID在User Namespace内外可以是不同的。比较常用的是,在宿主机上以一个非root用户运行创建一个User Namespace,然后在User Namespace里面却映射成root 用户。这意味着,这个进程在User Namespace里面有root权限,但是在User Namespace外面却没有root的权限。从Linux Kernel 3.8开始,非root进程也可以创建User Namespace,并且此用户在Namespace里面可以被映射成root,且在Namespace内有root权限。
下面,继续以一个例子来描述,代码如下。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/34_1.jpg?sign=1739303512-00kkuczxAglSUl1Pl1x1EwV4Undt89Pd-0-30805cc8e92f8fbea2c85f34166f285c)
本例在原来的基础上增加了syscall.CLONE_NEWUSER。首先,以root来运行这个程序,运行前在宿主机上看一下当前的用户和用户组,显示如下。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/34_2.jpg?sign=1739303512-2s5Jnx5Uf7kymVZQbTrCmNb4A5t91Ib4-0-c18b6d9b6708f699811d42619d88d966)
可以看到我们是root用户,接下来运行一下程序。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/34_3.jpg?sign=1739303512-6BG6uZEw7DAHqxMKxErgpPZihBkY5pqA-0-6c42434589258c9c07d158ff4fa9e0fe)
可以看到,它们的UID是不同的,因此说明User Namespace生效了。
2.1.7 Network Namespace
Network Namespace 是用来隔离网络设备、IP地址端口等网络栈的Namespace。Network Namespace可以让每个容器拥有自己独立的(虚拟的)网络设备,而且容器内的应用可以绑定到自己的端口,每个Namespace内的端口都不会互相冲突。在宿主机上搭建网桥后,就能很方便地实现容器之间的通信,而且不同容器上的应用可以使用相同的端口。
同样,在2.1.6小节的代码的基础上增加syscall.CLONE_NEWNET标识符,如下。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/35_1.jpg?sign=1739303512-6p4NS2haWMxdl9CkTOCiNr21eazB7YCq-0-e1620c16b3d1efb0a2958da94282925b)
首先,在宿主机上查看一下自己的网络设备,结果如下。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/35_2.jpg?sign=1739303512-0m0sL1yb5C1BzzrTttFDd3hFigJXK74i-0-f1e3e8ebed1107316944ce5376205c25)
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/36_1.jpg?sign=1739303512-VS5KMDrihpwK6Pw8d2aEW6jJ4LQslTl7-0-08fcfd1c96443c5e9d8904b4108fc47f)
可以看到,宿主机上有lo、eth0、eth1 等网络设备。下面,运行一下程序去Network Namespace里面看看。
![](https://epubservercos.yuewen.com/5CFC20/18978713008548006/epubprivate/OEBPS/Images/36_2.jpg?sign=1739303512-nsi4FpVmMXTn9sQ5rFvfYqpZRy3FU3jz-0-a58df6ca9367410d8a7d88f129e96c59)
我们发现,在Namespace里面什么网络设备都没有。这样就能断定Network Namespace与宿主机之间的网络是处于隔离状态了。