Windows中,将磁盘抽象成了一个个的磁盘驱动器,并为他们分配了CBDEF等等的多个盘符,文件系统由此来访问磁盘上的文件。在之前,即使只有一块磁盘,在装机时也会在一块磁盘上手动分区为C盘和其他的逻辑分区,防止在系统恢复时导致数据丢失,但是也经常因为C盘空间不够大导致问题。
而Unix或者inux上,/
之下,就是我们的根目录,没有其它的分区的概念,如果我们有多块磁盘或者多块设备的情况下,因为没有了分区、驱动器的概念,所以我们如果要访问这些设备,我们必然要一个切入点。而Linux采取的操作是设备 + 挂载,一般来说,设备会被挂载到/dev/
下,此时设备也是一个文件,它也存在于文件系统中,比如一块磁盘:/dev/sda
,就是我们加载的第一块磁盘。如果我们要挂载第二块磁盘,我们需要把/dev/sdb
挂载到某个目录下,例如/mnt
。
1. 挂载点
Unix允许任意目录挂载一个设备代表的目录树,比如我们可以使用lsblk
查看当前挂载的设备信息:
➜ ~ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda 8:0 0 363.1M 1 disk
sdb 8:16 0 4G 0 disk [SWAP]
sdc 8:32 0 1T 0 disk /mnt/wslg/distro
/
其中,Linux为我们挂载的三个硬盘,分别命名为sda、sdb和sdc,这里使用的是WSL2虚拟的Windows磁盘,其中的第三个,正对应着我们Windows主机对于的磁盘,但是被子系统挂载到Linux中,作为根目录了,其中它的挂载点分别是:/mnt/wslg/distro
和/
,也就是我们的Root目录,如果我们去访问/mnt/wslg/distro
,得到就是我们的根目录:
➜ ~ cd /mnt/wslg/distro
➜ distro ls
bin dev home lib lib64 lost+found mnt proc run snap sys usr wslCdKadG wslKCLOdl wslcfMceH wsllFeHKN
boot etc init lib32 libx32 media opt root sbin srv tmp var wslKBJNnH wslOaGApd wsljAFjog wslnOecCh
2. 在挂载点上挂载文件或者设备
mount系统调用可以帮助我们在文件系统的任何目录下挂载一个新的设备,比如是独立的磁盘、U盘,亦或者是文件,常见于挂载ISO镜像文件,比如此时我们有一个新的U盘插入系统之后,我们需要去访问它,此时我们就需要手动将这个新的设备挂载到磁盘的某个位置上。
插入U盘之后,以WSL为例,我们首先需要去查看对应的U盘盘符:F。
然后再在Linux中,使用mount命令挂载对应的U盘:
/mnt sudo mount -t drvfs F: /mnt/
完成之后,U盘中的内容就被挂载到了/mnt/文件夹下,此时你去访问/mnt就好像在访问U盘一样。
如果我们需要解除挂载,那么只需要
sudo umount /mnt
如果你先后挂载了A和B两个磁盘或者文件,此时的mnt并不会变得非常混乱,因为后挂载的B会在A之上,就好像一个栈一样,在AB都被挂载到一个挂载点(/mnt)下之后,你去访问将只能访问到B,一旦B被umout掉了之后,你才能去访问A的内容,所以一个挂载点上虽然可以挂载多个数据,但是只有最后一个被挂载的数据可用。
如果当前目录是/mnt目录,并且将A挂载在/mnt下,此时再去挂载B完成之后,你再调用ls查看文件目录你会发现仍然是A的目录,即使使用pwd查看路径也没有问题,但是一旦你退到上级目录,再重新进入/mnt,此时才是对于的B目录。
同理,你也可以挂载一个镜像文件,比如img文件:
➜ / sudo mount /home/r0ei1y/img/disk.img /mnt
不难发现,我们在Linux中对设备或者镜像文件的操作点就在于我们的挂载点,无论是挂载还是推出(umount)其实都是在对挂载点做操作。
3. 文件挂载与Linux启动的关系
相对而言,启动阶段的文件系统是特殊的。
系统启动初期是不存在文件系统这个东西的,是后来在经过初始化,文件系统才承担起文件管理的重任的,那么在BIOS上电到文件系统初始化完成之前的一段时间,Linux也需要去访问文件,使用一些二进制文件或者驱动来实现系统的启动,那么这一段时间似乎就成了一块真空的时间片。
3.1 initrd/initramfs
Init 初始化
ram 随机访问存储器(内存)
fs 文件系统
即一个用于Linux初始化的内存文件系统,也叫initrd,即init ram disk
。
用来支持两阶段的引导过程。initrd文件中包含了各种可执行程序和驱动程序,它们可以用来挂载实际的根文件系统,然后再将这个 initrd RAM磁盘卸载,并释放内存。在很多嵌入式Linux系统中,initrd 就是最终的根文件系统。
其中的文件一般来说非常少,比如一个用于系统其他组件初始化的脚本或者是一些最最基本的,用于支撑上述组件初始化的一些工具程序,比如busybox,它虽然是单个程序,但是其中有非常多的二进制功能模块,不同的指令对应不同的功能,比如使用busybox中内置的cat查看文本:
/bin busybox cat ~/install.sh
亦或者是使用 busybox中内置的shell程序来解析用户输入命令:
➜ ~bin busybox sh
BusyBox v1.30.1 (Ubuntu 1:1.30.1-7ubuntu3) built-in shell (ash)
Enter 'help' for a list of built-in commands.
/usr/bin $
总而言之,他们都是为了系统启动初期阶段内置的一些程序。如果你去实现一个最小的Linux,你就可以通过链接依赖BusyBox中的一些二进制命令来完成简易版的系统工具,比如cat、比如shell。(当然功能上稍微会弱一些)
3.2 切换文件系统
在上面提到的,initramfs完成自己的使命之后,他会做几件事情,将自己替换成对应的、真正的可持续的文件系统:
- 读入真正的磁盘设备,比如读入后是:
/dev/sda
- 创建新的挂载点比如是:
/new_root
- 把(真)磁盘设备挂载到挂载点
new_root
之上。 - 通过二进制工具(也就是某个程序,可能是busybox中的某个程序)将原有的,通过initramfs创建的内存文件系统替换为
new_root
对应的文件目录。
此时new_root,就成了新的/
目录,对应的正是此前通过initramfs中加载的/dev/sda
目录,
背后的系统调用正是pivot_root,对应的busybox命令是busybox switch_root。
4. [实践] 试试initramfs
实验工具集合:github.com/R0ei1y/exp-…
其中的Makefile中内置了三个相关的指令,详见工具集合中的readme
上文提到了,启动linux的时候,会有一个临时的内存文件系统帮我们做一些相关的启动工作,我们可以借助一个linux-5.4.226
内核来实践一下。
首先我们下载内核,完成之后下载initramfs需要的内容,置于initramfs文件夹下,整体结构长这样:
➜ initramfs tree
.
├── bin
│ └── busybox
└── init
整体上就是一个bin文件夹,下面存有busybox这个二进制文件,init则是一个shell脚本文件,我们在其中直接使用/bin/busybox下的sh命令
打开一个busybox中内置的shell,不做其他的操作:
➜ initramfs cat init
#!/bin/busybox sh
/bin/busybox sh
在这里你实际上可以通过switch_root替换掉我们的内存文件系统,但是秉持着“先跑起来再说”的原则,我们一步步来,在其中先运行一个busybox中的Shell。
其次,我们需编译一下Linux的内核,大致流程是先输入make menuconfig
进行编译,完成之后会出现vmlinux和在其它目录下的:./arch/x86/boot/bzImage
,后者是我们需要的Linux内核文件。
然后我们需要将initramfs文件夹下的文件制作成一个文件系统镜像,因为我们使用的是qemu来进行模拟,所以直接用cpio + gzip将它压缩成一个gz压缩文件即可。
@cd initramfs && find . -print0 | cpio --null -ov --format=newc | gzip -9 \
> ../initramfs.cpio.gz
完成之后,此时的initramfs.cpio.gz位于Linux内核源码的根目录,然后我们可以使用如下命令让qemu运行起来:
qemu-system-x86_64 \
-nographic \
-serial mon:stdio \
-m 128 \
-kernel ./arch/x86/boot/bzImage \
-initrd ./initramfs.cpio.gz \
-append "console=ttyS0 quiet acpi=off"
紧接着你会发现你运行起来的Qemu中,除了一个Shell,啥也没有,甚至常用的ls、clear等等命令都没有,
但是有一个特殊的命令是可以使用的,也就是cd,因为cd是Shell内置的命令,你可以在一个正常的Linux环境下使用where查看一下,诸如ls、clear等等通常都是单独的程序,而cd则是内置的命令,因为每个程序都有自己的工作目录,工作目录一般会由自己去切换而不是由其它的程序切换,cd就是起到切换工作目录作用的命令。
这样一来,我们就加载了我们的initramfs作为我们的初始文件系统,虽然上述的命令或者程序在这个文件系统中并不存在,但是在busybox中确有一些类似的程序,可以作为暂时的替代使用。
你可以在initramfs文件夹下预制一个你自己编写的HelloWorld的程序,你会发现也是能正常执行的(当然,你得静态编译):
/bin # busybox ls
busybox hello
/bin # ./hello
HelloWorld!
/bin #
正经的一个操作系统应该不会直接用初始文件系统给用户使用,所以在initramfs中,我们需要补全它的功能,在init文件中做文章,首先我们没有关于mount指令的一个壳程序,我们无法在initramfs中使用我们的mount、umount和创建文件夹相关的系统调用。我们都要在前面套上一个busybox的壳。
因为本质上你想要调用mount,最终是调用到mount系统调用,Linux一般会提供一个程序:mount程序会调用mount系统调用,mount程序就是mount调用的壳程序:
➜ ~ where mount /usr/bin/mount /bin/mount ➜ ~ where mkdir /usr/bin/mkdir /bin/mkdir ➜ ~
我们可以做一层链接,把busybox的一些命令给它连接到外部,这样你就可以愉快地ls、mkdir了,比如:
/ # /bin/busybox ln -s /bin/busybox /bin/ls
/ # ls
bin dev init root
/ # /bin/busybox ln -s /bin/busybox /bin/mount
/ # mount
mount: no /proc/mounts
/ # /bin/busybox ln -s /bin/busybox /bin/mkdir
/ # mkdir
BusyBox v1.31.1 (2020-03-22 13:22:47 UTC) multi-call binary.
Usage: mkdir [OPTIONS] DIRECTORY...
Create DIRECTORY
-m MODE Mode
-p No error if exists; make parent directories as needed
/ #
- 虚拟磁盘镜像创建
因为我们要实现initramfs向real disk fs的切换,所以我们要在qemu中指定一个磁盘驱动,也就是一个磁盘设备,我们使用如下的命令创建一个磁盘镜像文件,并格式化为 ext2:
dd if=/dev/zero of=./disk.img bs=1M count=1024
mkfs.ext2 -q ./disk.img
在qemu的启动项中添加:
-hda ./qemu-driver/disk.img # 对应disk.img的位置
- 在虚拟磁盘中创建init脚本
然后我们要挂载这个镜像,在mnt下操作,并在里面创建新的bin和etc/init文件,这是隶属于新的文件系统的文件了,当然你也可以直接从initramfs复制一份,我这里直接复制了一份,稍微改了一下内容:
➜ qemu-driver sudo mount disk.img /mnt
➜ /mnt tree
├── bin
│ ├── busybox
│ ├── hello
│ └── hello-dynamic
├── etc
│ └── init
└── lost+found [error opening dir]
#!/bin/busybox sh
export PS1='(iREd)➜ '
/bin/busybox sh
➜ qemu-driver sudo umount /mnt
- 在initramfs中,挂载新文件系统
重新启动qemu,此时我们可以按照如下的步骤去调用:
# 默认你已经做了busybox的链接了,如果没有在前三条命令前加上busybox
mknod /dev/sda b 8 0
mkdir -p /newroot
mount -t ext2 /dev/sda /newroot
上述的内容依次是:
- 创建设备节点,可以理解为找到qemu上挂载的
disk.img
- 创建挂载点:newroot
- 挂载
然后使用switch_root
工具来切换根文件系统:
- (错误的)使用switch_root切换文件系统
exec busybox switch_root /newroot /etc/init
你会发现失败了。
内核意料之外地(也算是意料之中,实验基本不会一次就成功)抛出了一个Panic:
(rEd)➜ exec busybox switch_root /newroot /etc/init
BusyBox v1.31.1 (2020-03-22 13:22:47 UTC) multi-call binary.
Usage: switch_root [-c CONSOLE_DEV] NEW_ROOT NEW_INIT [ARGS]
Free initramfs and switch to another root fs:
chroot to NEW_ROOT, delete all in /, move NEW_ROOT to /,
execute NEW_INIT. PID must be 1. NEW_ROOT must be a mountpoint.
[ 146.946058] Kernel panic - not syncing: Attempted to kill init! exitc0[ 146.946937] CPU: 0 PID: 1 Comm: init Not tainted 5.4.226 #1
这个报错的根本原因是,switch_root只能在PID = 1的进程中被调用,通常是init,而我们现在使用的Shell是由initramfs中的init派生出来的子进程,所以这个操作是不被允许的。
现实里基本上也不会出现这种情况,毕竟大多情况下initramfs完成了切换文件系统使命之后,就会主动去执行目标文件系统的init或者是/etc/init,很少有在init中直接启动一个Shell,然后Shell中做操作,Shell的PID并不等于1。
- (正确的)使用switch_root切换文件系统
因此,我们要把这个内容写入到initramfs的init中去,以确保在init脚本执行的对应的init进程中,完成对文件系统的切换:
busybox mknod /dev/sda b 8 0
busybox mkdir -p /newroot
busybox mount -t ext2 /dev/sda /newroot
exec busybox switch_root /newroot /etc/init
在这之后,就可以成功地从initramfs切换到disk.img镜像对应的文件系统了,因为我们修改了PS1的值,对应的用户命令行的显示就由原先的#
变成了(iREd)➜
:
(iREd)➜ ls
bin etc lost+found proc s
(iREd)➜ bin/hello
HelloWorld!
(iREd)➜
此外还要注意的点是,我们在新的文件系统的init文件中是这样写的:
#!/bin/busybox sh export PS1='(iREd)➜ ' /bin/busybox sh
如果我们将最后一行的内容注释掉,你会发现系统在完成switch_root之后就抛出了一个Panic了,这是因为我们的init进程在执行完/etc/init脚本之后关闭了,如果不做额外的配置,此时系统也会跟着关闭,并抛出一个Panic:
end Kernel panic - not syncing: Attempted to kill init! exit
由此,就完成了从内存文件系统 -> 磁盘文件系统的一次跃迁。