跳到主要内容

从零认识 rootfs:它到底是文件系统、分区,还是 Linux 里的角色?

· 阅读需 17 分钟

学习 Linux、嵌入式系统、Docker 或容器时,经常会遇到一个词:rootfs

它看起来像某种文件系统格式,但又不像 ext4xfsbtrfs 那样能被 mkfs.ext4 之类的命令创建。它有时又和分区、镜像、容器、initramfs、overlayfs 一起出现,让人很容易混淆:

rootfs 到底是什么?是文件系统?是分区格式?还是磁盘上的某个区域?

最直接的答案是:

rootfs 是 root filesystem 的缩写,指 Linux 系统中作为根目录 / 使用的那个文件系统。它是一个文件系统/挂载命名空间层面的角色,不是磁盘角色,也不是分区格式。

这句话看起来简单,但要真正理解它,需要先从 Linux 的目录树讲起。

Linux 为什么需要 rootfs?

在 Linux/Unix 系统里,所有路径都挂在同一棵目录树下面。这棵树的根是 /

比如:

/
├── bin
├── boot
├── dev
├── etc
├── home
├── proc
├── sys
├── usr
└── var

无论底层有多少块磁盘、多少个分区、多少种文件系统,用户和进程看到的都是这棵统一的路径树。

这和 Windows 里常见的 C:\D:\ 多盘符模型不同。Linux 的模型是:

先有一棵以 / 为根的目录树
再把不同文件系统挂载到这棵树的不同位置

例如:

/ 来自 /dev/nvme0n1p2
/boot 来自 /dev/nvme0n1p1
/home 来自 /dev/nvme0n1p3
/proc 来自 proc 虚拟文件系统
/sys 来自 sysfs 虚拟文件系统
/run 来自 tmpfs

其中最关键的是 /。因为其他挂载点都要挂到这棵树的某个位置,而这棵树本身必须先存在。

所以 Linux 启动时必须先解决一个根本问题:

系统启动后,哪个文件系统来承担 / 这个根?

承担这个角色的文件系统,就叫 rootfs

rootfs 是一种角色,不是一种格式

理解 rootfs 最容易出错的地方,是把它和磁盘、分区、文件系统格式混在一起。

可以按层次拆开:

磁盘设备: /dev/nvme0n1、/dev/sda、/dev/mmcblk0
分区: /dev/nvme0n1p2、/dev/sda1
文件系统格式: ext4、xfs、btrfs、squashfs、erofs
挂载点: /、/boot、/home、/var
系统角色: rootfs,也就是挂载到 / 的那个文件系统

举一个普通 Linux 主机的例子:

/dev/nvme0n1p2 是一个分区
这个分区里创建了 ext4 文件系统
这个 ext4 文件系统被挂载到 /
于是它在这个系统里承担 rootfs 角色

所以:

  • rootfs 不是磁盘。
  • rootfs 不是分区。
  • rootfs 不是 GPT、MBR 这样的分区表格式。
  • rootfs 也不是 ext4xfsbtrfs 这样的文件系统格式。
  • rootfs 是“哪个文件系统正在作为 / 使用”的角色名。

同样是 rootfs,底层可以完全不同:

ext4 挂载到 / -> ext4 文件系统承担 rootfs 角色
btrfs 挂载到 / -> btrfs 文件系统承担 rootfs 角色
squashfs 挂载到 / -> squashfs 镜像承担 rootfs 角色
tmpfs 挂载到 / -> 内存文件系统承担 rootfs 角色
NFS 挂载到 / -> 网络文件系统承担 rootfs 角色
overlayfs 挂载到 / -> overlayfs 叠加结果承担 rootfs 角色

这就是为什么 rootfs 这个词在嵌入式、Live CD、容器、Docker、initramfs 里都能出现。它不是描述底层材料,而是描述这个文件系统在当前系统中的职责。

为什么要单独给它一个名字?

因为 / 不是普通挂载点。

普通挂载点可以晚一点出现。比如 /home 不挂载,系统可能还能进入维护模式;/mnt/data 不挂载,最多是某些数据不可用。

/ 不存在,系统就没有路径命名空间的起点。内核后续要启动第一个用户态进程,要找 /sbin/init/init 或 systemd;用户态工具要访问 /etc/dev/usr;这些都依赖根目录树已经存在。

