[RFC PATCH v1] fuse: add periodic task to invalidate expired dentries
From: Luis Henriques
Date: Wed Mar 19 2025 - 07:31:15 EST
dentries may stay around for a long time, and a mechanism to invalidate
them if they have expired is desirable. This patch adds a task that will
periodically check if there are dentries which have expired and need to be
invalidated.
Signed-off-by: Luis Henriques <luis@xxxxxxxxxx>
---
Hi Miklos,
I know the 'epoch' patch discussion hasn't yet finished[1], but here's a
follow-up patch. It's still WIP, it hasn't gone through a lot of testing
yet, but it may help with the whole discussion.
As you suggested, this patch keeps track of all the dentries in a tree
sorted by expiry time. The workqueue will walk through the expired dentries
and invalidate them. If the epoch has been incremented, then *all* dentries
are invalidated.
I still have a few questions:
1. Should we have a mount option to enable this task?
2. Should the period (not really 'period', but yeah) be configurable?
Any feedback is welcome.
[1] https://lore.kernel.org/all/20250226091451.11899-1-luis@xxxxxxxxxx/
fs/fuse/dir.c | 138 +++++++++++++++++++++++++++++++++++++++++++++++
fs/fuse/fuse_i.h | 11 ++++
fs/fuse/inode.c | 4 ++
3 files changed, 153 insertions(+)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 1f578f455364..e51a7340fa5a 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -62,6 +62,142 @@ static inline u64 fuse_dentry_time(const struct dentry *entry)
}
#endif
+struct dentry_node {
+ struct rb_node node;
+ struct dentry *dentry;
+};
+
+static void fuse_dentry_tree_add_node(struct dentry *dentry)
+{
+ struct fuse_conn *fc = get_fuse_conn_super(dentry->d_sb);
+ struct dentry_node *dn, *cur;
+ struct rb_node **p, *parent;
+ bool start_work = false;
+
+ dn = kmalloc(sizeof(*dn), GFP_KERNEL);
+ if (!dn)
+ return;
+ dn->dentry = dget(dentry);
+ spin_lock(&fc->dentry_tree_lock);
+ start_work = RB_EMPTY_ROOT(&fc->dentry_tree);
+ p = &fc->dentry_tree.rb_node;
+ while (*p) {
+ parent = *p;
+ cur = rb_entry(*p, struct dentry_node, node);
+ if (fuse_dentry_time(dn->dentry) >
+ fuse_dentry_time(cur->dentry))
+ p = &(*p)->rb_left;
+ else
+ p = &(*p)->rb_right;
+ }
+ rb_link_node(&dn->node, parent, p);
+ rb_insert_color(&dn->node, &fc->dentry_tree);
+ spin_unlock(&fc->dentry_tree_lock);
+ if (start_work)
+ schedule_delayed_work(&fc->dentry_tree_work,
+ secs_to_jiffies(5));
+}
+
+static void fuse_dentry_tree_del_node(struct dentry *dentry)
+{
+ struct fuse_conn *fc = get_fuse_conn_super(dentry->d_sb);
+ struct dentry_node *cur;
+ struct rb_node **p, *parent;
+
+ spin_lock(&fc->dentry_tree_lock);
+ p = &fc->dentry_tree.rb_node;
+ while (*p) {
+ parent = *p;
+ cur = rb_entry(*p, struct dentry_node, node);
+ if (fuse_dentry_time(dentry) > fuse_dentry_time(cur->dentry))
+ p = &(*p)->rb_left;
+ else if (fuse_dentry_time(dentry) <
+ fuse_dentry_time(cur->dentry))
+ p = &(*p)->rb_right;
+ else {
+ rb_erase(*p, &fc->dentry_tree);
+ dput(cur->dentry);
+ kfree(cur);
+ break;
+ }
+ }
+ spin_unlock(&fc->dentry_tree_lock);
+}
+
+void fuse_dentry_tree_prune(struct fuse_conn *fc)
+{
+ struct rb_node *n;
+ struct dentry_node *dn;
+
+ cancel_delayed_work_sync(&fc->dentry_tree_work);
+
+ spin_lock(&fc->dentry_tree_lock);
+ while (!RB_EMPTY_ROOT(&fc->dentry_tree)) {
+ n = rb_first(&fc->dentry_tree);
+ dn = rb_entry(n, struct dentry_node, node);
+ rb_erase(n, &fc->dentry_tree);
+ dput(dn->dentry);
+ kfree(dn);
+ }
+ spin_unlock(&fc->dentry_tree_lock);
+}
+
+/*
+ * Global workqueue task that will periodically check for expired dentries in
+ * the dentries tree.
+ *
+ * A dentry has expired if:
+ * 1) it has been around for too long or
+ * 2) the connection epoch has been incremented
+ * For this second case, all dentries will be expired.
+ *
+ * The task will be rescheduled as long as the dentries tree is not empty.
+ */
+void fuse_dentry_tree_work(struct work_struct *work)
+{
+ struct fuse_conn *fc = container_of(work, struct fuse_conn,
+ dentry_tree_work.work);
+ struct dentry_node *dn;
+ struct rb_node *node;
+ struct dentry *entry;
+ u64 now;
+ int epoch;
+ bool expire_all = false;
+ bool is_first = true;
+ bool reschedule;
+
+ spin_lock(&fc->dentry_tree_lock);
+ now = get_jiffies_64();
+ epoch = atomic_read(&fc->epoch);
+
+ node = rb_first(&fc->dentry_tree);
+
+ while (node) {
+ dn = rb_entry(node, struct dentry_node, node);
+ node = rb_next(node);
+ entry = dn->dentry;
+ if (is_first) {
+ /* expire all entries if epoch was incremented */
+ if (entry->d_time < epoch)
+ expire_all = true;
+ is_first = false;
+ }
+ if (expire_all || (fuse_dentry_time(entry) < now)) {
+ rb_erase(&dn->node, &fc->dentry_tree);
+ d_invalidate(entry);
+ dput(entry);
+ kfree(dn);
+ } else
+ break;
+ }
+ reschedule = !RB_EMPTY_ROOT(&fc->dentry_tree);
+ spin_unlock(&fc->dentry_tree_lock);
+
+ if (reschedule)
+ schedule_delayed_work(&fc->dentry_tree_work,
+ FUSE_DENTRY_TREE_WORK_INTERVAL);
+}
+
static void fuse_dentry_settime(struct dentry *dentry, u64 time)
{
struct fuse_conn *fc = get_fuse_conn_super(dentry->d_sb);
@@ -81,6 +217,7 @@ static void fuse_dentry_settime(struct dentry *dentry, u64 time)
}
__fuse_dentry_settime(dentry, time);
+ fuse_dentry_tree_add_node(dentry);
}
/*
@@ -280,6 +417,7 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
invalid:
ret = 0;
+ fuse_dentry_tree_del_node(entry);
goto out;
}
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 06eecc125f89..942a3098111f 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -938,6 +938,13 @@ struct fuse_conn {
/** uring connection information*/
struct fuse_ring *ring;
#endif
+
+ /** Cache dentries tree */
+ struct rb_root dentry_tree;
+ /** Look to protect dentry_tree access */
+ spinlock_t dentry_tree_lock;
+ /** Periodic delayed work to invalidate expired dentries */
+ struct delayed_work dentry_tree_work;
};
/*
@@ -1219,6 +1226,10 @@ void fuse_request_end(struct fuse_req *req);
void fuse_abort_conn(struct fuse_conn *fc);
void fuse_wait_aborted(struct fuse_conn *fc);
+#define FUSE_DENTRY_TREE_WORK_INTERVAL secs_to_jiffies(5)
+void fuse_dentry_tree_prune(struct fuse_conn *fc);
+void fuse_dentry_tree_work(struct work_struct *work);
+
/**
* Invalidate inode attributes
*/
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index 5d2d29fad658..8984b7868c62 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -956,15 +956,18 @@ void fuse_conn_init(struct fuse_conn *fc, struct fuse_mount *fm,
memset(fc, 0, sizeof(*fc));
spin_lock_init(&fc->lock);
spin_lock_init(&fc->bg_lock);
+ spin_lock_init(&fc->dentry_tree_lock);
init_rwsem(&fc->killsb);
refcount_set(&fc->count, 1);
atomic_set(&fc->dev_count, 1);
atomic_set(&fc->epoch, 1);
init_waitqueue_head(&fc->blocked_waitq);
fuse_iqueue_init(&fc->iq, fiq_ops, fiq_priv);
+ INIT_DELAYED_WORK(&fc->dentry_tree_work, fuse_dentry_tree_work);
INIT_LIST_HEAD(&fc->bg_queue);
INIT_LIST_HEAD(&fc->entry);
INIT_LIST_HEAD(&fc->devices);
+ fc->dentry_tree = RB_ROOT;
atomic_set(&fc->num_waiting, 0);
fc->max_background = FUSE_DEFAULT_MAX_BACKGROUND;
fc->congestion_threshold = FUSE_DEFAULT_CONGESTION_THRESHOLD;
@@ -1999,6 +2002,7 @@ void fuse_conn_destroy(struct fuse_mount *fm)
fuse_abort_conn(fc);
fuse_wait_aborted(fc);
+ fuse_dentry_tree_prune(fc);
if (!list_empty(&fc->entry)) {
mutex_lock(&fuse_mutex);