“文件”在文件系统之中,这是人人理解的概念。但“文件”之上还有一个文件系统?那岂不是成套娃了。但这个其实是可以的。这个就涉及到今天我们要讲的 loop 设备。
很多童鞋在学习 Linux 的文件系统时,涉及到对磁盘设备的格式化,挂载等操作,但苦于没有一个真实的硬盘,一时不知道如何实践。这种时候就可以使用一个文件来模拟块设备。这就是 loop 设备的作用。我们借助 loop 设备,可以让一个文件被当做一个块设备来访问。
举个例子,我们在 ext4 的文件系统目录下创建了一个 minix_test.img 文件,把它当作一个块设备,在上面格式化 minix 的文件系统,并挂载到 /mnt/minix 上。示意图如下:
这种方式有两个很明显好处:
不需要真实的硬盘,就可以格式化、挂载、测试文件系统。
可以近距离的观察文件系统对块设备的使用,比如如何划分 inode 区域、数据区域、位图区域等。这些都将反馈到文件上。
接下来看下如何使用 loop 设备。我们将会用一个普通文件上格式化成 minix 文件系统,然后挂载到 Linux 目录树上。我们使用 loop 设备有两种方式:
用 mount 挂载文件系统的时候,指定某个文件以 loop 设备的方式进行挂载。具体操作如下:
首先,我们创建一个 1GiB 的文件:
然后,我们在这个文件上进行 minix 文件系统的格式化:
最后,我们用 loop 设备的方式进行挂载:
这样挂载成功之后,就可以在 /mnt/minix 下进行操作了。该目录挂载的是 minix 类型的文件系统。minix 文件系统是一个磁盘类型的文件系统,它的数据是会写到磁盘进行持久化。所以,我们在这个 /mnt/minix 目录下做的任何操作,这些都会反映到 ./minix_test.img 这个文件上。这个文件就像磁盘一样,在这个上面承载了一个文件系统的数据。
可以尝试在 /mnt/minix 目录下创建一个文件,然后用 hexdump 工具查看 minix_test.img 的内容变化。如下:
如果你对 minix 文件系统的分区熟悉的话,就可以明显看到 minix 是如何在 minix_test.img 文件上划分的 inode 区域、数据区域、位图区域等。
这种方式稍微步骤多一点,但其实更容易让人理解其中原理。实际的效果和方式一是等价的。我们可以使用 losetup 命令来管理 loop 设备。
首先,我们创建一个 1GiB 的文件:
然后,我们在这个文件上进行 minix 文件系统的格式化:
再然后,创建和使用 loop 设备:
创建完 loop 设备之后,可以用 losetup 命令列举当前所有的 loop 设备,和它们关联的文件。
最后,把 loop 设备挂载到目录上:
这种方式挂载的文件系统和方式一本质上是一样的。
mount 和 losetup 等命令的源代码都在 util-linux 开源库中,感兴趣的童鞋们可以自行查看。
本质上来讲,loop 设备是块设备的一种特殊的驱动实现。接下来我们来简单看下 loop 设备的基本原理。
loop 就是一种特殊的块设备驱动。loop 设备是一种 Linux 虚拟的伪设备,它和真实的块设备不同,它并不代表一种特定的硬件设备,而仅仅是满足 Linux 块设备接口的一个虚拟设备。它的作用就是把一个文件模拟成一个块设备。
loop 设备的代码位于 Linux 的 drivers/block/loop.c 中。在这个文件中,它定义了块设备驱动的接口。
块设备驱动的编程范式:
对应了 block_device_operations 的 ioctl 方法。
获取到后端“文件”的句柄,也就是 struct file* 结构。获取到之后,会做一些校验工作。然后初始化 loop 设备相关的结构体,队列等。
最关键的当然还是把 loop 设备和后端“文件”的句柄关联起来:lo->lo_backing_file = file; 这样的话,等到读写 loop 设备的时候,就可以把请求转发过去。
loop 设备它对外就是一个块设备,如果在这个之上创建了文件系统,并被当作块设备挂载到目录上之后,那么它的请求来自于它之上的文件系统。
文件系统调用 submit_bio ,把请求投递到块层的队列中。每一个块层的设备它都需要实现一个入队的处理,以供 submit_bio 的流程中调用。
loop 设备实现的入队方法就是 loop_queue_rq 。文件系统调用 submit_bio 之后最终就会调用到 loop_queue_rq 这个函数。
loop_queue_work 函数会把请求放到一个 lo->workqueue 队列中,每一个 loop 设备都对应有这么一个队列。在创建设备文件的时候会同步生成。
loop 设备还有一个名为 loop_workfn 的函数,是专门用来处理投递到该设备的请求。
在 do_req_filebacked 函数中,会按照不同的命令类型来处理请求,比如,写请求会调用 lo_write_simple ,读请求会调用 lo_read_simple 等。
在这个 lo_write_simple 函数中,就会调用 lo_write_bvec ,把写请求写入关联的“文件”中。
在 lo_write_bvec 中,调用的是 vfs_iter_write 函数来进行写入。这个函数其实是 VFS 层的一个封装函数,所以相当于就是从顶层调用后端文件的写操作。
在 lo_write_bvec 中,最关键的是 lo->lo_backing_file 这个文件句柄的获取。它的类型是 struct file* ,代表了一个内核打开的文件。即该 loop 设备关联的文件。它的赋值就是在 loop 设备创建的时候。
现在来看下,当我们使用 loop 设备,来挂载一个 minix 文件系统,它的 I/O 路径又会是怎么样的呢?
假定创建的 minix_test.img 位于一个 ext4 文件系统。
应用程序 -> 系统调用 -> vfs -> minix 文件系统 -> 块层 -> loop 设备 -> 绑定的文件 -> vfs -> ext4 -> ...
示意图如下:
图片
其实,loop 设备在很多场景下我们都用过。以下是比较典型的例子:
其实,Linux 的套娃还远不止这些。举个例子,loop 设备绑定的文件,它可能也是抽象出来的一个文件,比如是一个网络文件系统抽象出来的文件。那这条 I/O 链路就更长了。只要你敢想,在 Linux 中,存在无限套娃的可能。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70013542/viewspace-3019293/ ,如需转载,请注明出处,否则将追究法律责任。