容器是如何拥有自己的“小世界” - Namespace 和 CGroups(上)

300 阅读6分钟

上篇我们知道了容器在自己的“小世界”里能够看到什么,看到的规则是什么。这篇我们来看看,容器如何拥有了自己的“小世界”。 想要知道容器是如何拥有了自己的“小世界”,需要回答以下两个问题

  • 容器“小世界”的边界是如何产生的,它是怎么看不到外面世界的。
  • 容器“小世界”的活动范围是如何固定的,它是怎么在自己的地盘安分守已,不会干扰到其他容器的。

容器“小世界”的边界是如何产生的,它是怎么看不到外面世界的 让我们先运行容器并进入到容器内部,还是用之前 Ubuntu 的镜像。 docker run -it ubuntu:latest /bin/sh 接着执行 ps 指令,发现只有两个进程在运行着,一个是我们进入容器时执行的 /bin/sh,pid 是 1,另一个是 ps 指令。

容器内进程视图

接着在容器内部执行 top 命令。我们再来看看宿主机上的进程列表,使用命令 ps -ef。

宿主机进程视图

# 使用 { docker top 容器id } 命令,可以更便捷地查看容器在宿主机上映射后的进程信息 UID PID PPID C STIME TTY TIME CMD root 914 894 0 18:15 pts/0 00:00:00 /bin/sh root 1046 914 0 18:16 pts/0 00:00:00 top 发现在宿主机上新增和容器内部对应的两个进程,但是和容器里看到的视图完全不一样。容器中的“小世界”到底是怎么做到与世隔绝的呢? 我们知道容器是运行在宿主机上的,容器内运行的进程理所当然地也会运行在宿主机上。但是在容器内部的视图是看不到容器外的进程,而且容器内进程的 PID 也是容器内部自行计算的(譬如上图的 /bin/sh,容器内部 PID 为 1,宿主机 PID 为 914),和宿主机上真实的进程 PID 不一样。也就是说,Docker 在宿主机上创建容器时,做了一些手脚,使得容器的进程空间和宿主机隔离开,并且容器内的进程编号也会重新计算。这就是 Linux 的 Namespace 技术。 Namespace 的本质其实是一种资源隔离方案。为不同的进程集合提供不同的 Namespace,不同进程集合彼此不能访问其对应的 Namespace。在同一 Namespace 下的进程可以感知彼此的存在和变化,而对外界的进程一无所知。这样利用 Namespace 机制将每个容器的资源隔离开,每个容器就有了自己的“小世界”。 Namespace 既然可以做到“隔离”资源,那么一定会涉及三个行为:

  • 创建进程时,加入到指定的 Namespace 中,即 Linux clone() 系统调用,创建不同资源类型的 Namespace,并将新建的进程作为其 Namespace 的一个成员。
  • 将某进程加入到某个已存在的 Namespace 中,即 Linux setns () 系统调用。
  • 将某进程脱离出指定的 Namespace 中,即 Linux unshare() 系统调用。

使用以上三个系统调用,可以实现对不同资源进行隔离。从 Linux 3.8 版本内核之后,可以在 /proc/{ pid }/ns 文件下看到指向不同 Namespace 编号的文件。如果 namespace 编号相同,则说明是在同一个 Namespace 下。

img
[ ] 中的数字就是 namespace 编号

分别对应了 Linux 下六种不同的 Namespace: PID Namespace

  • 对进程 PID 重新编号,可以在 Namespace 下可以有相同的 PID。每个 PID Namespace 都有自己的计数程序。
  • 当新建一个 PID Namespace 时,默认启动进程 PID 为 1。在 UNIX 系统中,PID 为 1 的进程是 init 进程,即所有进程的父进程,当某个进程成了孤儿进程时,init 进程负责回收资源并结束这个子进程。Docker 启动时,第一个进程就是 dockerinit,用来监控进程和回收资源。
  • 除了 SIGKILL(销毁进程) 和 SIGSTOP(暂停进程),默认情况下,init 进程会屏蔽接受到的信号量。
  • 更多关于 PID Namespace 的信息可以查看下面的链接

hustcat.github.io/pid-namespa…

Mount Namespace

  • 隔离文件系统挂载点,使得被隔离后只能看到当前 Namespace 里的挂载点信息,也是 Linux 历史上第一个 Namespace。可以在 /proc/{ pid }/mounts 下查看到挂载在当前 Namespace 下的文件系统。
  • 更多Linux 挂载特性可以查看下面的链接

www.ibm.com/developerwo…

IPC ( Interprocess Communication ) Namespace

  • 容器内部的进程间通信对宿主机来说,实际上是具有相同 PID Namespace 中的进程间通信,因此需要一个唯一的标识符来进行区别。申请 IPC 资源就申请了这样一个全局唯一的32位 ID。

UTS ( UNIX Time-sharing System ) Namespace

  • 提供主机名和域名的隔离。使得每个容器可以拥有独立的主机名和域名,在网络上被视为一个独立的结点,而不是宿主机上的一个进程。

Network Namespace

  • 提供了关于网络资源的隔离,包括网络设备、IPv4 和 IPv6 协议栈,IP 路由表、防火墙、端口(socket)等等
  • 关于更多的信息可以查看下面的链接

ipvlan: Initial check-in of the IPVLAN driver.lwn.net

User Namespace

  • 主要是用于隔离用户、特殊权限的相关信息。隔离后的进程可以在新的 User Namespace 中拥有不同的用户和用户组。这个要注意,在容器内部最好以非 root 用户身份运行应用,如果有必须要以 root 用户运行的容器,可以将用户重新映射到宿主机上权限较低的用户,这样可以防止对宿主机的一些攻击。
  • 在 User Namespace 中创建的第一个进程被赋予了在此 Namespace 中的全部权限,这是为了 init 进程能够完成必要的初始化工作。
  • 更多的信息,可以查看以下的链接

docs.docker.com/engine/secu…

从上面的 Linux Namespace 可以看出来,有些资源和对象是不能被 Namespace 的,例如时间。一旦某个容器修改了时间,会影响到在宿主机上的全部容器。这也是容器和虚拟机相比最主要的问题:隔离的不够彻底。 容器的“小世界”就是被各种 Namespace 限制了“视图”的进程,只能看到自己被指定的“小世界”。但是对宿主机来说,这些被“隔离”了的进程其实跟其他的进程没有太大的区别,容器只是运行在宿主机上一种特殊的进程。 既然容器在宿主机上与其他的进程无异,它们之间就存在着资源竞争的关系,那 Docker 又是怎么保证容器运行时的资源呢?下篇就来看看另一个问题:容器“小世界”的活动范围是如何固定的,它是怎么在自己的地盘安分守已,不会干扰到其他容器的。 最后放上 Linux Namespace 的官方文档~

lwn.net/Articles/53…