从零认识 rootfs:它到底是文件系统、分区,还是 Linux 里的角色?
学习 Linux、嵌入式系统、Docker 或容器时,经常会遇到一个词:rootfs。
它看起来像某种文件系统格式,但又不像 ext4、xfs、btrfs 那样能被 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也不是ext4、xfs、btrfs这样的文件系统格式。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、移动挂载点、chroot、pivot_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 里的 init 或 systemd 启动。
为什么不让内核直接挂载真正 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 是“作为 / 的文件系统或目录树”。
chroot 和 pivot_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 是一种文件系统格式
不是。
ext4、xfs、btrfs、squashfs 是文件系统格式。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 环境里的/从哪里来”。
