Должна ли операция FUSE getattr быть сериализована?

Я реализую файловую систему FUSE, предназначенную для обеспечения доступа через знакомые вызовы POSIX к файлам, которые фактически хранятся за API RESTful. Файловая система кэширует файлы, как только они были получены в первый раз, так что они становятся более доступными при последующих обращениях.

Я запускаю файловую систему в многопоточном режиме (это FUSE по умолчанию), но обнаружил, что вызовы getattr, похоже, сериализованы, хотя другие вызовы могут происходить параллельно.

При открытии файла FUSE всегда сначала вызывает getattr, а клиент, который я поддерживаю, требует, чтобы размер файла, возвращенный этим первоначальным вызовом, был точным (у меня нет никакого контроля над этим поведением). Это означает, что если у меня нет кэшированного файла, мне нужно получить информацию через вызовы API RESTful. Иногда эти вызовы происходят в сетях с высокой задержкой, время прохождения которых составляет около 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

Как вы можете видеть, разница во времени с двумя выходами date до ls отличается только на одну секунду, но две из них после 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 были бы такими быстрыми, что вы не заметили бы и не заботились бы о том, было ли это сериализовано, или, может быть, потому, что я делаю что-то глупое в конфигурации. Я использую версию 2.7.4 от FUSE, поэтому возможно, что это была старая ошибка, которая с тех пор была исправлена.

Если у кого-то есть понимание этого, мы будем очень благодарны!

Ответ 1

Я подписался на список рассылки FUSE, разместил свой вопрос и недавно получил следующий ответ от Миклоша Шереди:

Поиск (т.е. первое обнаружение файла, связанного с именем), является сериализован для каждого каталога. Это в VFS (общая файловая система часть в ядре), поэтому в принципе любая файловая система подвержена этот вопрос, а не только предохранитель.

Большое спасибо Миклосу за помощь. Полный поток см. http://fuse.996288.n3.nabble.com/GetAttr-calls-being-serialised-td11741.html.

Я также заметил, что сериализация была для каждого каталога, т.е. вышеупомянутый эффект был бы замечен, если оба файла находились в одном каталоге, но не в том случае, если они были в отдельных каталогах. Для моего приложения этого смягчения достаточно для меня, клиенты в моей файловой системе действительно используют каталоги, поэтому, хотя я могу ожидать много вызовов getattr в тесной последовательности, вероятность того, что все они происходят в одном каталоге, достаточно низка для меня, чтобы не беспокоиться.

Для тех, для кого это смягчение недостаточно, если ваша файловая система поддерживает список каталогов, вы можете воспользоваться предложением Дэвида Штрауса, который должен использовать вызов readdir в качестве триггера для запуска вашего кеша:

В наших файловых системах мы пытаемся предварительно извлечь и кешировать атрибут информация (которая будет неизбежно запрошена) во время readdir, поэтому мы не нужно ударять бэкэнда для каждого из них.

Поскольку бэкэнд для моей файловой системы не имеет понятия о каталогах, я не смог воспользоваться его предложением, но, надеюсь, это будет полезно для других.