Docker 的基石之一:手动体验 chroot 文件系统隔离

聊到 Docker 容器,大家最常听到的词就是“隔离”。容器能把应用和它的依赖打包在一起,并在隔离的环境中运行。但这隔离到底是怎么实现的呢?虽然现代 Docker 运用了 Linux 内核的多种高级特性(比如 Namespaces 和 Cgroups),但理解一个更早、更简单的技术——chroot(Change Root,改变根目录),能帮我们极好地理解文件系统隔离这个核心概念。

下面,我们就跟着之前的命令行操作,一步步手动构建一个 chroot 环境,亲身体验一下文件系统“障眼法”的魅力。

什么是 chroot

chroot 是 Unix/Linux 系统上的一个系统调用和命令。它的作用是改变一个正在运行的进程及其子进程眼中的“根目录”(/。当一个进程被 chroot 到某个目录(比如 /my/new/root)后,对于这个进程来说,所有以 / 开头的路径,实际上访问的都是宿主机上的 /my/new/root 目录。它相当于为进程的文件系统访问创建了一个“监狱”(Jail)。

重要提醒: chroot 仅仅隔离了文件系统的视图。它并不能隔离进程 ID(PID)、网络接口、用户 ID,也不能限制资源使用。它本身不是一个强大的安全边界,尤其是面对有特殊权限的用户时。

手动搭建一个最小化的 chroot 环境:实战步骤

我们的目标是创建一个独立的目录,里面只包含运行一个基本 Shell (bash) 和一个简单命令 (ls) 所需的最少组件。

1. 创建“监狱”目录:
这个目录将成为我们 chroot 进程的新根目录 /

# 这是我们的新根目录
mkdir oldboyedu-linux92

2. 拷贝基础程序(二进制文件):
我们需要一个 Shell 来交互,还需要一个命令来测试。

# 在“监狱”里创建标准的 'bin' 目录
mkdir oldboyedu-linux92/bin

# 拷贝 bash 这个 Shell 程序
cp /bin/bash oldboyedu-linux92/bin/

# 拷贝 ls 这个列目录程序
# 先找到 ls 在哪里: which ls (输出可能是 /usr/bin/ls 或 /bin/ls)
cp /usr/bin/ls oldboyedu-linux92/bin/ # 根据实际 `which ls` 的结果调整路径

3. 最关键的一步:拷贝依赖环境(共享库):
Linux 下大部分程序都不是完全独立的,它们需要依赖外部的共享库文件(.so 文件 - Shared Objects)才能运行。这些库包含了许多程序都会用到的通用代码。当我们 chroot 之后,进程只能看到新根目录里的文件,所以我们必须把所有需要的库文件也拷贝到“监狱”里。

ldd 命令是查找依赖的好帮手,它可以列出一个程序运行时需要哪些动态库。

# 查看 bash 需要哪些库
ldd /bin/bash
# 输出可能类似(不同系统和版本会不同):
#   linux-vdso.so.1 (...)
#   libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (...)
#   libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (...)
#   libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (...)
#   /lib64/ld-linux-x86-64.so.2 (...)  <--- 这个是动态链接器/加载器本身!非常重要!

# 查看 ls 需要哪些库
ldd /usr/bin/ls # (或 /bin/ls)
# 输出可能类似:
#   linux-vdso.so.1 (...)
#   libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (...)
#   libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (...)
#   libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (...)
#   libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (...)
#   /lib64/ld-linux-x86-64.so.2 (...)
#   libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (...)

# 在“监狱”里创建对应的库目录
mkdir oldboyedu-linux92/lib
mkdir oldboyedu-linux92/lib64 # 通常用来放 64 位库和动态加载器

# 根据 ldd 的输出,把 bash 和 ls 需要的 *所有* 库都拷贝进去
# 注意:要使用你自己系统上 ldd 输出显示的真实路径!
# bash 需要的库:
cp /lib/x86_64-linux-gnu/libtinfo.so.6 oldboyedu-linux92/lib/
cp /lib/x86_64-linux-gnu/libdl.so.2 oldboyedu-linux92/lib/
cp /lib/x86_64-linux-gnu/libc.so.6 oldboyedu-linux92/lib/
cp /lib64/ld-linux-x86-64.so.2 oldboyedu-linux92/lib64/

# ls 需要的库 (只需拷贝 bash 没覆盖到的):
cp /lib/x86_64-linux-gnu/libselinux.so.1 oldboyedu-linux92/lib/
cp /lib/x86_64-linux-gnu/libpcre2-8.so.0 oldboyedu-linux92/lib/
cp /lib/x86_64-linux-gnu/libpthread.so.0 oldboyedu-linux92/lib/
# 注意:像 libc.so.6, libdl.so.2, ld-linux-x86-64.so.2 这些库 bash 也需要,之前应该已经拷贝过了。

# 验证一下目录结构
ll -R oldboyedu-linux92/

为什么依赖库这么重要? 如果没有这些 .so 文件,bashls 启动时会找不到需要的功能代码,直接报错退出,常见的错误是 "file not found" 或者 "error while loading shared libraries"。

4. 切换根目录,进入“监狱”:
现在,使用 chroot 命令。我们告诉它:用 oldboyedu-linux92/ 作为新的根目录,并且在新根目录下运行 /bin/bash 这个程序。

# 执行 chroot 命令。通常需要 root 权限
sudo chroot oldboyedu-linux92/ /bin/bash
# 或者如果你已经是 root 用户:
# chroot oldboyedu-linux92/ /bin/bash

# 成功后,你的命令提示符会变化,可能变成类似这样:
# bash-5.0#

在“监狱”内部:

  • 试试运行 ls /。你会看到 bin, lib, lib64 这些目录,也就是宿主机上 oldboyedu-linux92 目录的内容,而不是宿主机真正的根目录 / 下的内容!
  • 试试运行 ls -l 或者 for i in "haha" "xixi"; do echo $i ; done。这些命令现在都能正常工作了,因为 lsbash 以及它们依赖的库都在这个“监狱”里。
  • 试试运行一个你没有拷贝进去的命令,比如 pwd 或者 cat (除非你的 lsbash 依赖它们,否则大概率没拷贝)。你会得到 "command not found" 的错误,因为它在这个受限的文件系统里找不到对应的程序。

5. 模拟多个“容器”:
chroot 本身只改变了文件系统视图。如果我们复制这个“监狱”目录,并再次 chroot 到新的目录,会发生什么?

# (首先需要退出之前的 chroot 环境,输入 exit)
exit

# 复制整个"监狱"目录
cp -r oldboyedu-linux92 oldboyedu-linux93

# 在第一个终端,进入第一个"监狱"
sudo chroot oldboyedu-linux92/ /bin/bash
# bash-5.0# echo "haha" > /haha.txt
# bash-5.0# ls /
# bin  haha.txt  lib  lib64

# 打开第二个终端,进入第二个"监狱"
sudo chroot oldboyedu-linux93/ /bin/bash
# bash-5.0# echo "xixi" > /xixi.log
# bash-5.0# ls /
# bin  lib  lib64  xixi.log

观察结果:

  • 在第一个 chroot 环境里,你创建了 haha.txt,它位于这个环境的 / 下。
  • 在第二个 chroot 环境里,你创建了 xixi.log,它位于这个环境的 / 下。
  • 这两个环境的文件系统是相互隔离的。第一个环境看不到 xixi.log,第二个环境看不到 haha.txt

6. 数据到底在哪里?
现在,在宿主机上(不要在 chroot 环境里)查看:

# 退出所有 chroot 环境
# exit
# exit

# 查看宿主机的真实根目录,这里没有 haha.txt 或 xixi.log
ls /

# 查看我们创建的两个"监狱"目录
ls /root/oldboyedu-linux92/  # 假设你的"监狱"在 /root 下创建的
# 输出: bin  haha.txt  lib  lib64

ls /root/oldboyedu-linux93/
# 输出: bin  lib  lib64  xixi.log

真相大白:
chroot 进程创建的文件,实际上是存储在宿主机上你指定的那个“监狱”目录里的!chroot 只是改变了进程看待文件系统的“视角”,并没有改变文件实际存储的位置。

chroot 与 Docker 的关系

  • 概念基石: chroot 完美地演示了文件系统隔离的基本思想,这是容器技术的核心特征之一。Docker 容器镜像的层叠文件系统(OverlayFS 等)可以看作是 chroot 概念的极其复杂和高效的实现。当你 docker run 一个容器时,Docker 会准备一个类似 chroot 环境的根文件系统,但使用的是更先进的文件系统技术。
  • 不只是 chroot Docker 提供的隔离远不止文件系统。它还使用了 Linux Namespaces 来隔离:
    • PID Namespace: 每个容器有自己独立的进程树,容器内的 1 号进程是容器的入口进程,看不到宿主机的其他进程。
    • Network Namespace: 每个容器有自己独立的网络栈(IP 地址、路由表、端口等)。
    • Mount Namespace: 每个容器有自己独立的文件系统挂载点视图(这是对 chroot 的极大增强)。
    • UTS Namespace: 独立的主机名和域名。
    • IPC Namespace: 独立的进程间通信资源。
    • User Namespace: 独立的 User ID 和 Group ID 映射(可以将容器内的 root 用户映射为宿主机上的非特权用户,提升安全性)。
  • 资源限制: Docker 还使用 Control Groups (Cgroups) 来限制容器可以使用的资源(CPU、内存、磁盘 I/O 等),这是 chroot 完全不具备的能力。

结论:

chroot 是一个理解容器文件系统隔离原理的绝佳入门工具。它让我们直观地看到,通过改变进程的根目录,可以创建一个看似独立的文件环境。然而,它提供的隔离非常有限。Docker 则在此基础上,结合了 Namespaces 和 Cgroups 等现代 Linux 内核特性,提供了更为全面和安全的隔离机制,构成了我们今天使用的强大容器技术。

希望这个结合实例的讲解能帮助你更深入地理解 Docker 背后的技术原理!

posted on 2025-04-05 08:19  Leo-Yide  阅读(310)  评论(0)    收藏  举报