因此 rootfs 是系统启动链路中的核心角色:

内核启动
-> 准备早期根文件系统
-> 找到或构造真正的 rootfs
-> 切换到这个 rootfs
-> 启动 init / systemd
-> 系统进入正常运行

给它一个单独名字,可以跨越很多不同场景讨论同一件事:

  • 嵌入式里说“烧录 rootfs”,通常指烧录根文件系统镜像或目录树。
  • Docker 里说“容器 rootfs”,指容器进程看到的 /
  • initramfs 里说“早期 rootfs”,指内核启动初期解包出来的临时根。
  • LXC 里说“rootfs tarball”,指能作为容器根目录的完整目录树。

如果不用 rootfs 这个词,每次都要说“那个会被挂载为 /、让系统或容器进程作为根目录看到的文件系统”,太长,也不利于讨论启动流程和容器隔离。

rootfs 里面通常有什么?

一个能作为 Linux 根文件系统使用的目录树,通常至少要包含用户态运行所需的基本目录和文件。

常见结构类似:

/
├── bin
├── dev
├── etc
├── lib
├── lib64
├── proc
├── root
├── run
├── sbin
├── sys
├── tmp
├── usr
└── var

不同发行版、不同用途会有差异。桌面/服务器发行版的 rootfs 通常很大,包含包管理器、系统服务、shell、库文件、配置文件等。嵌入式系统的 rootfs 可能非常小,只包含 BusyBox 和少量配置。容器镜像的 rootfs 则可能只包含运行某个应用所需的最小依赖。

从这个角度看,rootfs 也经常被用来指“一个可作为根目录使用的目录树内容”。

例如:

rootfs.tar.gz 一个打包好的根文件系统目录树
rootfs.ext4 一个 ext4 格式的根文件系统镜像
rootfs.squashfs 一个 squashfs 格式的只读根文件系统镜像

这里的 rootfs 不是说它们的格式都叫 rootfs,而是说它们的用途是“作为根文件系统”。

Linux 启动时 rootfs 怎么来?

一个简化的 Linux 启动过程可以这样理解:

1. Bootloader 加载内核和启动参数
2. 内核初始化 CPU、内存、驱动等基础设施
3. 内核准备早期根文件系统
4. 内核根据 root= 参数或 initramfs 逻辑找到真正的根
5. 挂载真正的 rootfs 到 /
6. 启动 /sbin/init、/init 或 systemd

启动参数里常见的 root= 就是在告诉内核真正的根文件系统来源。

比如:

root=UUID=xxxx-xxxx
root=/dev/nvme0n1p2
root=/dev/mmcblk0p2
root=/dev/nfs

这不是指定“rootfs 格式”,而是指定“哪个设备或来源要被挂载成 /”。

很多现代发行版还会使用 initramfs。它是一个早期用户态环境,通常被内核解包到内存中的早期根文件系统里。它的任务包括加载磁盘驱动、解密磁盘、组装 RAID/LVM、寻找真正的根文件系统,然后切换过去。

所以启动早期可能存在两个阶段:

早期 rootfs:initramfs 提供的临时根
真正 rootfs:磁盘、网络或其他来源上的最终根

这也是为什么在启动相关文档里,rootfs 这个词有时会显得更加“内核化”:Linux 内核本身确实有早期 rootfs 的概念。

临时 rootfs 如何切换到真正 rootfs?

从启动过程看,临时 rootfs 切换到真正 rootfs 的过程通常发生在 initramfs 阶段。

这个过程常被描述为:

从 initramfs 切换到 real rootfs

在用户态工具里,常见命令名是 switch_root。底层可能涉及 mount、移动挂载点、chrootpivot_root 等机制,但现代 initramfs 里经常由 switch_root 或发行版自己的 initramfs 脚本封装。

一个更完整的启动链路可以这样看:

Bootloader
-> 加载 Linux kernel
-> 加载 initramfs
-> kernel 启动
-> kernel 解包 initramfs 到早期 rootfs
-> 执行 initramfs 里的 /init
-> /init 找到真正 rootfs
-> 挂载真正 rootfs
-> 切换 / 到真正 rootfs
-> 执行真正 rootfs 里的 /sbin/init 或 systemd

