Linux 式套娃,把“文件系统”安装在一个“文件”上?

背景

“文件”在文件系统之中,这是人人理解的概念。但“文件”之上还有一个文件系统?那岂不是成套娃了。但这个其实是可以的。这个就涉及到今天我们要讲的 loop 设备。

很多童鞋在学习 Linux 的文件系统时,涉及到对磁盘设备的格式化,挂载等操作,但苦于没有一个真实的硬盘,一时不知道如何实践。这种时候就可以使用一个文件来模拟块设备。这就是 loop 设备的作用。我们借助 loop 设备,可以让一个文件被当做一个块设备来访问。

举个例子,我们在 ext4 的文件系统目录下创建了一个 minix_test.img 文件,把它当作一个块设备,在上面格式化 minix 的文件系统,并挂载到 /mnt/minix 上。示意图如下:

图片

这种方式有两个很明显好处:

不需要真实的硬盘,就可以格式化、挂载、测试文件系统。

可以近距离的观察文件系统对块设备的使用,比如如何划分 inode 区域、数据区域、位图区域等。这些都将反馈到文件上。

如何使用 loop 设备?

接下来看下如何使用 loop 设备。我们将会用一个普通文件上格式化成 minix 文件系统,然后挂载到 Linux 目录树上。我们使用 loop 设备有两种方式:

  1. 一种是直接 mount 带上 -o loop 的参数。这种省去了显式创建 loop 设备的过程,步骤简单。
  2. 另一种方式是先显式的创建 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 中。在这个文件中,它定义了块设备驱动的接口。

块设备驱动的编程范式:

  1. 首先,要分配并初始化一个 gendisk 结构体,这是内核代表块设备的核心结构体。它包含了与磁盘相关的信息,loop 设备作为一种特殊的块设备驱动,这个自然是不能少的。
  2. 然后,初始化一个请求队列。块设备使用请求队列来管理对设备的 I/O 请求。文件系统调用 submit_bio 的调用时,最终就是把请求投递到驱动的队列中。
  3. 然后,请求处理函数。这个很容易理解,队列里的请求总是要处理的,每个块设备驱动都可以自定义处理方式。
  4. 最后,块设备操作表(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 是个好工具,可以用来查看文件上的二进制内容。

来源:奇伢云存储 

<think>嗯,用户遇到了VSCode中gcc路径配置导致的循环报错问题,具体是“No such file or directory”。我需要先理解这个错误的原因,然后给出解决方案。 首先,根据引用[1]和[2],gcc找不到文件或目录可能有两个主要原因:要么是gcc本身路径没配置好,要么是源文件路径错误。用户提到的是循环报错,可能是在配置过程中路径设置不当,导致VSCode无法正确找到gcc或源文件。 接下来,引用[3]提到VSCode需要正确配置头文件路径和编译命令。用户可能在设置includePath时没有正确指定,或者在tasks.json中未正确添加-I参数。另外,引用[4]指出工作目录的问题,如果相对路径不正确,也会导致找不到文件。 可能的解决方案包括检查系统环境变量中的gcc路径,确认是否正确安装gcc。然后检查VSCode的工作目录设置,确保源文件和头文件的路径正确。另外,tasks.json中的args需要包含必要的编译选项,如-I来指定头文件路径。 用户可能已经尝试过配置路径,但由于循环报错,可能某些配置之间有冲突。例如,环境变量中的路径和VSCode内部设置的路径不一致,或者多个扩展干扰了编译过程。需要逐步排查,先验证gcc在命令行中是否能正常工作,再检查VSCode的任务配置。 还要注意,如果用户的项目结构复杂,子目录中的文件引用上级目录的文件时,相对路径可能需要调整。或者,在launch.json中设置正确的工作目录,避免默认的工作目录与当前文件所在目录不一致的问题,如引用[4]所述。 最后,可能需要指导用户如何清理配置,重新设置,或者禁用其他扩展,以排除干扰因素。此外,查看编译日志和调试输出,可以帮助定位具体的问题所在。</think>### 分步解决方案:VSCode gcc路径配置循环报错问题 #### 1. **验证GCC基础环境配置** - **检查系统环境变量** 在终端中执行 `gcc --version`,若提示找不到命令,说明系统未正确配置GCC路径。需手动添加GCC的安装路径到系统环境变量(如Windows的`Path`或Linux的`$PATH`)。 *示例:Windows下MinGW默认路径为 `C:\MinGW\bin`* [^1][^2]。 - **确认GCC安装完整性** 若环境变量正确但报错 `cc1plus: No such file or directory`,可能是GCC组件缺失。重新安装GCC时需勾选C/C++编译器选项(如MinGW的`mingw32-gcc-g++`包)[^1][^5]。 #### 2. **配置VSCode工作目录与文件路径** - **修正源文件路径** 确保在VSCode中打开的**工作区根目录**包含源文件。若文件位于子目录(如`src/main.c`),需在`tasks.json`中通过`${fileDirname}`指定绝对路径: ```json "args": ["${file}", "-o", "${fileDirname}/main", "-I", "头文件绝对路径"] ``` 避免相对路径歧义(如`.\main.c`可能被解析为错误目录)[^2][^4]。 - **设置头文件搜索路径** 在`c_cpp_properties.json`中明确添加头文件目录: ```json "includePath": ["${workspaceFolder}/include", "第三方库绝对路径"] ``` 同时需在`tasks.json`的编译命令中添加 `-I 头文件路径` 参数[^3][^5]。 #### 3. **调试tasks.json配置** - **避免路径循环冲突** 若配置后仍报错,检查`tasks.json`中是否存在路径重复包含或冲突。例如: ```json "args": ["-I", "/usr/local/include", "-I", "${workspaceFolder}/include"] // 明确优先级 ``` 使用绝对路径替代相对路径,减少环境变量依赖[^3][^5]。 - **启用编译日志诊断** 在`tasks.json`中添加 `"verbose": true`,查看完整编译命令: ```json "options": {"verbose": true} ``` 根据输出日志定位缺失文件或路径错误[^5]。 #### 4. **处理扩展冲突与缓存问题** - **禁用冲突扩展** 临时禁用其他C/C++相关扩展(如Clang插件),排除插件间配置覆盖问题。 - **清理VSCode缓存** 删除项目目录下的`.vscode`文件夹(备份设置后),重启VSCode重新生成配置。 #### 5. **验证跨平台编译配置** - **匹配系统架构与工具链** 若涉及交叉编译(如Android NDK),确保`launch.json`中配置的编译器路径与目标平台一致(如`arm-linux-androideabi-gcc`路径正确)。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值