第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号进程虚拟化出来的一个空间而已。

图2.1

当前Linux一共实现了6种不同类型的Namespace。

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来讲解。先来看一下如下代码,非常简单。

解释一下代码,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查看一下系统中进程之间的关系,如下。

然后,输出一下当前的PID,代码如下。

验证一下父进程和子进程是否不在同一个UTS Namespace中,验证代码如下。

可以看到它们确实不在同一个UTS Namespace中。由于UTS Namespace对hostname做了隔离,所以在这个环境内修改hostname应该不影响外部主机,下面来做一下实验。

在这个sh环境内执行如下代码示例。

另外启动一个shell,在宿主机上运行hostname,看一下效果。

可以看到,外部的hostname并没有被内部的修改所影响,由此可了解UTS Namespace的作用。

2.1.3 IPC Namespace

IPC Namespace用来隔离System V IPC和POSIX message queues。每一个IPC Namespace都有自己的System V IPC和POSIX message queue。

在上一版本的基础上稍微改动了一下代码。

可以看到,仅仅增加syscall.CLONE_NEWIPC代表我们希望创建IPC Namespace。下面,需要打开两个shell来演示隔离的效果。

首先在宿主机上打开一个shell。

这里,能够发现可以看到一个queue了。下面,使用另外一个shell去运行程序。

通过以上实验,可以发现,在新创建的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。

我们需要打开两个shell。首先在宿主机上看一下进程树,找一下进程的真实PID。

可以看到,go main函数运行的PID为20190。下面,打开另外一个shell运行一下如下代码。

可以看到,该操作打印了当前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标识,如下。

首先,运行代码,然后查看一下/proc的文件内容。proc是一个文件系统,提供额外的机制,可以通过内核和内核模块将信息发送给进程。

因为这里的/proc还是宿主机的,所以看到里面会比较乱,下面,将/proc mount到我们自己的Namespace下面来。

可以看到,瞬间少了好多文件。下面就可以使用ps来查看系统的进程了。

可以看到,在当前的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权限。

下面,继续以一个例子来描述,代码如下。

本例在原来的基础上增加了syscall.CLONE_NEWUSER。首先,以root来运行这个程序,运行前在宿主机上看一下当前的用户和用户组,显示如下。

可以看到我们是root用户,接下来运行一下程序。

可以看到,它们的UID是不同的,因此说明User Namespace生效了。

2.1.7 Network Namespace

Network Namespace 是用来隔离网络设备、IP地址端口等网络栈的Namespace。Network Namespace可以让每个容器拥有自己独立的(虚拟的)网络设备,而且容器内的应用可以绑定到自己的端口,每个Namespace内的端口都不会互相冲突。在宿主机上搭建网桥后,就能很方便地实现容器之间的通信,而且不同容器上的应用可以使用相同的端口。

同样,在2.1.6小节的代码的基础上增加syscall.CLONE_NEWNET标识符,如下。

首先,在宿主机上查看一下自己的网络设备,结果如下。

可以看到,宿主机上有lo、eth0、eth1 等网络设备。下面,运行一下程序去Network Namespace里面看看。

我们发现,在Namespace里面什么网络设备都没有。这样就能断定Network Namespace与宿主机之间的网络是处于隔离状态了。