关键点是:切换 rootfs 的工作通常不是内核全自动完成的,而是 initramfs 里的早期用户态脚本完成的。

启动早期,当前 / 还是 initramfs 提供的临时 rootfs:

当前 /
= initramfs 提供的临时 rootfs

initramfs 里的 /init 通常会做这些事:

# 1. 挂载早期需要的虚拟文件系统
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev

# 2. 加载必要驱动、等待设备出现
# 例如磁盘、USB、NVMe、RAID、LVM、加密卷、网络等

# 3. 找到真正的 root 设备
# 可能来自 /proc/cmdline 里的 root=UUID=...
# 也可能来自脚本、网络、标签、LVM 名称等

# 4. 把真正 rootfs 挂到临时目录
mount /dev/nvme0n1p2 /newroot

# 5. 切换到真正 rootfs
exec switch_root /newroot /sbin/init

切换前,文件系统视图大概是:

/ initramfs 临时 rootfs
├── init 早期启动脚本
├── bin
├── dev
├── proc
├── sys
└── newroot 真正 rootfs 临时挂载点
├── sbin/init
├── etc
├── usr
└── var

执行:

exec switch_root /newroot /sbin/init

之后,文件系统视图变成:

/ 真正 rootfs
├── sbin/init 或 systemd
├── etc
├── usr
├── var
├── dev
├── proc
└── sys

原来的 initramfs 临时 rootfs 会被释放,或者至少从正常系统视图里消失。随后系统继续从真正 rootfs 里的 initsystemd 启动。

为什么不让内核直接挂载真正 rootfs?因为真正 rootfs 很多时候不是一开始就能直接访问的。

它可能位于:

  • 加密磁盘里,需要先解锁;
  • LVM 或 RAID 上,需要先组装卷;
  • NVMe、USB、SCSI 设备上,需要先加载驱动;
  • 网络 NFS 上,需要先配置网络;
  • btrfs 子卷、只读 squashfs、overlay 组合上,需要先执行额外挂载逻辑。

所以 initramfs 的角色是:

提供一个最小临时 Linux 环境,用来找到、准备、挂载真正的 rootfs,然后把系统控制权交给它。

这里还有一个容易误解的地方:临时 rootfs 并不是被“转换成”正式 rootfs。真实发生的是:

1. 临时 rootfs 先作为 /
2. 真正 rootfs 被挂载到 /newroot
3. 启动脚本把进程视角里的 / 切换到 /newroot
4. 新的 /sbin/init 接管系统启动
5. 临时 rootfs 退出历史舞台

也就是说,这是一次“根目录视角切换”,不是文件系统格式转换。

在 Linux 上怎么查看当前 rootfs?

最推荐的命令是:

findmnt /

它会显示当前 / 的来源、文件系统类型和挂载选项。

也可以只看关键字段:

findmnt -no SOURCE,FSTYPE,OPTIONS /

可能输出类似:

/dev/nvme0n1p2 ext4 rw,relatime

这说明当前作为 / 的文件系统来自 /dev/nvme0n1p2,格式是 ext4

在 Docker 容器里可能看到:

overlay overlay rw,relatime,lowerdir=...,upperdir=...,workdir=...

这说明容器里的 / 来自一个 overlayfs 叠加结果。

还可以看内核提供的挂载信息:

cat /proc/mounts | awk '$2 == "/" {print}'

或者:

awk '$5 == "/" {print}' /proc/self/mountinfo

查看启动参数:

cat /proc/cmdline

如果里面有 root=UUID=...root=/dev/...,那就是内核启动时被告知的根文件系统来源。

需要注意的是,正常系统启动完成后,findmnt /mount 里不一定会直接出现 rootfs 这个关键词。你更常看到的是具体来源和具体格式,比如:

/dev/nvme0n1p2 on / type ext4

或者:

overlay on / type overlay

能不能看到 rootfs 这个词,取决于当前环境和启动阶段。早期启动环境、initramfs、某些容器或特殊系统里可能能看到:

rootfs / rootfs rw 0 0

但看不到 rootfs 这个词,并不代表没有 rootfs。只要系统里存在 /,就一定有某个文件系统承担了 rootfs 角色。

