大家应该都知道,一个嵌入式系统想要完成 bringup,除了 bootloader 和内核之外,还得有根文件系统。
很多人可能也接触过 busybox,网上也有大量教程教你制作根文件系统。操作往往都比较复杂,比如先创建一系列文件夹:
mkdir dev etc lib var proc tmp home root mnt sys
再到 rcS 脚本里塞一堆启动命令。跟着操作一步步来,通常都能做出来,但这个过程你真的理解了吗?
ramdisk、initrd、initramfs、rootfs、ramfs、tmpfs——这些词你可能都听过、都接触过,但你是否真的清楚它们各自的含义和功能?
不管是在群里讨论、面试现场,还是和同事交流,这几个概念经常被混为一谈。这背后既有历史遗留问题,也有旧资料误导的因素。
想要理清这些概念,得先看看一块嵌入式板子从上电到进入用户态,根文件系统究竟经历了哪几个阶段。搞清楚这个流程需要什么,你才能明白移植文件系统时每一步操作的意义,那些混淆的概念自然也就清楚了。
一图流如下,建议结合这张图来理解整篇文章:

1. 一个嵌入式可工作的根文件系统包括哪些?
在从零开始的BSP之路里完成了 uboot 和 kernel 的移植之后,一启动就会撞上这样的崩溃报错:
...
[ 0.760125] VFS: Cannot open root device "" or unknown-block(0,0): error -6
[ 0.772015] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
VFS: Cannot open root device,意思是内核走到了"挂载根文件系统"这一步,发现压根没有根文件系统可挂,于是直接 panic。
那为什么内核启动必须挂根文件系统?
因为内核跑起来之后,要做的最后一件事就是把控制权交给用户态的第一个进程。而要让这个进程正常工作,还得准备好它需要的运行环境——C 库、shell、配置文件等等。这一切的载体,就是文件系统。
所以要做出一个能用的根文件系统,得解决下面几个问题。
1.1 初始化脚本
start_kernel 完成所有初始化之后,最后会去 exec 一个 PID 1 入口程序,通常是 /sbin/init。
/sbin/init 启动后会读取配置文件 /etc/inittab,根据里面的 init.d/rcS 去运行 rcS 脚本,所以这个 rcS 脚本才是真正规定"开机做什么"的地方。
rcS 脚本内容主要包括:
- 挂载
proc、sysfs、tmpfs 这些虚拟文件系统
- 通过
mknod 或 mdev -s 扫描内核已注册的设备,在 /dev 下建出对应节点
modprobe 加载存储、网卡等驱动模块
与此同时,还得准备好 init 程序启动时所需要的 C 库。
所以做 rootfs 时总有一操作步骤是从交叉编译工具链里把 aarch64-none-linux-gnu/libc/lib/*.so* 一并拷到 lib 目录下。
做完这些,你就得到了一个最小的可供运行目录树。
网上制作根文件系统的教程中 mkdir dev etc lib var ...、写 /etc/init.d/rcS 这些操作,本质上都是在做这件事。
1.2 文件系统类型与存储介质
但你会发现一个问题:虽然得到了一个目录树,它其实还只是一些普通的文件夹罢了。
要想在 Linux 系统中让用户使用,还需要有一个文件系统来提供 open、read、write、mount 这些接口。这一部分在 VFS 章节已经做了详细讲解,这里不再展开。
按照存储介质,文件系统可以分为以下三类:
1.2.1 块设备文件系统
这是我们最常听说的文件系统类型。
现在嵌入式系统中最常用的是 ext4,它可读写、有日志、生态最完善,通常用在 eMMC、SD 卡这种容量比较大的存储上。
除此之外,路由器、机顶盒和一些智能设备常用的还有 squashfs——只读、压缩,特别适合装在 SPI NOR flash 这种小容量介质上。
块设备文件系统的共同点是:文件最终会序列化落盘永久保存。
所以它们既是文件系统驱动,也定义了一套磁盘格式(包括 superblock、inode 表、目录块、数据块的二进制布局等)。这也是它们都有"镜像文件"概念的原因:比如 mkfs.ext4 能产出一个 ext4 格式的二进制镜像,mksquashfs 能产出一个 squashfs 镜像,烧到对应介质上就能直接挂载。
1.2.2 内存型文件系统
与之对应的是 tmpfs / ramfs,它们运行时直接在内存里造一棵目录树就能用,掉电即丢失。
和块设备型相比,它们没有"格式",也不需要打包,挂载后就是一棵空的目录树,用户往里写什么就有什么。
ramfs 实现比较简单,tmpfs 是 ramfs 的升级版。我们日常 Linux 系统里很多地方用的都是 tmpfs,例如 /tmp、/run,以及做驱动最常打交道的设备节点 /dev,都是 tmpfs。
另外,嵌入式系统中很多时候不只使用一个文件系统,tmpfs 也可以作为内核启动时的初始文件系统进行初始化,然后再切换到上面提到的块设备文件系统。这个初始文件系统有一个专门的格式就是 initramfs,下面会展开讲,它的本质就是 tmpfs。
1.2.3 网络型文件系统
代表是 NFS——通过网络把远端服务器(比如开发用的 PC)上的某个目录挂到板子上当根。
开发板端不关心远端实际是什么文件系统,内核 NFS 客户端会把所有 VFS 调用透明地翻译成网络请求,远端是 ext4、xfs、btrfs 都行。
这种方式主要用在开发阶段,改个文件直接保存就能生效,不用反复烧镜像,迭代速度比较快。
1.3 内核怎么找到它
镜像做好、烧好之后,还得告诉内核"根文件系统在哪、是什么类型",否则它没法挂载。这就是开头那个 Cannot open root device panic 的来源。
这一步通常通过 bootloader 命令行传递给内核,也就是大家常说的 bootargs 来完成。上面几种不同介质的文件系统,也通过不同方法传入内核:
- 块设备根文件系统:在 bootargs 中设置参数
root=/dev/mmcblk0p2 rootfstype=ext4,直接告诉内核分区路径和文件系统类型
initramfs:不需要在 bootargs 里写 root=,而是让 bootloader 把 cpio.gz 加载到一段固定内存地址,内核启动早期会自动识别并解压
- NFS:通过
root=/dev/nfs nfsroot=... 配合 ip=dhcp 这类网络参数
对应我们平时可能涉及到的操作如下:
# 块设备根
setenv bootargs "root=/dev/mmcblk0p2 rootfstype=ext4 rw console=ttyS0,115200"
# initramfs(由 bootloader 把 cpio 加载到内存,内核自动识别)
bootm ${kernel_addr} ${ramdisk_addr} ${dtb_addr}
# NFS 根
setenv bootargs "root=/dev/nfs nfsroot=192.168.1.100:/srv/nfsroot ip=dhcp"
2. ramdisk / initrd / initramfs 概念区分与详解
梳理完根文件系统的流程和原理,现在就可以把开头提到的那几个容易混淆的概念逐一拆开了。
根据前面的分析会发现,ramdisk / initrd / initramfs / ramfs / tmpfs 本就属于不同层面,从功能和阶段上区分开之后就非常清楚了。
ramfs/tmpfs 前面已经讲过,它们是一种针对内存存储的文件系统格式,通过打包成 cpio 的格式,内核启动时直接解压并加载到内存中运行。
重点来看 ramdisk / initrd / initramfs。
2.1 ramdisk
ramdisk 这个概念比较特殊,它既不是文件系统的格式类型,也不属于上面说的文件系统具体介质。
它是一段内存,通过驱动将其模拟成块设备,用户态看到的是 /dev/ram0。它本身不直接存目录、存文件,要在它上面再 mkfs 一个真实文件系统(比如 ext2)才能挂载使用。本质上和 /dev/mmcblk0、/dev/sda 是同一类东西。
它在历史上的角色是给老式的 initrd 流程 当介质(下面讲),但现代 Linux 内核启动已经不走这条路了,所以你在板子上一般看不到 /dev/ram0。
不过这个名字沿用在了现在的 bootloader 命令和文件名里,例如 bootm <kernel> <ramdisk>。但这些叫 ramdisk 的东西装的内容根本不是 ramdisk,现在大部分情况下实际是 cpio.gz 归档(也就是 initramfs 格式)。
2.2 initrd / initramfs
这两个概念最容易混淆,它们都是"启动期临时根文件系统"的加载机制,但实现路径完全不同:initrd 是旧方案,initramfs 是现代方案。
那为什么要切换根文件系统?为什么不能直接使用块设备文件系统呢?这个问题解释起来情况比较复杂,简单来说一般有以下几种情况:
- 多平台通用性:不同平台对应的存储介质、驱动等都有区别,如果直接在 bootargs 中写死无法实现软件兼容
- 对应的块设备驱动不是 builtin 而是 ko 形式,需要内核启动到中后期才能加载
- 涉及到加解密
可能还有其他情况,但本质都是:在块设备文件系统 ready 之前,还有一些初始化工作要做,这期间需要有一个临时的根文件系统供系统运行。
initrd(initial ramdisk)是老方案,已经被 initramfs 完全淘汰了。 initrd 重点在 ramdisk,它的介质就是上面提到的 /dev/ram0。流程如下图:

它依赖 ramdisk 块设备 + ext2 驱动两个组件,流程复杂、镜像大小固定、切换易出错。在 Linux 2.6 之前用的就是这种方案,可以看出早期的实现完全是仿照真实文件系统的思路设计的。
既然本就是要运行在内存中,而且又是启动时的临时根文件系统,何必要搞得跟真实文件系统一样复杂呢?
于是就有了 initramfs。
initramfs(initial RAM filesystem)是现代方案,工作机制如下:

整个过程没有 mount 操作,也没有任何块设备驱动,因为 rootfs 在内核早期初始化就挂好了,只是把内容填充进去。
虽然 initrd 已经过时了,但 initrd= 这个词在 bootloader 命令、内核命令行参数、文件名里依然广泛存在。这就是一个历史问题,也是导致概念容易混淆的根本原因。
Linux 2.6 以前确实是 initrd(真正的 ramdisk + ext2),那时候 bootloader 设计者确实是按"加载一个块设备镜像"的语义命名的。然而现在的内核默认早就不支持这种流程了,所以你见到的无论是 ramdisk.img、rootfs.img、uInitrd、boot.img 等等,都是将 cpio 格式加上特殊的格式头然后压缩得到的,本质都是一样的。
3. 总结
这一节结合制作根文件系统的一些常规操作,讲了一个根文件系统加载流程的几个阶段,这也能很好地帮助你理解制作过程中每一步操作的意义。
同时也区分了嵌入式文件系统上比较容易混淆的几个概念:
ramdisk 是块设备——把一段内存模拟成 /dev/ram0,本身不是文件系统
ramfs / tmpfs 是内存型文件系统,挂载即用,/tmp、/run、/dev 都是它
initrd 是启动期加载机制(老方案:ramdisk + ext2 镜像),已经被淘汰
initramfs 是启动期加载机制(现代方案:cpio 归档 + ramfs/tmpfs),目前主流
想深入了解 Linux Kernel 启动流程和文件系统实现的读者,也可以到云栈社区和其他嵌入式开发者一起交流。