项目源码地址:https://github.com/lixd/mydocker>欢迎Star支持在阅读本文前,建议先了解以下核心技术点:基础机制:深入理解Nam...">
当前位置:首页 > 技术 > 正文内容

使用 pivot_root 实现容器文件系统隔离

访客 技术 2026年7月24日 1
pivot_root 文件系统切换示意图
本篇是"从零实现简易容器"系列的第四章,重点介绍如何通过 Linux 系统调用 pivot_root 更换进程的根文件系统(rootfs),从而实现容器与宿主机之间的文件系统隔离。 > 项目源码地址:https://github.com/lixd/mydocker > 欢迎 Star 支持 在阅读本文前,建议先了解以下核心技术点:
  • 基础机制:深入理解 Namespace、Cgroups 与镜像层的核心作用
  • 命名空间隔离:Linux Namespace 如何构建独立视图
  • 资源控制
    • Cgroups 入门:限制 CPU 与内存使用
    • Cgroups 子系统详解:blkio、net_cls 等模块解析
    • Docker 资源管理背后的原理
  • 联合文件系统:OverlayFS 在容器镜像中的应用
  • 网络模型:veth、bridge 和 iptables 构建容器通信
实验环境信息如下:
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:        20.04
Codename:       focal
Kernel:         5.4.0-74-generic
> 注意:需以 root 权限运行操作

1. 引言

此前我们已利用 Namespace 实现了 PID、Mount、UTS 等隔离,并借助 Cgroups 完成了资源配额控制。但当前容器仍共享宿主机的文件系统结构——这意味着容器内执行 ls / 显示的是宿主目录内容,且所有挂载点均被继承。 这显然不符合标准容器的行为。为此,我们需要引入"根文件系统"的概念,即为每个容器提供一个独立的、自包含的基础文件层级。本文将实现这一功能,使容器运行在一个专用的 rootfs 上。

2. 构建容器根文件系统

真正的 Docker 镜像是分层的只读文件系统,但我们这里简化处理,直接提取一个轻量级镜像的内容作为初始 rootfs。 选择 busybox 是因为它集成了大量常用 Unix 工具,体积小且功能完整。 操作步骤如下:
  1. 拉取镜像:
    docker pull busybox
  2. 启动临时容器并导出其文件系统:
    docker run -d busybox top
    containerId=$(docker ps --filter "ancestor=busybox" | grep -v IMAGE | awk '{print $1}')
    docker export -o busybox.tar $containerId
  3. 解压 tar 包生成 rootfs 目录:
    mkdir -p /root/busybox
    tar -xvf busybox.tar -C /root/busybox
此时,/root/busybox 即为我们后续要挂载的根文件系统,其结构如下:
bin/   dev/   etc/   home/   proc/   root/   sys/   tmp/   usr/   var/
请注意:rootfs 不包含内核,仅包含用户空间所需的二进制程序、库和配置文件。

3. 使用 pivot_root 切换根目录

关键在于使用 pivot_root 系统调用替换当前进程的根文件系统。

3.1 系统调用说明

函数原型:
int pivot_root(const char *new_root, const char *put_old);
- new_root:新根目录路径。 - put_old:原根目录将被移至该位置。 调用后,进程的根目录变为 new_root,旧根自动挂载到 put_old。与 chroot 不同,pivot_root 可完全脱离旧根,允许卸载原文件系统。

3.2 实现细节

由于 systemd 默认将 mount namespace 设为 shared,必须显式设置为 private,防止事件传播导致错误。 以下是 Go 中的关键实现逻辑:
func setupRootfs() error {
    // 获取当前工作目录(即 /root/busybox)
    cwd, err := os.Getwd()
    if err != nil {
        return fmt.Errorf("获取当前路径失败: %v", err)
    }

    // 将当前 mount namespace 设为私有
    if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil {
        return fmt.Errorf("设置 MS_PRIVATE 失败: %v", err)
    }

    // 绑定挂载自身,确保 new_root 与原根不在同一文件系统
    if err := syscall.Mount(cwd, cwd, "", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
        return fmt.Errorf("bind mount 失败: %v", err)
    }

    // 创建用于存放旧根的临时目录
    oldRoot := filepath.Join(cwd, ".old_root")
    if err := os.Mkdir(oldRoot, 0700); err != nil && !os.IsExist(err) {
        return fmt.Errorf("创建 .old_root 目录失败: %v", err)
    }

    // 执行 pivot_root 切换
    if err := syscall.PivotRoot(cwd, oldRoot); err != nil {
        return fmt.Errorf("pivot_root 执行失败: %v", err)
    }

    // 切换到新的根目录
    if err := os.Chdir("/"); err != nil {
        return fmt.Errorf("切换工作目录失败: %v", err)
    }

    // 卸载旧根并清理
    if err := syscall.Unmount("/.old_root", syscall.MNT_DETACH); err != nil {
        return fmt.Errorf("卸载旧根失败: %v", err)
    }
    os.Remove("/.old_root")

    // 重新挂载必要的虚拟文件系统
    mountProc()
    mountDev()
    return nil
}

func mountProc() {
    flags := uintptr(syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV)
    syscall.Mount("proc", "/proc", "proc", flags, "")
}

func mountDev() {
    tmpfsFlags := syscall.MS_NOSUID | syscall.MS_STRICTATIME
    syscall.Mount("tmpfs", "/dev", "tmpfs", uintptr(tmpfsFlags), "mode=755")
}
同时,在父进程启动子进程时指定工作目录:
func NewParentProcess(tty bool) (*exec.Cmd, *os.File) {
    cmd := exec.Command("/proc/self/exe", "init")
    cmd.Dir = "/root/busybox"  // 设置 init 进程的工作目录
    // ...其余设置
    return cmd, writePipe
}

4. 验证效果

编译并运行测试命令:
go build .
./mydocker run -it /bin/ls
输出应为:
bin   dev   etc   home   proc   root   sys   tmp   usr   var
表明当前环境已是基于 /root/busybox 的独立文件系统,而非宿主机目录。

5. 总结

本文完成了容器文件系统隔离的核心步骤:
  • 从 busybox 镜像提取并构建 rootfs
  • 利用 pivot_root 切换根目录,实现真正意义上的文件系统隔离
  • 挂载 /proc/dev 等必需的虚拟文件系统
结合之前的命名空间隔离与资源限制,我们现已实现了一个具备基本功能的容器运行时。 > 完整代码位于 GitHub 的 feat-rootfs 分支 > 测试前请确保 /root/busybox 目录存在且包含正确文件系统内容

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。