rootfs 和 /boot、/home 有什么区别?

/boot/home/var 这些目录可以是 rootfs 内部的普通目录,也可以是额外挂载进来的文件系统。

比如一个简单系统可能只有一个分区:

/dev/nvme0n1p2 -> ext4 -> 挂载到 /

这时 /boot/home/var 都只是 rootfs 里的普通目录。

另一个系统可能拆成多个分区:

/dev/nvme0n1p2 -> ext4 -> /
/dev/nvme0n1p1 -> vfat -> /boot/efi
/dev/nvme0n1p3 -> xfs -> /home

这时 /home 是另一个文件系统挂载过来的。但从路径树上看,它仍然位于 / 下面。

所以 rootfs 的特殊性在于:它是整棵树的根。其他文件系统都要挂到这棵树的某个节点上。

Docker 容器里的 rootfs 是什么?

Docker 容器也有自己的 /

这并不是说 Docker 启动了一个新内核。普通 Docker 容器和宿主机共享同一个 Linux 内核,但它通过 namespace 让容器进程看到一套隔离后的文件系统视图。

Docker 镜像本质上可以理解为一组文件系统层:

只读镜像层 1
只读镜像层 2
只读镜像层 3
容器可写层

启动容器时,Docker 通常通过 overlay2 把这些层叠加成一个最终目录树。容器进程看到的 /,就是这个叠加结果。

简化表示:

image readonly layers
+
container writable layer
=
container rootfs

所以当你运行:

docker run -it ubuntu:24.04 /bin/bash

容器里的 / 来自 ubuntu:24.04 镜像层加上这个容器自己的可写层。

而:

docker run -it alpine:3.20 /bin/sh

容器里的 / 来自 Alpine 镜像层加上可写层。

这两个容器看到的 rootfs 内容不同,因为镜像不同。

Docker 启动时能不能更改容器 rootfs?

可以,但要分层理解。

普通 Docker 使用方式里,你通常不是在 docker run 时直接指定一个任意目录作为 /,而是通过镜像决定 rootfs 的来源:

docker run ubuntu:24.04
docker run alpine:3.20
docker run debian:bookworm

如果你想让容器使用自己的 rootfs,常见做法是构建或导入一个镜像。

例如,把一个已有 rootfs 目录打包成镜像:

tar -C /path/to/rootfs -cf rootfs.tar .
docker import rootfs.tar my-rootfs:latest
docker run -it my-rootfs:latest /bin/sh

这时容器内的 / 就来自你导入的那个目录树。

也可以用 Dockerfile 构建:

FROM debian:bookworm
RUN apt-get update && apt-get install -y curl
COPY ./app /app
CMD ["/app/start.sh"]

构建出的镜像会形成新的 rootfs 内容。

但普通 docker run 没有一个类似这样的参数:

docker run --rootfs /some/host/path

Docker 把底层 rootfs 管理封装在镜像、存储驱动和 OCI runtime 后面。

更底层的 OCI runtime,比如 runc,确实有 rootfs 路径的概念。Docker 最终会把容器配置转换成 OCI 规范,再交给底层 runtime 执行。底层 runtime 会设置 mount namespace,并让容器进程进入指定的 rootfs。

可以这样理解:

Docker CLI
-> 通过 image 选择 rootfs 内容
-> containerd 准备容器运行配置
-> OCI spec 描述 rootfs path
-> runc 设置 namespace、mount、pivot_root/chroot
-> 容器进程看到自己的 /

所以答案是:

Docker 容器启动时当然会设置容器内的 rootfs,但普通用户通常通过“换镜像、构建镜像、导入 rootfs tarball”来改变它,而不是直接在 docker run 中指定一个宿主机目录作为 /

bind mount 会改变 rootfs 吗?

不会。bind mount 或 volume 只是把某个目录挂进容器内的某个路径。

例如:

docker run -v /host/data:/data ubuntu

这会让容器内 /data 对应宿主机 /host/data,但容器的 / 仍然来自镜像和可写层组成的 rootfs。

你也可以挂载到更关键的位置:

docker run -v /host/etc:/etc ubuntu

