探秘Linux的块设备和根

我们的学习习惯基本都是由浅入深的, 比如我们先学习如何使用fdisk工具来给磁盘分区, 之后才想到去看看fdisk到底对磁盘做了什么, 许久以后看到除了fdisk还有别的分区工具可以给磁盘分区.通常我们只需要知道怎么用就可以了, 也有很多原因促使我们去思考它的背后到底发生了什么,这些原因可能是你碰到了具体问题, 不得不让你往下去看, 去看你认为肯定不会出问题的那一部分, 也有可能是你觉得现在的自己技术太浮于表面, 想深入一些, 也可能是受周围人的影响, 一起学习.

佛家讲究因果, 使用者往往接触的是果, 因为使用者在乎的是可用性, 开发者或者说设计者则要考虑因, 因为什么样的因就会导致什么样的果, 因为这样的设计, 所以就有那样的bug, 就像黑客帝国中的Neo 要找到architecture!

本文由来于心中的两个疑问,即平凡的存储器件是怎么从分区变成一个个块设备的, 根是怎么被mount的.

 

通常,在processor上电后, 最早执行的代码是固化在CPU ROM中的程序, 它其实就是最早被执行的bootloader, 它的终极目的是从存储介质(包含uart, USB)加载另外一个bootloader, 比如u-boot. 拿启动介质是eMMC或者SD的例子来说, romcode一般会从offset = sector_size * 1的位置开始读取程序, 这么做的原因其实是为了跳开分区表. 

 


当然笔者也见过一个奇葩的例子, 海思的一款ARM9的芯片, ROMCODE直接从eMMC data partition offset 0的位置开始读取程序, 这就导致当你从emmc 上的u-boot启动, 兴冲冲的进到ramdisk对eMMC进行分区, 格式化, 烧写系统之后, 发现系统再也起不来了, 那是因为分区表已经覆盖了第一个分区,破坏了bootloader.  整个系统没有使用分区工具来划分分区, 而是通过u-boot的boot arguments描述分区信息. 

 

 

Linux 下Storage device 通常是作为块设备被访问的,例如mtdblock, mmcblock 和宇宙第一强的nandblk 块设备(实在太崇拜该设备了,将NAND发挥到了极致).  Storage device的设备驱动作为底层支撑, 负责注册块设备并且直接和存储器件打交道, 接受,执行和响应块设备层的过来的访问请求.  比如说,  一次文件读取的操作会变成文件系统提交到块设备的块读取请求, 该块设备的读访问请求, 在块设备驱动和Host controller driver会把它被转化为MMC协议的CMD17或者CMD18指令给到EMMC物理设备.

 

Storage device的设备驱动注册到块设备层,并且扫描分区表, 识别分区表, 然后解析分区表, 把磁盘上的分区注册为块设备. 所以, 当你奇怪为什么没有mmcblkp0这个设备时,  其实不是没有mmcblk0p0,而是p0被定义为就是mmcblk0. 代表了整个磁盘. 

木叶的根

 

我们知道u-boot的boot argument 会把root device的设备名称带给内核, 例如通过参数root=/dev/mmcblk0p2来告知内核, 根目录所在的分区.  事情真的那么简单吗?

原来这里面有一点点小弯弯:

原来在天地混沌的时候, 内核已经为了自己的未来初始化了一个ramfs, 并在ramfs中创建了一些必要的目录和设备节点, 例如/dev , /dev/console, /root.

然后, 为了mount mmcblk0p2, 它又根据设备文件名/dev/mmcblk0p2查找设备变量, 在ramfs下创建设备节点/dev/root, 这个设备文件指向的就是设备mmcblk0p2. 然后把ramfs下的设备/dev/mmcblk0p2挂载到ramfs的/root下. 切换当前路径到ramfs的/root下. 至此已经完成了设备mmcblk0p2的挂载工作, 但是此时它还不是根.

接着, 内核挂载devtmpfs到dev目录, 然后调用sys_chroot(“.”)把change root 到当前路径, 也就是ramfs的/root.

反映到代码上:

 

因此在系统起来后, cat /proc/mounts, 我们可以看到以下信息:

 

可以看到有一个文件系统的类型是rootfs, 它被mount到了“/”.

这个文件系统在init/do_mount.c中被定义, 它就是ramfs的一个实例,

该类型的文件系统在init_rootfs()中被注册.

 

从mount命令的输出上, 我们可以看到/dev/mmcblk0p2被挂载在了”/”, 殊不知这里的根已经不是原来的根.

 

结束语

 

本文没有对代码的细节作过多的分析, 一方面本文不是为了做代码分析的, 另一方面网络上有很多朋友也做过块设备的代码分析, 本文罗列了代码的脉络是为了来更好的表达分析的结果. Block layer是个很复杂的子系统, 有很多关于它的内容,比如IO-schecduler,  buffer cache等,  相信自己可以越来越深入的研究这个子系统.