Удаление файла из тэга

С удалением файлов всё делается через функцию unlink соответствующий ноды:

const struct inode_operations tagfs_tag_dir_inode_ops = {
  // …
  .unlink = tagfs_tag_dir_unlink
  // …
};

Сама функция-обработчик получает указатель на родительскую ноду (фактически на директорию) и dentry на сам удаляемый элемент:

int tagfs_tag_dir_unlink(struct inode* dir, struct dentry* de)

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

?? А вот для полного удаления файла из ФС есть специальная папка «all-files». ??
?? А что с удалением тэгов ??


Далее, т.к. есть no-тэги, то удаление из такого no-тэга приводит к добавлению тэга к файлу (что в общем-то логично).

Если же реализовать эту логику, то получится интересный эффект. Например:
Пусть есть файл file1, который находится в папке tag1 (т.е. к файлу file1 привязан тэг tag1). Если удалить файл из тэга, то он появится в тэге no-tag1 — всё логично.
Далее удаляем файл file1 из тэга tag1 и он должен появится в директории tag1.

И файл действительно появляется в директории tag1. Файловая система так думает: файл есть. Операционная система считает, что такого файла нет. Если точнее, то такого файла нет, но известно его имя (file1) и тип (символьная ссылка). В терминале это выглядит так:

В программе это выглядит: при вычитывании содержимого директории readdir выдаёт корректное имя, однако при попытке, например, вызвать lstat64 получаем ошибку и код 2: файл не существует.

Если включить логирование операций iterate и lookup выяснится интересная вещь: в процессе iterate наша ФС выдаёт имя вновь появившегося файла. Однако lookup на этот файл не делается.
И в результате файл остаётся с известным именем и типом, но ничего другого нет и операции с таким файлом не работают.

Такое поведение связано с такой вещью как negative dentry cache — кэширование негативных dentry
т.е. linux в своих благих намерениях запоминает dentry для удалённых файлов в кэше (возможно, отдельном кэше) и когда требуется сделать lookup то берётся значение оттуда.
Причём официального доступа к этому кэшу для внешних модулей не предоставляется.
В общем, предполагается, что сам по себе файл появится не может, только через вызов symlink или create какой-нибудь.

Подробнее что это и зачем можно прочитать здесь: Dealing with negative dentries.

И линукс сам предлагает пути обхода своего же функционала. Нашёл 2 способа:

  • 1. не создавать (и не оставлять) негативных dentry. Такой подход используется в реализации ext4 для некоторых случаев (VFS negative dentries are incompatible with Encoding and Case-insensitiveness).
    Реализуется это так:
    если удаляется файл, то для dentry вызывается функция d_invalidate(dentry);
    если делается lookup на файл, которого сейчас нет, то вместо добавления в хэш-лист негативного dentry через d_add(dentry, NULL) никаких добавлений в хэш-лист не делаем и возвращаем NULL.
  • 2. Использовать ревалидацию:
    int d_revalidate(struct dentry *dentry, int flags) — определяет, является ли указанный объект элемента каталога действительным.
    VFS вызывает эту функцию, когда она пытается использовать объект dentry из кэша dcache.
    Для большинства файловых систем этот метод установлен в значение NULL, потому что объекты denry, которые находятся в кэше, всегда действительны.
    В основном, обратное утверждение будет верно для сетевых ФС, когда кэш хранится на одной машине, а файлы — на другой.

По результатам проб:
Способ 1 (использовать d_invalidate и не использовать d_add) — работает.

Про revalidate — эта функция как-бы и предназначена для работы с файловыми системами, где всё может само поменяться.

В операциях dentry есть два revalidate-а: обычный и weak. Описание про weak функцию ( d_weak_revalidate ) говорит, что она применяется на граничных («jumped») dentry: стык с родительской файловой системой, корневой нод, и т.п. Для нашего случая это не требуется, поэтому пробуем обычную форму.

В функцию обработчик передаётся собственно сам dentry и набор флагов, относящихся к lookup-у: LOOKUP_RCU и т.п.

Обработчик будет вешаться только на удаляемые или несуществующие dentry. И возвращать она будет 0 — т.е. ошибок нет, однако dentry уже невалидный.
Для назначения обработчика используем функцию d_set_d_op. В результате получится что-то типа:

int tagfs_tag_dir_revalidate(struct dentry* de, unsigned int flags) {
  return 0;
}

const struct dentry_operations tagfs_tag_dir_negative_dentry_ops = {
  .d_revalidate = tagfs_tag_dir_revalidate
};

// ...
d_set_d_op(de, &tagfs_tag_dir_negative_dentry_ops);

В общем, второй способ также рабочий.


С удалением тэгов может показаться что всё проще — нет операции удаления и ничего не делается.

Однако не всё так просто: пользователи иногда удаляют директории вместе с содержимым (команда rm), аналогично через файловые менеджер (например, midnight commander), или даже через unlink. И в первых двух случаэтот коммандер помимо собственно удаления директории (суть тэга) ещё пытается предварительно эту директорию почистить. И вложенные директории почистить. А так как вложенность директорий может достигать всего множества тэгов начнётся тотальное удаление тэгов из файлов.

В нашем случае всё ещё интереснее, т.к. во вложенных директориях есть как «прямые» тэги, так и обратные no-тэги. Последовательное удаление файла из такой пары тэгов выдаёт результат, зависящий от порядка применения операции удаления: файл может появится в тэге или исчезнуть из тэга. В общем, всё весело.


Ещё возникают особые случаи с удалением тэгов и последующим их появлением снова. Как выше писалось, чтобы всё работало, нужно корректно обрабатывать кэш негативных dentry. Однако, этого может быть мало. В частности:

Иногда могут возникать фантомы в структуре директорий: директория tag1 вложена в директорию tag1 (для тэговой файловой системы это неправильное поведение); в директории tag1 отсутствует директория tag3 (должна быть):

Тестовый скрипт для воспроизведения:

cd /tagfs/tag

cd only-tags
mkdir tag1
mkdir tag2

cd ../tags/tag1
echo First tag1 ls
pwd
ls -a -i -l
echo ----------

cd ../../only-tags

rm -frd tag1
mkdir tag3
mkdir tag1

cd ../tags/tag1
echo Second tag1 ls
pwd
ls -a -i -l
echo ----------

cd ../../only-tags

rm -frd tag1
rm -frd tag2
rm -frd tag3

Такое поведение связано с тем, что был удалён tag1 и был создан tag3, который использовал свободный номер (ino). поэтому директория tags/tag1 фактически отображает содержимое tag3 и само это содержимое не изменится и не перечитается: linux будет смотреть на наличие существующих номеров и всё кэшировать. Наверное, такое поведение можно исправить, если повесить на всё revalidate и каким-то образом проверять, что некий сгенерированный ino для тэга (а мы их генерируем, т.к. требуется уникальность) теперь уже соответствует другой директории.

Также, как вариант, можно сделать «ротацию», т.к. при создании нового тэга искать начиная с последнего добавленного номера. Понятно, что это тоже может привести к странным результатам. Однако это будет не сразу и при «обычном» использовании такую ситуацию будет повторить нелегко.

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