『7x24小时有问必答』
坚持高质量原创,拒绝内容堆砌,喜欢的话点击上方星标,更新第一时间收到提醒,谢谢关注!

大家应该都知道一个嵌入式系统启动除了需要  bootloader、内核以外还需要根文件系统才能完成 bringup。
大家可能也接触过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  是现代方案。
所以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工程师。
崇尚实用主义,主张从用中学。

系列介绍:
从零开始的BSP之路,BSP系列的开章,站在芯片原厂工程师视角,
带大家从零进行 Bringup 工作,并深入讲解这个过程中涉及到的内容以及原理实现。

BSP工程师的内核基本功,旨在为驱动及BSP工程师讲解
工作中会用到的内核知识及其底层原理。

往期推荐:

</ramdisk></kernel>

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

上一主题上一主题         下一主题下一主题
QQ手机版小黑屋粤ICP备17165530号

关于我们·投诉举报· 用户帮助· 联系我们 · 本站服务 · 版权声明· 隐私政策 · 投搞指南

法律保护:PLC技术网,plcjs.com,plcjs.net等字样
Copyright 2010-2030. All rights reserved. 


微信公众号二维码 抖音二维码 百家号二维码 今日头条二维码哔哩哔哩二维码