这会覆盖容器内 /etc,但它仍然不是替换整个 rootfs,只是覆盖 rootfs 里面的一个子路径。这样做很容易破坏容器内部依赖,除非你非常清楚后果。

所以:

换镜像 / 导入 rootfs -> 改变容器 rootfs 的基础内容
bind mount / volume -> 在 rootfs 上额外挂载或覆盖局部路径

rootfs、chroot、pivot_root 的关系

rootfs 是“作为 / 的文件系统或目录树”。

chrootpivot_root 是让进程改变它看到的根目录的机制。

chroot 会把某个目录变成进程视角下的 /。例如:

sudo chroot /path/to/rootfs /bin/sh

这个 shell 会把 /path/to/rootfs 当作自己的 /

chroot 不是完整容器隔离。它主要改变路径解析根目录,不自动隔离进程、网络、用户、mount namespace 等。

容器 runtime 通常会结合 mount namespace、pivot_root、bind mount、cgroup、user namespace 等机制,构造出更完整的隔离环境。

可以粗略理解:

rootfs 目标目录树或文件系统
chroot 改变进程看到的 /
pivot_root 在 mount namespace 中切换根挂载
namespace 隔离进程看到的系统视图
cgroup 限制和统计资源

常见误解

误解一:rootfs 是一种文件系统格式

不是。

ext4xfsbtrfssquashfs 是文件系统格式。rootfs 是作为 / 的角色。

一个 ext4 文件系统可以是 rootfs,也可以挂到 /home,也可以挂到 /data。它是不是 rootfs,取决于它挂载在哪里、在当前系统里承担什么职责。

误解二:rootfs 就是根分区

不完全对。

在很多传统 Linux 安装中,rootfs 确实来自某个“根分区”,比如 /dev/sda2。但 rootfs 不一定来自分区。

它也可以来自:

  • 内存中的 initramfs
  • 网络上的 NFS
  • 只读 squashfs 镜像
  • overlayfs 叠加结果
  • 容器运行时准备的目录树
  • LVM、RAID、加密块设备之上的文件系统

所以“根分区”只是 rootfs 的一种常见承载方式,不等于 rootfs 本身。

误解三:容器有自己的 rootfs,所以容器有自己的内核

不是。

容器有自己的文件系统视图,也就是自己的 /,但普通容器和宿主机共享内核。

容器隔离主要依赖 Linux 内核提供的 namespace、cgroup、capability、seccomp、mount 等机制。rootfs 只是其中的文件系统视图部分。

误解四:看不到 rootfs 关键词,就说明系统没有 rootfs

不是。

启动完成后的系统往往显示的是具体来源和具体类型:

/dev/nvme0n1p2 on / type ext4

这行的意思就是:/dev/nvme0n1p2 上的 ext4 文件系统正在承担 rootfs 角色。

rootfs 是角色名,不一定会原样出现在命令输出里。

一张总览图

可以把这些概念放到同一张图里:

物理磁盘 / 虚拟磁盘 / 镜像文件 / 网络存储 / 内存
|
v
分区 / 块设备 / 目录树 / 镜像层
|
v
文件系统格式或叠加机制
ext4 / xfs / btrfs / squashfs / tmpfs / overlayfs / NFS
|
v
挂载到某个路径
/、/boot、/home、/data
|
v
如果挂载到 /,它在当前命名空间里就是 rootfs

再看 Docker:

Docker image layers
|
v
overlay2 合成 merged 目录
|
v
OCI runtime 把它设置为容器进程的 /
|
v
容器内看到的 rootfs

最后总结

rootfs 最核心的含义是:

在某个 Linux 系统或容器的文件系统命名空间里,作为根目录 / 使用的那个文件系统或目录树。

它不是磁盘,不是分区,也不是文件系统格式。

它更像一个系统角色:

  • 从磁盘层看,它可能来自某个分区。
  • 从文件系统格式看,它可能是 ext4、btrfs、squashfs、overlayfs 等。
  • 从挂载层看,它挂载在 /
  • 从系统启动和运行视角看,它承担 rootfs 角色。

理解 rootfs 的关键,是把“底层存储是什么”和“在 Linux 命名空间里扮演什么角色”分开。

一句话记忆:

rootfs 不是“某种格式”,而是“当前这个 Linux 环境里的 / 从哪里来”。