使用 loop 设备访问虚拟磁盘

2022-11-20 ⏳6.9分钟(2.7千字) g

loop 设备是 Linux 下一种虚拟设备。它广泛应用于跟虚拟磁盘、ISO镜像等文件的处理领域。最近在折腾 ARM 虚拟系统,需要使用 loop 设备制作系统镜像。多方调研才对它有了相对全面的认识。为了把文章讲清楚,本文还会介绍 Linux 磁盘管理的基础知识,也欢迎新手阅读。

磁盘设备

Linux 以文件的形式管理磁盘。一般第一块设备对应的文件是/dev/sda。大家可能不知道,我们是可以直接读写这个文件的。在 Linux 系统中,这就是一个普通的文件。唯一的不同点就是文件的大小是固定的,也就是磁盘的容量。我们没法在这个文件结尾处追加数据。但整个文件所有数据都可以读写。比如我们可以把写入hello:

⚠️ 不要在自己的系统上操作,会清空整个磁盘上的数据!⚠️

echo -n hello > /dev/sda

然后我们还可以读出这五个字节:

head -c 5 /dev/sda
hello%

Linux 内核的驱动会将对应的读写调用转换成磁盘操作指令,最终hello这五个字节的数据会被保存到底层的磁盘上。以这种方式操作磁盘,不同程序之间还可能相互影响,程序还需要自行处理文件名跟文件内容的映射。所以内核提供解决了这些基础问题,于是便有了分区和文件系统。

分区表

所谓分区就是把磁盘划分成几块不同的空间。每一块的类型和开始结束位置等信息都保存硬盘开头的某段空间内。这一段也叫分区表。主流的分区表有 DOS 和 GPT 两种。Linux 下常见的分区工具是fdisk和gdisk,都支持两种分区表。

一旦完成分区,Linux 就会自动识别分区表信息,然后为每一个分区创建对应的设备文件。比如我们把/dev/sda分成两个区,内核就会分别创建/dev/sda1和/dev/sda2两个文件。

这两个文件跟/dev/sda一样,也可以直接读写。唯一的不同是读写/dev/sda1的程序不会影响读写/dev/sda2的程序。这是操作系统提供的最小粒度的隔离。但同样不支持通过文件名读写数据。

文件系统

为此,我们还需要为每个分区做格式化处理,也就创建文件系统信息。Linux 支持很多文件系统,比如 EXT4/FAT/XFS/BTRFS 等等。最觉的是 EXT4,很多 Linux 发行版都用它作 root 文件系统。每种文件系统都提供对应的工具。比如创建 EXT4 系统需要使用mkfs.ext4:

mkfs.ext4 /dev/sda1

注意,创建文件系统的时候需要指定分区的设备文件/dev/sda1,而不能是磁盘的设备文件。理论上也可以直接用/dev/sda,但我没试过。读者可以试试,后果自负😄

创建好 EXT4 文件系统后,我们还是不能直接使用。相反,我们需要执行所为的挂载(mount)操作:

mount /dev/sda1 /mnt

mount第一个参数是分区设备,第二个参数是要挂载的文件夹,也叫挂载点。/mnt是 Linux 下传统的挂载点,一般临时挂载设备都用它。你也可以自己任意创建挂载目录。不管挂载目录里原来有什么文件,一但完成挂载,里就会展示对应设备里的文件和目录。

挂载对应的卸载umount,不是unmount😂

umount /mnt

卸载后挂载点目录会重新显示原来的内容。所以初学者不要慌😄

自动挂载

其实大家可以把挂载跟前面的/dev/sda1对比。因为分区设备的路径是内核确定的,所以内核会根据分区表信息自动创建分区设备。这本身就是在「挂载」分区表信息。但到了文件系统就不能这么操作了。因为内核不知道要把文件系统挂载到哪个文件夹,所以内核没办法自动挂载。需要用户手工操作。正因如此,早期的 Linux 系统对用户非常不友好。因为不明真想,用户老是觉得 Linux 不好用。其实确实不好用。早期在挂载的时候还要用户手工指定文件系统类型!

Linux 的这种设计也是有初衷的。UNIX 系统无条件信任用户。哪怕用户输入rm -rf /它也会执行!因为相信,所以就要求用户充分了解自己的系统,也就要求用户对自己的行为负责。所以能力越大,责任越大。作为 UNIX 用户,你不能无脑执行一个命令然后要求系统为你的行为兜底。这是 Linux 从 UNIX 继承的哲学。

