[PATCH v4] fuse: add new function to invalidate cache for all inodes

From: Luis Henriques
Date: Tue Feb 11 2025 - 04:26:22 EST


Currently userspace is able to notify the kernel to invalidate the cache
for an inode. This means that, if all the inodes in a filesystem need to
be invalidated, then userspace needs to iterate through all of them and do
this kernel notification separately.

This patch adds a new option that allows userspace to invalidate all the
inodes with a single notification operation. In addition to invalidate
all the inodes, it also shrinks the sb dcache.

Signed-off-by: Luis Henriques <luis@xxxxxxxxxx>
---
* Changes since v3
- Added comments to clarify semantic changes in fuse_reverse_inval_inode()
when called with FUSE_INVAL_ALL_INODES (suggested by Bernd).
- Added comments to inodes iteration loop to clarify __iget/iput usage
(suggested by Joanne)
- Dropped get_fuse_mount() call -- fuse_mount can be obtained from
fuse_ilookup() directly (suggested by Joanne)

(Also dropped the RFC from the subject.)

* Changes since v2
- Use the new helper from fuse_reverse_inval_inode(), as suggested by Bernd.
- Also updated patch description as per checkpatch.pl suggestion.

* Changes since v1
As suggested by Bernd, this patch v2 simply adds an helper function that
will make it easier to replace most of it's code by a call to function
super_iter_inodes() when Dave Chinner's patch[1] eventually gets merged.

[1] https://lore.kernel.org/r/20241002014017.3801899-3-david@xxxxxxxxxxxxx

fs/fuse/inode.c | 83 +++++++++++++++++++++++++++++++++++----
include/uapi/linux/fuse.h | 3 ++
2 files changed, 79 insertions(+), 7 deletions(-)

diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index e9db2cb8c150..5aa49856731a 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -547,25 +547,94 @@ struct inode *fuse_ilookup(struct fuse_conn *fc, u64 nodeid,
return NULL;
}

+static void inval_single_inode(struct inode *inode, struct fuse_conn *fc)
+{
+ struct fuse_inode *fi;
+
+ fi = get_fuse_inode(inode);
+ spin_lock(&fi->lock);
+ fi->attr_version = atomic64_inc_return(&fc->attr_version);
+ spin_unlock(&fi->lock);
+ fuse_invalidate_attr(inode);
+ forget_all_cached_acls(inode);
+}
+
+static int fuse_reverse_inval_all(struct fuse_conn *fc)
+{
+ struct fuse_mount *fm;
+ struct super_block *sb;
+ struct inode *inode, *old_inode = NULL;
+
+ inode = fuse_ilookup(fc, FUSE_ROOT_ID, &fm);
+ if (!inode || !fm)
+ return -ENOENT;
+
+ iput(inode);
+ sb = fm->sb;
+
+ spin_lock(&sb->s_inode_list_lock);
+ list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
+ spin_lock(&inode->i_lock);
+ if ((inode->i_state & (I_FREEING|I_WILL_FREE|I_NEW)) ||
+ !atomic_read(&inode->i_count)) {
+ spin_unlock(&inode->i_lock);
+ continue;
+ }
+
+ /*
+ * This __iget()/iput() dance is required so that we can release
+ * the sb lock and continue the iteration on the previous
+ * inode. If we don't keep a ref to the old inode it could have
+ * disappear. This way we can safely call cond_resched() when
+ * there's a huge amount of inodes to iterate.
+ */
+ __iget(inode);
+ spin_unlock(&inode->i_lock);
+ spin_unlock(&sb->s_inode_list_lock);
+ iput(old_inode);
+
+ inval_single_inode(inode, fc);
+
+ old_inode = inode;
+ cond_resched();
+ spin_lock(&sb->s_inode_list_lock);
+ }
+ spin_unlock(&sb->s_inode_list_lock);
+ iput(old_inode);
+
+ shrink_dcache_sb(sb);
+
+ return 0;
+}
+
+/*
+ * Notify to invalidate inodes cache. It can be called with @nodeid set to
+ * either:
+ *
+ * - An inode number - Any pending writebacks within the rage [@offset @len]
+ * will be triggered and the inode will be validated. To invalidate the whole
+ * cache @offset has to be set to '0' and @len needs to be <= '0'; if @offset
+ * is negative, only the inode attributes are invalidated.
+ *
+ * - FUSE_INVAL_ALL_INODES - All the inodes in the superblock are invalidated
+ * and the whole dcache is shrinked.
+ */
int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid,
loff_t offset, loff_t len)
{
- struct fuse_inode *fi;
struct inode *inode;
pgoff_t pg_start;
pgoff_t pg_end;

+ if (nodeid == FUSE_INVAL_ALL_INODES)
+ return fuse_reverse_inval_all(fc);
+
inode = fuse_ilookup(fc, nodeid, NULL);
if (!inode)
return -ENOENT;

- fi = get_fuse_inode(inode);
- spin_lock(&fi->lock);
- fi->attr_version = atomic64_inc_return(&fc->attr_version);
- spin_unlock(&fi->lock);
+ inval_single_inode(inode, fc);

- fuse_invalidate_attr(inode);
- forget_all_cached_acls(inode);
if (offset >= 0) {
pg_start = offset >> PAGE_SHIFT;
if (len <= 0)
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 5e0eb41d967e..e5852b63f99f 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -669,6 +669,9 @@ enum fuse_notify_code {
FUSE_NOTIFY_CODE_MAX,
};

+/* The nodeid to request to invalidate all inodes */
+#define FUSE_INVAL_ALL_INODES 0
+
/* The read buffer is required to be at least 8k, but may be much larger */
#define FUSE_MIN_READ_BUFFER 8192