FUSE getattr操作是否应该始终串行化

Should the FUSE getattr operation always be serialised?

本文关键字:getattr 操作 是否 FUSE      更新时间:2023-10-16

我正在实现一个FUSE文件系统,旨在通过熟悉的POSIX调用提供对文件的访问,这些文件实际上存储在RESTful API后面。文件系统会在第一次检索到文件后对其进行缓存,以便在后续访问中更容易获得这些文件。

我在多线程模式下运行文件系统(这是FUSE的默认模式),但发现getattr调用似乎是串行的,即使其他调用可以并行进行。

当打开一个文件时,FUSE总是首先调用getattr,而我支持的客户端需要这个初始调用返回的文件大小是准确的(我无法控制这种行为)。这意味着,如果我没有缓存文件,我实际上需要通过RESTful API调用来获取信息。有时,这些呼叫发生在高延迟网络上,往返时间约为600毫秒。

由于getattr调用的明显顺序性,对当前未缓存的文件的任何访问都将导致整个文件系统在为该getattr提供服务时阻止任何新操作。

我已经想出了很多方法来解决这个问题,但都显得很难看或冗长,真的,我只想让getattr调用像所有其他调用一样并行运行。

查看源代码,我不明白getattr为什么会这样做,FUSE确实锁定了tree_lock互斥锁,但仅用于读取,并且不会同时发生写入。

为了在这个问题中发布一些简单的内容,我提出了一个非常基本的实现,它只支持getattr,并允许简单地演示这个问题。

#ifndef FUSE_USE_VERSION
#define FUSE_USE_VERSION 22
#endif
#include <fuse.h>
#include <iostream>
static int GetAttr(const char *path, struct stat *stbuf)
{
    std::cout << "Before: " << path << std::endl;
    sleep(5);
    std::cout << "After: " << path << std::endl;
    return -1;
}
static struct fuse_operations ops;
int main(int argc, char *argv[])
{
    ops.getattr = GetAttr;
    return fuse_main(argc, argv, &ops);
}

使用两个终端在(大致)同一时间调用路径上的ls表明,第二个getattr调用只有在第一个调用完成后才开始,这导致第二个ls需要大约10秒,而不是5秒。

终端1

$ date; sudo ls /mnt/cachefs/file1.ext; date
Tue Aug 27 16:56:34 BST 2013
ls: /mnt/cachefs/file1.ext: Operation not permitted
Tue Aug 27 16:56:39 BST 2013

终端2

$ date; sudo ls /mnt/cachefs/file2.ext; date
Tue Aug 27 16:56:35 BST 2013
ls: /mnt/cachefs/file2.ext: Operation not permitted
Tue Aug 27 16:56:44 BST 2013

如您所见,ls之前的两个date输出之间的时间差仅相差1秒,但ls之后的两个输出之间的时差相差5秒,这与GetAttr功能中的延迟相对应。这表明第二个调用被阻塞在FUSE的某个深处。

输出

$ sudo ./cachefs /mnt/cachefs -f -d
unique: 1, opcode: INIT (26), nodeid: 0, insize: 56
INIT: 7.10
flags=0x0000000b
max_readahead=0x00020000
   INIT: 7.8
   flags=0x00000000
   max_readahead=0x00020000
   max_write=0x00020000
   unique: 1, error: 0 (Success), outsize: 40
unique: 2, opcode: LOOKUP (1), nodeid: 1, insize: 50
LOOKUP /file1.ext
Before: /file1.ext
After: /file1.ext
   unique: 2, error: -1 (Operation not permitted), outsize: 16
unique: 3, opcode: LOOKUP (1), nodeid: 1, insize: 50
LOOKUP /file2.ext
Before: /file2.ext
After: /file2.ext
   unique: 3, error: -1 (Operation not permitted), outsize: 16

上面的代码和示例与实际应用程序或应用程序的使用方式完全不同,但展示了相同的行为。我在上面的例子中没有显示这一点,但我发现一旦getattr调用完成,后续的打开调用就可以并行运行,正如我所期望的那样。

我搜索了医生,试图解释这种行为,并试图找到其他人报告类似的经历,但似乎什么都找不到。可能是因为getattr的大多数实现都很快,你不会注意到或关心它是否被串行化,也可能是因为我在配置中做了一些愚蠢的事情。我使用的是FUSE的2.7.4版本,所以这可能是一个已经修复的旧错误。

如果有人对此有任何见解,我们将不胜感激!

我注册了FUSE邮件列表,发布了我的问题,最近收到了Miklos Szeredi的以下回复:

查找(即首先查找与名称相关联的文件)是按目录序列化。这是在VFS(公共文件系统部分在内核中),因此基本上任何文件系统都容易受到这个问题,不仅仅是导火索。

非常感谢米克洛斯的帮助。有关完整线程,请参阅http://fuse.996288.n3.nabble.com/GetAttr-calls-being-serialised-td11741.html.

我还注意到串行化是按目录进行的,即如果两个文件都在同一目录中,则会出现上述效果,但如果它们在不同的目录中,就不会出现这种效果。对于我的应用程序来说,这种缓解措施对我来说已经足够了,我的文件系统的客户端确实使用目录,因此,虽然我可能会期望大量的getattr调用连续进行,但它们都发生在同一目录上的可能性很低,我不必担心。

对于那些这种缓解措施还不够的人来说,如果你的文件系统支持目录列表,你可以利用David Strauss的建议,即使用readdir调用作为触发来启动你的缓存:

在我们的文件系统中,我们尝试预取和缓存属性readdir期间的信息(将不可避免地被请求),因此我们不必为每一个都打后端。

由于我的文件系统后端没有目录的概念,我无法利用他的建议,但希望这对其他人有帮助。