我是很喜欢这种设计,但它确实对新人不友好。随着时间的发展,Linux 通过 udev 系统实现了自动挂载功能。典型的领域就是插上优盘后自动弹出对应的文件夹。这套体系只是前面所说内容的外挂,本质上还是在用mount命令来挂载。只不过新系统对应挂载点的位置做了统一约定,所以才能实现自动打开优盘或者光盘的功能。

以上都是背景知识。说了半天,还没提今天的主角 loop 设备!

虚拟光盘

我们前面说过,内核会把写入/dev/sda文件的内容直接保存到真正的硬盘设备。但有时候我们不想直接操作设备,比如读取ISO镜像。所谓ISO镜像就是从光盘里复制的数据文件。正常情况下要读取该文件的内容需要先把它记录到一张新光盘,然后用光驱设备读取。对应的 Linux 内核则是在/dev/cdrom设备文件(不同发行版可能会略有差异)。也就是说从/dev/cdrom这个文件读取的内容就是来自光盘的内容。

可以现在社会光驱已经很少见了,刻录机更是稀有。即便所有的设备都置办齐全,应用程序能读取的也不过是另一个文件/dev/cdrom。也就是说经历了ISO文件➜刻录机➜光盘➜光驱➜/dev/cdrom一系列过程,应用程序只不过是读了另外一个文件😂哪能不能省掉所有的中间流程,直接读ISO文件就香吗?

这就要用到 loop 设备。loop 是一种虚拟设备,对应设备文件为/dev/loop0。如果有多个则依次编号。操作 loop 设备需要用到losetup命令。

比如我们先把ISO文件关联到 loop 设备:

losetup /dev/loop0 alpine-virt-3.16.3-x86_64.iso

关联之后可以查看对应关系:

losetup
NAME       SIZELIMIT OFFSET AUTOCLEAR RO BACK-FILE                              DIO LOG-SEC
/dev/loop0         0      0         0  0 /home/ts/alpine-virt-3.16.3-x86_64.iso   0     512

这个时候我们就可以挂载光盘了:

mount /dev/loop0 /mnt
ls /mnt
apks  boot  efi

也就是说现在/dev/loop0设备等同于原来的/dev/cdrom设备。挂载之后就可以在/mnt目录查看光盘镜像里的内容了。

以上两步甚至可以合二为一:

mount -o loop  alpine-virt-3.16.3-x86_64.iso

用完之后后需要先卸载/mnt,再取消关联:

losetup -d /dev/loop0

光盘比较特殊,不能分区。所以不能算是典型使用场景。但是这里边有一个关键,好所有对 loop 设备的读写操作都会被「转发」给对应的普通文件,上例中是ISO镜像文件。也就是说我们不需要真正的光驱设备,却实现了读取ISO镜像内容。这就是所谓的虚拟光驱。

虚拟硬盘

同样的原理也适用于普通磁盘。我们在使用虚拟机的时候会创建各种虚拟磁盘,这些本质上也是一种镜像文件。跟ISO文件不同的是虚拟磁盘文件可以写入内容,而且容量还可以调整。但处理的原理一样的,都需要通过 loop 设备关联。

并非所有虚拟磁盘都能被内核直接处理。像 cow2 这种被 QEMU 压缩过,内核无法处理是。但我们可以把它们转换成所谓的 raw 格式文件,内核就能处理了。

在 Linux 下,创建虚拟磁盘就是创建一个普通的文件:

dd if=/dev/zero of=disk1.img bs=1M count=256M

然后跟 loop 设备关联:

losetup /dev/loop0 disk1.img

这个时候/dev/loop0就变成跟/dev/sda设备一样的磁盘设备了。唯一的区别是/dev/sda会把数据真正保存到硬盘里,而此时的/dev/loop0则会把数据保存到disk1.img文件。

我们可以使用fdisk对/dev/loop0进行分区。分区后内核会自动创建类似/dev/loop0p1这样的分区设备文件。然后我们用mkfs.ext4给/dev/loop0p1等分区创建文件系统。最后将其挂载到某个文件夹。这之后在挂载点内的所有文件操作都会被记录到disk1.img这个虚拟的磁盘中。

也有文章说需要执行kpartx -a /dev/loop0系统才会创建对应的分区设备节点,而且是在/dev/mapper目录。但我实验下来不需要这一步。可能是新系统更加智能了。但如果大家执行了此命令,那么在解除关联之前还需要执行kpart -d /dev/loop0。

如果disk1.img是 QEMU 创建的并且安装了操作系统,那么我们也可以在挂载之后查看对应系统的根目录,甚至还可以修改里面的文件。但前提是当前系统支持对应的文件系统。

以上就是全部内容,希望能对大家有所帮助。

参考链接: