背景
“文件”在文件系统之中,这是人人理解的概念。但“文件”之上还有一个文件系统?那岂不是成套娃了。但这个其实是可以的。这个就涉及到今天我们要讲的 loop 设备。
很多童鞋在学习 Linux 的文件系统时,涉及到对磁盘设备的格式化,挂载等操作,但苦于没有一个真实的硬盘,一时不知道如何实践。这种时候就可以使用一个文件来模拟块设备。这就是 loop 设备的作用。我们借助 loop 设备,可以让一个文件被当做一个块设备来访问。
举个例子,我们在 ext4 的文件系统目录下创建了一个 minix_test.img 文件,把它当作一个块设备,在上面格式化 minix 的文件系统,并挂载到 /mnt/minix 上。示意图如下:
图片
这种方式有两个很明显好处:
不需要真实的硬盘,就可以格式化、挂载、测试文件系统。
可以近距离的观察文件系统对块设备的使用,比如如何划分 inode 区域、数据区域、位图区域等。这些都将反馈到文件上。
如何使用 loop 设备?
接下来看下如何使用 loop 设备。我们将会用一个普通文件上格式化成 minix 文件系统,然后挂载到 Linux 目录树上。我们使用 loop 设备有两种方式:
- 一种是直接 mount 带上 -o loop 的参数。这种省去了显式创建 loop 设备的过程,步骤简单。
- 另一种方式是先显式的创建 loop 设备,该 loop 设备绑定一个文件,并提供了块设备的对外接口。我们就可以把这个 loop 设备当作一个普通的块设备文件,进行格式化,然后挂载到目录上。
方式一:mount -o loop
用 mount 挂载文件系统的时候,指定某个文件以 loop 设备的方式进行挂载。具体操作如下:
首先,我们创建一个 1GiB 的文件:
dd if=/dev/zero of=./minix_test.img bs=1M count=1024
然后,我们在这个文件上进行 minix 文件系统的格式化:
mkfs.minix ./minix_test.img
最后,我们用 loop 设备的方式进行挂载:
mount -o loop ./minix_test.img /mnt/minix/
这样挂载成功之后,就可以在 /mnt/minix 下进行操作了。该目录挂载的是 minix 类型的文件系统。minix 文件系统是一个磁盘类型的文件系统,它的数据是会写到磁盘进行持久化。所以,我们在这个 /mnt/minix 目录下做的任何操作,这些都会反映到 ./minix_test.img 这个文件上。这个文件就像磁盘一样,在这个上面承载了一个文件系统的数据。
可以尝试在 /mnt/minix 目录下创建一个文件,然后用 hexdump 工具查看 minix_test.img 的内容变化。如下:
# 在 minix 文件系统之上创建一个文件,并写入一个字符串
echo "hello world" >> /mnt/minix/hello.txt
# 查看 minix_test.img 的内容
hexdump -C ./minix_test.img
如果你对 minix 文件系统的分区熟悉的话,就可以明显看到 minix 是如何在 minix_test.img 文件上划分的 inode 区域、数据区域、位图区域等。
方式二:先创建 loop 设备,再挂载使用
这种方式稍微步骤多一点,但其实更容易让人理解其中原理。实际的效果和方式一是等价的。我们可以使用 losetup 命令来管理 loop 设备。
首先,我们创建一个 1GiB 的文件:
dd if=/dev/zero of=./minix_test.img bs=1M count=1024
然后,我们在这个文件上进行 minix 文件系统的格式化:
mkfs.minix ./minix_test.img
再然后,创建和使用 loop 设备:
# 方式一:假定 /dev/loop5 是空闲可用的 loop 设备,下面把 /dev/loop5 和 minix_test.img 关联起来
losetup /dev/loop5 ./minix_test.img
# 方式二:可以简单一点,让 losetup 命令自动找到一个空闲的 loop 设备,然后进行关联
losetup --find --show ./minix_test.img
创建完 loop 设备之后,可以用 losetup 命令列举当前所有的 loop 设备,和它们关联的文件。
最后,把 loop 设备挂载到目录上:
# 假设上一个步骤创建的是 /dev/loop5
mount /dev/loop5 /mnt/minix
这种方式挂载的文件系统和方式一本质上是一样的。
mount 和 losetup 等命令的源代码都在 util-linux 开源库中,感兴趣的童鞋们可以自行查看。
什么是 loop 设备?
本质上来讲,loop 设备是块设备的一种特殊的驱动实现。接下来我们来简单看下 loop 设备的基本原理。
loop 设备的原理
loop 就是一种特殊的块设备驱动。loop 设备是一种 Linux 虚拟的伪设备,它和真实的块设备不同,它并不代表一种特定的硬件设备,而仅仅是满足 Linux 块设备接口的一个虚拟设备。它的作用就是把一个文件模拟成一个块设备。
loop 设备它是怎么模拟的块设备?
loop 设备的代码位于 Linux 的 drivers/block/loop.c 中。在这个文件中,它定义了块设备驱动的接口。
块设备驱动的编程范式:
- 首先,要分配并初始化一个 gendisk 结构体,这是内核代表块设备的核心结构体。它包含了与磁盘相关的信息,loop 设备作为一种特殊的块设备驱动,这个自然是不能少的。
- 然后,初始化一个请求队列。块设备使用请求队列来管理对设备的 I/O 请求。文件系统调用 submit_bio 的调用时,最终就是把请求投递到驱动的队列中。
- 然后,请求处理函数。这个很容易理解,队列里的请求总是要处理的,每个块设备驱动都可以自定义处理方式。
- 最后,块设备操作表(block_device_operations),这个将包含对设备的操作方法,比如打开,读写控制等。
loop 设备如何关联到后端“文件” ?
用户态的处理( losetup 或 mount )
- 打开后端“文件”,拿到文件描述符。
- 打开 loop 设备文件,拿到 loop 设备的描述符。
- 调用 ioctl 把这两个关联起来ioctl(dev_fd, LOOP_CONFIGURE, &lc->config)
内核的处理
- ioctl 的系统调用对应调用 loop 中的 lo_ioctl 函数。
对应了 block_device_operations 的 ioctl 方法。
- 当设置参数为 LOOP_CONFIGURE 的时候,会调用 loop_configure 来分配和初始化 loop 设备。
- loop_configure
获取到后端“文件”的句柄,也就是 struct file* 结构。获取到之后,会做一些校验工作。然后初始化 loop 设备相关的结构体,队列等。
最关键的当然还是把 loop 设备和后端“文件”的句柄关联起来:lo->lo_backing_file = file; 这样的话,等到读写 loop 设备的时候,就可以把请求转发过去。
loop 设备的请求来自于哪里?
loop 设备它对外就是一个块设备,如果在这个之上创建了文件系统,并被当作块设备挂载到目录上之后,那么它的请求来自于它之上的文件系统。
文件系统调用 submit_bio ,把请求投递到块层的队列中。每一个块层的设备它都需要实现一个入队的处理,以供 submit_bio 的流程中调用。
static const struct blk_mq_ops loop_mq_ops = {
.queue_rq = loop_queue_rq,
.complete = lo_complete_rq,
};
loop 设备实现的入队方法就是 loop_queue_rq 。文件系统调用 submit_bio 之后最终就会调用到 loop_queue_rq 这个函数。
> loop_queue_rq
> loop_queue_work
loop_queue_work 函数会把请求放到一个 lo->workqueue 队列中,每一个 loop 设备都对应有这么一个队列。在创建设备文件的时候会同步生成。
loop 设备如何把请求传递给后端文件?
loop 设备还有一个名为 loop_workfn 的函数,是专门用来处理投递到该设备的请求。
> loop_workfn
> loop_handle_cmd
> do_req_filebacked
在 do_req_filebacked 函数中,会按照不同的命令类型来处理请求,比如,写请求会调用 lo_write_simple ,读请求会调用 lo_read_simple 等。
在这个 lo_write_simple 函数中,就会调用 lo_write_bvec ,把写请求写入关联的“文件”中。
static int lo_write_simple(struct loop_device *lo, struct request *rq,
loff_t pos)
{
// lo_backing_file 代表当前 loop 设备关联的文件
ret = lo_write_bvec(lo->lo_backing_file, &bvec, &pos);
}
在 lo_write_bvec 中,调用的是 vfs_iter_write 函数来进行写入。这个函数其实是 VFS 层的一个封装函数,所以相当于就是从顶层调用后端文件的写操作。
在 lo_write_bvec 中,最关键的是 lo->lo_backing_file 这个文件句柄的获取。它的类型是 struct file* ,代表了一个内核打开的文件。即该 loop 设备关联的文件。它的赋值就是在 loop 设备创建的时候。
Linux 套娃之后,I/O 链路是什么样的?
现在来看下,当我们使用 loop 设备,来挂载一个 minix 文件系统,它的 I/O 路径又会是怎么样的呢?
假定创建的 minix_test.img 位于一个 ext4 文件系统。
应用程序 -> 系统调用 -> vfs -> minix 文件系统 -> 块层 -> loop 设备 -> 绑定的文件 -> vfs -> ext4 -> ...
示意图如下:
图片
loop 设备的典型应用有哪些?
其实,loop 设备在很多场景下我们都用过。以下是比较典型的例子:
- 系统模拟和测试:可以使用 loop 设备来模拟不同的存储配置,无需使用物理硬件,就可以进行软件测试或系统配置实验。
- 文件系统开发:开发者可以使用 loop 设备来挂载文件系统,从而方便地测试和调试新的文件系统。
- ISO 映像挂载:Loop 设备还常用于挂载 ISO 文件,无需刻录到物理介质上,使其内容可直接访问。
- 加密磁盘:loop 设备还能和一些加密技术(如dm-crypt)结合,因为 loop 设备可以绑定几乎任意类型的文件,这就给了人们无限的想象空间。我们可以创建一个加密的磁盘镜像,增强数据安全。
其实,Linux 的套娃还远不止这些。举个例子,loop 设备绑定的文件,它可能也是抽象出来的一个文件,比如是一个网络文件系统抽象出来的文件。那这条 I/O 链路就更长了。只要你敢想,在 Linux 中,存在无限套娃的可能。
总结
- 没有磁盘也想玩磁盘类文件系统?可以,用 loop 设备。只要你有文件,Linux 的 loop 设备都可以把文件变成一个“块设备”。
- loop 设备就是一种特殊的块设备驱动。
- 文件之上的的文件系统的增、删、改、查的 I/O 请求,它都将落到文件上。这个可以让我们近距离的观察到文件系统是如何管理磁盘的。
- hexdump 是个好工具,可以用来查看文件上的二进制内容。