...[ 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 下建出对应节点
与此同时,我们还需要准备 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 是现代方案。
所以initrd / initramfs是一种 Linux 内核启动切换根文件系统的机制,至于为什么要切换根文件系统,为什么不能直接使用块设备文件系统呢?
这个问题解释起来情况可能比较复杂,我们这里简单说一下,一般可能会有以下几种情况:
多平台通用性,因为不同平台对应的存储介质、驱动等都有区别,如果直接在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),目前主流;
我们的专栏持续更新中,欢迎关注~
原创不易,如果觉得对你有帮助请点赞、推荐和关注吧,这对我非常重要,感谢!!!
- END -
如果有什么问题,欢迎添加我的微信讨论。我建了一个小群,后面会陆续加一些我身边认识的行业大佬,都是来自一线大厂P7以上的工程师,任何有关行业、工作、跳槽的问题都可以在群里讨论~~
自我介绍:
曾就职于AMD,现就职于某大厂芯片部门资深BSP工程师。
崇尚实用主义,主张从用中学。
系列介绍:
带大家从零进行 Bringup 工作,并深入讲解这个过程中涉及到的内容以及原理实现。
往期推荐:
</ramdisk></kernel>
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!