揭秘容器(一):内核空间 chroot & namespace
潘忠显 / 2023-10-16
本文在翻译并补充了 CNCF 上的博客,系列文章共4篇,旨在从容器发展的历史角度,带领读者认识容器。文章会通过简单的示例,结合历史背景,引导你从最小 Linux 环境开始认识内核层次的一些技术,一直到构建安全容器,逐层地去认识现代云架构。
最终目的,是更深入地理解 Linux 内核、容器工具、运行时、软件定义网络和编排软件(如 Kubernetes)的设计理念、底层工作原理,完美地适应当今和未来的容器编排世界。
本篇为系列文章的第一篇,介绍 Linux 内核相关主题,提供一些必要的基础知识来理解容器。深入了解 UNIX、Linux 的历史,并结合示例来讨论 chroot
、命名空间和 cgroup 等容器解决方案。
1. 介绍
如今当我们谈起容器,大多数人往往会想到大蓝鲸,或者蓝底白色的方向盘。
让我们把这些想法放在一边,问问自己:容器到底是什么?
如果我们查看 Kubernetes 的相应文档,我们只能找到关于“为什么要使用容器?”的解释,以及大量 Docker 的参考文献。Docker 本身将容器解释为“软件的标准单元”。他们的解释仅仅提供了一个概述,却没对底层技术有太多解密。这导致人们倾向于将容器想象为廉价的虚拟机(VM),而无法去接近技术的真相。
从字面上,“Container” 这个词,确实很难让人去形象地理解其真正的含义,在容器编排生态中的另外一个词 “Pod” 也是这样。
如果我们把它拆下来,容器只是在单个主机上运行的独立进程组,它们符合一组通用特性。其中部分神奇特性,是直接内置于 Linux内核中的,而且它们大多有不同的历史来源。
容器必须满足四个主要要求才能被接受:
- 它们必须在单个主机上运行,两台计算机不能运行一个容器。
- 它们是进程组。Linux 进程以树形结构组织,而容器必须有一个根进程(root process)。
- 它们需要被隔离,无论这具体意味着什么。
- 它们必须满足一些通用特性,而这些特性通常会随着时间而变化,因此我们必须指出最常见的特性是什么。
仅这些要求就会导致混乱,而且并不能给我们展现一个清晰的画面。因此,为了简单起见,让我们从历史起源开始吧。
2. chroot
大多数 UNIX 操作系统都可以更改当前正在运行的进程(及其子进程)的根目录。最早是在 1979 年在第七版 Unix 开发期间首次引入的 chroot,并于 1982 年被添加到 BSD(Berkeley Software Distribution)。在 Linux 中 chroot(2)
被作为系统调用或相应的独立包装程序 chroot(1)
使用,但很明显 chroot 比 Linux 出现的更早。
Chroot 也被称为“监狱(jail)”,因为早在 1991 年就有人将其用作蜜罐来监控黑客;2000 年,FreeBSD 将 chroot 扩展到 FreeBSD jail
命令;在 2005 年,Solaris 引入了区域(Solaris Zones)的概念,用于操作系统服务虚拟化。
2.1 使用简单的根目录
使用 chroot,可以更改当前正在运行的进程及其子进程的根目录。调用 chroot 后,后续命令将针对新根目录 (/
) 运行。chroot 目前仍被应用程序广泛地使用,例如不同发行版的在构建服务过程中。chroot 在 BSD 上的实现与 Linux 的实现有很大不同,我们重点关注 Linux 上的版本。
仅仅需要如下的几步,就能运行自己的 chroot 环境:
> mkdir -p new-root/{bin,lib64,root}
> cp /bin/bash new-root/bin
> cp /lib64/{ld-linux-x86-64.so*,libc.so*,libdl.so.2,libreadline.so*,libtinfo.so*} new-root/lib64
> sudo chroot new-root
上边首先创建一个新的根目录,然后将 Bash shell 及其依赖项复制到并运行chroot
。这个 jail 毫无用处:只有 bash 及其内置函数,例如 cd
和 pwd
。
2.2 使用容器的根目录
我们可以从现有的容器中,剥离出其根文件系统(rootfs),包含所有二进制文件、库和必要的文件结构。 前提是这些容器镜像遵循开放容器规范 (OCI)。剥离过程,可以使用两个工具 skopeo
和 umoci
:
- skopeo 是由 containers 提供远程仓库的镜像管理工具,包括:复制镜像、检索镜像信息、删除镜像、从公共镜像仓库向私有仓库同步镜像等,在CentOS 中可以直接使用 yum 安装
- umoci (umoci modifies Open Container images) 是OCI 镜像规范的参考实现,为用户提供创建、操作容器镜像以及以其他方式与容器镜像交互的能力,在CentOS 上得使用 https://github.com/opencontainers/umoci 源码
make install
安装
> skopeo copy docker://opensuse/tumbleweed:latest oci:tumbleweed:latest
> umoci unpack --image tumbleweed:latest bundle
然后使用刚下载并提取的 rootfs,通过 chroot
方式进入监狱:
> sudo chroot bundle/rootfs
为了比较 chroot
前后的不同,这里直接看 /etc/os-release
中的 NAME。看上去我们已经进入到一个新的完整环境:
2.3 不满足隔离要求
在上边的操作中,看上去像是跟使用 docker run -ti opensuse/tumbleweed:latest /bin/bash
的效果差不多,那是不是这样就算运行容器了呢?
绝对不是,chroot 只是改变了根目录,而非创建了真正的独立隔离、安全环境。
a. 文件系统没有隔离
调用系统调用 chroot(2)
时,在当前工作目录下,使用相对路径仍引用新根目录之外的文件。
该调用仅会更改根路径,不会更改其他任何内容。另外,对 chroot 的进一步调用会覆盖当前的监狱。只有具有该能力的特权进程 CAP_SYS_CHROOT
才能调用 chroot。root 用户可以通过运行如下程序轻松逃离监狱:
#include <sys/stat.h>
#include <unistd.h>
int main(void) {
mkdir(".out", 0755);
chroot(".out");
chdir("../../../../../");
chroot(".");
return execl("/bin/bash", "-i", NULL);
}
上边的代码中,通过覆盖当前的监狱来创建一个新的监狱,并将工作直接更改为 chroot 环境之外的某个相对路径,再次调用 chroot 可以将我们带出监狱。这可以通过生成新的交互式 bash shell 来验证(在简单的示例的根目录下,没有 ls
指令;在运行上述程序之后,因为切换到主机的根目录,可以运行 ls
):
如今,容器运行时不再使用 chroot,而是使用取代 chroot 的 pivot_root(2)
,它的好处是:在调用时会将旧的挂载放入单独的目录中;之后可以卸载这些旧的挂载,以使文件系统对中断的进程完全不可见。
b. 进程没有隔离
在真实 Linux 机器上,我们通过 pstree -p
可以看到,PID=1的是systemd进程。
进入到 Docker 容器调试时,我们可以看到 PID=1的是我们通过 docker run
指令指定的程序。容器内的PID=1和容器外的PID=1的进程是完全独立的进程,这也就是进程隔离:
而仅仅通过 chroot 运行的 /bin/bash 并非1号进程,我们可以通过 echo $$
打印当前 shell 的PID来观察:
在我们从镜像中提取 rootfs 示例的基础上,我们可以通过 mount
挂载外部的 /proc(后文有详细介绍),看到外部进程的情况,甚至可以杀死监狱之外运行的进程:
c. 设备没有隔离
与 mount
proc 类似,如果将 sys 进行挂载,也能看到所有的设备,比如网卡:
因此,缺少隔离和具备离开监狱的能力,会导致许多与安全相关的问题。如何解决这个问题?这就是下一节要介绍的 Linux 命名空间的功能。
d. mount 之后的清理
因为上边的 bundle 目录中有 mount proc 和 sys 的操作,直接在外部 rm -rf bundle
目录是无法删除的,需要使用 umount
命令之后才可以删除:
> umount bundle/rootfs/{sys,proc}
> rm -rf bundle &> /dev/null
3. Linux 命名空间
命名空间是一项 Linux 内核功能,于 2002 年随 Linux 2.4.19 引入。命名空间背后的想法是将特定的全局系统资源包装在抽象层中。内核命名空间抽象,允许不同的进程组具有不同的系统视图。这使得命名空间内的进程看起来拥有自己的全局资源的隔离实例。
目前 Linux 实现了 8 个不同的命名空间:mnt、pid、net、ipc、uts、user、cgroup、time,这8个命名空间并非从一开始就实现的,而是在不断更新的 Linux 内核版本中逐渐实现的。
内核为每个进程分配一个符号链接 /proc/<pid>/ns/
。对于此命名空间中的每个进程,此符号链接指向的 inode 编号都是相同的。这通过其符号链接之一指向的索引节点号来唯一标识每个命名空间。
util-linux 包中有提供 lsns
命令,可以罗列所有当前可访问的命名空间,或有关单个给定命名空间的有用信息。比如上边我们看到的 bash 所在的命名空间,是systemd创建的;而我们通过 docker run
运行的容器,其使用的 namespace 则是启动容器的父进程 /bin/bash 创建的:
在深入研究这些命名空间之前,让我们先了解一下命名空间相关的 API。
3.1 命名空间相关 API
Linux 中有三个系统调用可以直接操作命名空间,分别是 clone()
、unshare()
和 setns()
。
clone()
clone(2)
函数可以创建一个新的子进程,其方式类似于 fork(2)
。fork(2)
创建新进程时,会隐式地共享父进程的一些执行上下文,比如 mount 命名空间;而 clone(2)
可以让子进程显式地共享父进程的部分执行上下文,例如内存空间、文件描述符表和信号处理程序表。
可以传递不同的命名空间标志来clone(2)
为子进程创建新的命名空间,这些标志包括:CLONE_NEWNS
, CLONE_NEWIPC
, CLONE_NEWNET
等,与上边介绍的 8 种命名空间对应。
unshare()
unshare(2)
允许进程解除当前与其他进程共享的部分执行上下文的共享,而不需要新创建进程。这里要理解,解除共享并非都会变成空,有的会将旧的复制一份,然后解除共享关联。
很多系统调用会有对应的命令对应,比如 unshare(1)
就是对应该系统调用的命令工具,后文中我们会看到 unshare(1)
的使用。
setns()
setns(2)
可将进程与命名空间重新关联,其参数是指向命名空间的文件描述符。
命令 nsenter(1)
可以从某个命名空间运行程序,或者说加入到某个命名空间,这个跟 setns(2)
有点联系。
3.2 可用命名空间
从 Linux 内核 5.6 版本开始,有 8 种命名空间。所有类型的命名空间功能都是相同的:每个进程都与一个命名空间关联,并且只能查看或使用该命名空间以及后代命名空间关联的资源。这样,每个进程(或其进程组)都可以对资源有单独的视图。隔离哪些资源取决于为给定进程组创建的命名空间类型。
挂载 (mnt)
每个进程都有挂载命名空间(mount namespace),该命名空间用于控制挂载点。系统调用 mount(2)
和 umount(2)
可以修改调用进程的挂载命名空间,并且影响在相同命名空间中的所有进程。
clone(2)
如果设置 CLONE_NEWNS
标志,会给创建的子进程新建一个挂载命名空间,这个新的挂载命名空间是通过复制父进程的挂载命名空间来进行初始化的。通过 fork(2)
或者不设置 CLONE_NEWNS
的 clone(2)
创建的子进程,和父进程拥有相同的挂载命名空间。
这里提到的 clone 标志是 CLONE_NEWNS
,既 NEW NameSpace 的含义,不能明确的描述是创建 mnt 类型的新命名空间。这跟后边介绍的如 pid 命名空间的 CLONE_NEWPID
有些差别,因为 mnt 是第一个实现的命名空间,当时设计者没有预料到的之后还会有其他的命名空间。
mnt 命名空间的一个应用场景是以更安全的方式创建类似于监狱的环境。如何创建这样的命名空间?这可以通过 API 函数调用或命令行工具轻松完成:
- 通过
unshare -m
将当前进程(bash)的mount命名空间解除与父进程的关联 - 通过
mount
将 tmpfs 挂载到文件夹 - 在新的挂载空间中的挂载点目录里,进行文件操作
> sudo unshare -m
# mkdir mount-dir
# mount -n -o size=10m -t tmpfs tmpfs mount-dir
# df mount-dir
# touch mount-dir/{0,1,2}
我们再另外起一个 bash 进程,使用的仍然是之前的挂载命名空间,是看不到该目录下有任何文件。
这里挂载的 tmpfs 是一个虚拟文件系统(VFS),跟 /proc 一样都是只在内存当中,它是内核的一部分,后边会写一篇小文介绍。如果命名空间被破坏,挂载内存就会丢失且无法恢复。
查看挂载的有几种常用的方式:
cat /proc/{pid}/mountinfo
可以查看某进程的挂载信息mount -l
可以显示当前环境的挂载信息df -h
可以查看当前环境的挂载信息
挂载命名空间抽象使我们能够创建整个虚拟环境,即使没有 root 权限,我们也是 root 用户。
在主机系统上,我们可以通过文件系统 mountinfo
内的文件查看挂载点 proc
:
> grep mount-dir /proc/$(pgrep -u root bash)/mountinfo
349 399 0:84 / /mount-dir rw,relatime - tmpfs tmpfs rw,size=1024k
如何在源代码级别使用这些挂载点?程序往往会在相应的/proc/$PID/ns/mnt
文件上保留一个文件句柄,它引用所使用的命名空间。
最后,与挂载命名空间相关的实现场景可能非常复杂,但它们使我们能够创建灵活的容器文件系统树。挂载可以有不同的风格(共享、从属、私有、不可绑定),这在Linux 内核的共享子树文档中得到了最好的解释。
进程ID(pid)
PID 命名空间是在 Linux 2.6.24 (2008) 中引入的,它为进程提供了一组独立的进程标识符 (PID)。这意味着驻留在不同命名空间中的进程可以拥有相同的 PID。一个进程有两个 PID:命名空间内的 PID,以及主机系统上命名空间外的 PID。PID 命名空间可以嵌套,因此如果创建一个新进程,从当前命名空间到初始 PID 命名空间,每个命名空间都会有一个 PID。
在 PID 命名空间中创建的第一个进程的编号为 1,并获得与通常的 init 进程相同的特殊待遇。例如,命名空间内的所有进程将重新设置为命名空间的 PID 1,而不是主机 PID 1。此外,终止此进程将立即终止其 PID 命名空间中的所有进程及其任何后代(有没有在容器中直接杀掉试试?)。
让我们创建一个新的 PID 命名空间:
> sudo unshare -fp --mount-proc
看起来是已经做到了隔离。--mount-proc
标志表示在新的命名空间重新挂载 proc 文件系统,只有这样,我们才能看到与命名空间对应的 PID 子树。
另一种选择是通过手动挂载 proc 文件系统 mount -t proc proc /proc
,但这也会覆盖主机上的挂载,之后必须重新挂载。
回顾之前我们介绍 “b.进程没有隔离” 的小节中,也有调用 mount -t proc proc /proc
,但是看到的却是主机系统的进程,原因是我们没有调用 unshare -fp
来创建新的 PID 命名空间,而是跟老的 bash 在同一个 PID 命名空间当中。
网络(net)
网络命名空间在 Linux 2.6.29 (2009) 中完成,可用于虚拟化网络协议栈。每个网络命名空间都包含其自己的资源属性/proc/net
。此外,网络命名空间在初始创建时,会包含一个环回接口。
让我们解除与当前网络命名空间的共享:
> sudo unshare -n
每个网络接口(物理或虚拟)在每个命名空间中仅会出现一次。接口可能会在命名空间之间移动。(???)
每个命名空间都包含一组私有 IP 地址、其自己的路由表、套接字列表、连接跟踪表、防火墙和其他网络相关资源。
销毁网络命名空间会销毁所有虚拟接口,将所有物理接口移回初始网络命名空间。
网络命名空间的一个可能的用例是通过虚拟以太网 (veth) 接口对创建软件定义网络(SDN) 。网络对的一端接入桥接接口,而另一端将分配给目标容器。这就是像 flannel 这样的 pod 网络的工作方式。
接下来,看看具体是如何工作的。首先,我们需要创建一个新的网络命名空间,这 ip
也可以通过以下方式完成:
> sudo ip netns add mynet
> sudo ip netns l
mynet
因此,我们创建了一个名为 myset 的新网络命名空间。创建网络命名空间使用 ip
命令,会在/var/run/netns
下创建绑定挂载 mynet
。即使没有进程在其中运行,这也允许命名空间持续存在。
我们 ip netns exec
可以进一步检查和操作我们的网络命名空间:
网络是关闭的,可以打开它:
> sudo ip netns exec mynet ip link set dev lo up
现在让我们创建一个 veth 对,以便稍后进行通信:
> sudo ip link add veth0 type veth peer name veth1
> sudo ip link show type veth
两个接口都会自动连接,这意味着发送到的数据包veth0
将被 veth1
接收,反之亦然。现在我们将 veth 对的一端关联到我们的网络命名空间:
> sudo ip link set veth1 netns mynet
> ip link show type veth
我们的网络接口肯定需要一些地址:
> sudo ip netns exec mynet ip addr add 172.2.0.1/24 dev veth1
> sudo ip netns exec mynet ip link set dev veth1 up
> sudo ip addr add 172.2.0.2/24 dev veth0
> sudo ip link set dev veth0 up
现在应该可以进行双向通信:
> ping -c1 172.2.0.1
> sudo ip netns exec mynet ping -c1 172.2.0.2
已经可以工作,但至此我们仍然无法从网络命名空间访问任何互联网。
我们需要一个网桥或类似的东西,以及来自命名空间的默认路由。
我把这个任务留给你,现在让我们继续下一个命名空间。
UNIX 分时系统 (uts)
UTS 命名空间是在 Linux 2.6.19 (2006) 中引入的,它允许我们从当前主机系统取消共享域和主机名。
> sudo unshare -u
UTS 命名空间是容器化的一个很好的补充,特别是在涉及容器网络相关主题的场景。
进程间通信(ipc)
Linux 2.6.19 (2006) 也附带了 IPC 命名空间,它隔离进程间通信 (IPC) 资源,包括 System V IPC 对象和 POSIX 消息队列。IPC 命名空间也可以分隔两个进程之间的共享内存 (SHM):每个进程将能够对共享内存段使用相同的标识符,并生成两个不同的区域,以免误用。当一个 IPC 命名空间被销毁时,该命名空间中的所有 IPC 对象也会自动被销毁。
用户ID(user)
在 Linux 3.5 (2012) 中,用户和组 ID 的隔离终于可以通过命名空间实现。Linux 3.8 (2013) 使得创建用户命名空间成为可能,即使没有实际特权。
用户命名空间使得进程的用户和组 ID 在命名空间内部和外部可以不同。比如,进程可以在用户命名空间之外拥有普通的非特权用户 ID,同时在内部拥有完全特权。
> id -u
> unshare -U
创建用户命名空间后,文件 /proc/$PID/{u,g}id_map
会公开 PID 的用户 ID 和组 ID 的映射关系。一般来说,这些文件中的每一行都包含两个用户命名空间之间一系列连续用户 ID 的一对一映射,类似于下边这样:
上面的示例转换为: 对于 root 用户 ID 0,命名空间映射到从 ID 1000 开始的范围。这仅适用于 ID 为 的用户 1000
,因为定义的长度为1
。
如果现在一个进程尝试访问一个文件,它的用户和组 ID 会被映射到初始用户命名空间以进行权限检查。当进程检索文件用户和组 ID(通过stat(2)
)时,ID 会按相反方向映射。
在取消共享示例中(我们上面做的),我们 getuid(2)
在编写适当的用户映射之前隐式调用,这将导致未映射的 ID。该未映射的 ID 会自动转换为溢出用户 ID(65534 或 中的值/proc/sys/kernel/overflow{g,u}id
)。
该文件 /proc/$PID/setgroups
包含使用 allow
或 deny
来启用或禁用在用户命名空间内调用系统调用的权限。setgroups(2)
添加该文件是为了解决用户命名空间引入的额外安全问题:非特权进程可以创建一个用户拥有所有权限的新命名空间。这个以前没有特权的用户将能够通过删除组 setgroups(2)
来访问他以前没有的文件。
用户命名空间为容器世界带来了巨大的安全性,这对于运行无根容器(rootless container)至关重要。
控制组群(cgroup)
cgroup 的主要目标是支持资源限制、优先级、核算和控制,于 2008 年作为 Linux 2.6.24 内核功能被加入。Linux 4.6 (2016) 中添加了 cgroup 命名空间,以防止将主机信息泄漏到命名空间中,同时发布了从 2013 年开始进行重新设计的 cgroups 的第二个版本。最新的一个例子是 OOM Killer 它增加了将 cgroup 作为单个单元终止的功能,以保证工作负载的整体完整性。
我们首先创建一个新的 cgroup,只需在 cgroup 目录下(默认在 /sys/fs/cgroup
)创建一个新的子目录:
> sudo mkdir /sys/fs/cgroup/memory/demo
> ls /sys/fs/cgroup/memory/demo
您可以看到那里已经添加了一些默认值。现在,我们可以为该 cgroup 设置内存限制并关闭交换,以使我们的示例能被杀掉。
要将进程分配给 cgroup,我们可以将相应的 PID 写入文件cgroup.procs
:
> sudo su
# echo 100000000 > /sys/fs/cgroup/memory/demo/memory.limit_in_bytes
# echo 0 > /sys/fs/cgroup/memory/demo/memory.swappiness
# echo $$ > /sys/fs/cgroup/memory/demo/cgroup.procs
现在我们可以执行一个示例应用程序来消耗超过允许的 100 MB 内存。
> yes | sponge
上边的命令会不断的通过管道,给 sponge 输入 ‘y’ 字符。我们会看到由于设置的内存限制,sponge 进程会很快被终止。而我们的主机系统仍然可以使用。
为了解释之前在容器中的一个现象,我们这里再演示一下指定CPU核心和利用率的 cgroup 的用法:
> # 设置只使用 2,3 两个核
> mkdir -p /sys/fs/cgroup/cpuset/demo
> echo "2,3" > /sys/fs/cgroup/cpuset/demo/cpuset.cpus
> echo "0" > /sys/fs/cgroup/cpuset/demo/cpuset.mems
> echo $$ > /sys/fs/cgroup/cpuset/demo/cgroup.procs
> # 设置CPU利用率限制为50%
> mkdir -p /sys/fs/cgroup/cpu/demo
> echo "100000" > /sys/fs/cgroup/cpu/demo/cpu.cfs_quota_us
> echo "100000" > /sys/fs/cgroup/cpu/demo/cpu.cfs_period_us
> echo $$ > /sys/fs/cgroup/cpu/demo/cgroup.procs
> yes > /dev/null&
> yes > /dev/null&
3.3 组成命名空间
这小节英文原文是 Composing namespaces。这里 compose 有创作歌曲诗歌、构成组成等含义,不是使用 combine,因此其含义并不是说将多个 namespaces 组合到一起,而是多个进程可以组合起来,使用同一个命名空间的意思。就像在 Kubernetes Pod 中那样,一个 Pod 中可以有多个容器,而这些容器共享存储、网络、隔离 pid 等资源。
为了演示这一点,让我们创建一个具有隔离 PID 的新命名空间:
> sudo unshare -fp --mount-proc
# ps aux
现在可以使用对系统调用 setns(2)
封装的 nsenter
命令来加入到上述命名空间中。为此,我们必须找出我们想要加入哪个命名空间:
> export PID=$(pgrep -u root bash)
> sudo ls -l /proc/$PID/ns
现在,可以通过以下方式轻松加入命名空间 nsenter
:
> sudo nsenter --pid=/proc/$PID/ns/pid unshare --mount-proc
# ps aux
现在我们可以看到我们是同一个 PID 命名空间的成员!另外,也可以通过 nsenter
进入已经运行的容器,但这个主题将在稍后介绍。
4. 将所有内容放在一起
4.1 演示应用程序
本小节将提供一个小型应用程序,可用于演示通过命名空间 API 创建一个简单的隔离环境。
该程序的目的是在新的命名空间中生成一个新的子进程,并在子进程中执行参数中的命令。命令执行完成后,应用程序终止。您可以通过以下方式测试和验证实施:
> gcc -o namespaces namespaces.c
> ./namespaces ip a
> ./namespaces ps aux
> ./namespaces whoami
实际运行结果如下。这里需要强调一下,因为有些 clone(2)
中的 flag 是在很晚才加入的,所以如果依赖老的 libc.so,会报参数错误(如下图左边)。正常的编译和执行结果见下图右侧:
这确实不是一个工作容器,但它应该让您对容器运行时如何利用命名空间来管理容器有一点感觉。此示例作为您自己使用命名空间 API 进行小实验的起点。
4.2 使用容器运行时
还记得我们从 chroot 部分中的映像中提取的 rootfs 吗?我们可以使用低层次的容器运行时,例如 runc
轻松地从 rootfs 运行容器:
> runc run -b bundle container
如果我们现在检查系统命名空间,我们会看到 runc
已经为我们创建了 mnt、uts、ipc、pid 和 net 的命名空间:
有关容器运行时及其用途的更多信息,将会下一篇文章中进行介绍。
如果您运行 Linux,那么很容易从头开始使用不同的隔离技术。而容器运行时在不同抽象级别上,利用各种隔离功能,为容器提供了稳定、健壮的开发和生产平台。
5. 结论
本文介绍了 chroot 如何改变运行的根目录,并说明了这种简单的操作并不能达到隔离的需求;然后介绍了 Linux 命名空间,看到了通常使用 clone 选项或 unshare 解除共享,然后通过 setns 或 mount 等方式设置新的命名空间;最后我们通过简单的例子,展示了API的使用,以及多个进程使用相同命名空间的应用。
读完本文,是不是对容器的一些隔离技术有种豁然开朗的感觉?对容器、Pod等资源共享粒度一个大概的认识?
有很多主题没有在这里讨论,避免细节太多而带偏主题。当然,深入研究 Linux 命名空间主题的一个很好的资源是 Linux 程序员手册:NAMESPACES(7)
,比如使用 man 2 clone
、man 1 unshare
或者直接查看 https://man7.org/linux/man-pages/man7/cgroups.7.html。
下一篇博客文章将涵盖容器运行时、安全性以及围绕最新容器技术的整体生态系统。敬请关注!