[PATCH v7 3/3] exfat: reclaim name filters under memory pressure

From: Yang Wen

Date: Sun Oct 04 2026 - 09:58:21 EST


Each active large directory can hold a dynamically sized name filter with a
bitmap of up to 4 MiB. Add a per-superblock LRU and shrinker so filters can
be reclaimed when memory is tight. Account reclaimable filters in pages so
reclaim cost tracks their differing allocation sizes.

Keep the scan budget initialized by the shrinker core instead of replacing
it with the number of filters freed. If the LRU is already empty, return
SHRINK_STOP. This guarantees that do_shrink_slab() either consumes its scan
budget or stops, even when the LRU becomes empty before nr_to_scan is
satisfied.

Signed-off-by: Yang Wen <anmuxixixi@xxxxxxxxx>
---
fs/exfat/dir.c | 159 +++++++++++++++++++++++++++++++++++++++++++-
fs/exfat/exfat_fs.h | 10 +++
fs/exfat/super.c | 8 +++
3 files changed, 174 insertions(+), 3 deletions(-)

diff --git a/fs/exfat/dir.c b/fs/exfat/dir.c
index 9f9183116784..cf780f7f540c 100644
--- a/fs/exfat/dir.c
+++ b/fs/exfat/dir.c
@@ -10,6 +10,7 @@
#include <linux/filelock.h>
#include <linux/hash.h>
#include <linux/log2.h>
+#include <linux/shrinker.h>
#include <linux/stringhash.h>

#include "exfat_raw.h"
@@ -127,12 +128,158 @@ static bool exfat_name_filter_set(struct exfat_name_filter *filter,
return filter->nr_set_bits >= nr_bits / 3;
}

+static unsigned long
+exfat_name_filter_nr_pages(const struct exfat_name_filter *filter)
+{
+ size_t size = struct_size(filter, bitmap,
+ BITS_TO_LONGS(BIT(filter->order)));
+
+ return DIV_ROUND_UP(size, PAGE_SIZE);
+}
+
+static struct exfat_name_filter *
+exfat_name_filter_detach_locked(struct exfat_sb_info *sbi,
+ struct exfat_inode_info *ei)
+{
+ struct exfat_name_filter *filter = ei->name_filter;
+
+ if (!filter)
+ return NULL;
+
+ ei->name_filter = NULL;
+ list_del_init(&ei->name_filter_lru);
+ sbi->name_filter_pages -= exfat_name_filter_nr_pages(filter);
+ return filter;
+}
+
+static void exfat_name_filter_touch(struct exfat_inode_info *ei)
+{
+ struct exfat_sb_info *sbi = EXFAT_SB(ei->vfs_inode.i_sb);
+
+ spin_lock(&sbi->name_filter_lock);
+ if (ei->name_filter)
+ list_move_tail(&ei->name_filter_lru, &sbi->name_filter_lru);
+ spin_unlock(&sbi->name_filter_lock);
+}
+
void exfat_name_filter_free(struct inode *inode)
{
struct exfat_inode_info *ei = EXFAT_I(inode);
+ struct exfat_sb_info *sbi;
+ struct exfat_name_filter *filter;

- kvfree(ei->name_filter);
- ei->name_filter = NULL;
+ if (!READ_ONCE(ei->name_filter))
+ return;
+
+ sbi = EXFAT_SB(inode->i_sb);
+ spin_lock(&sbi->name_filter_lock);
+ filter = exfat_name_filter_detach_locked(sbi, ei);
+ spin_unlock(&sbi->name_filter_lock);
+ kvfree(filter);
+}
+
+static unsigned long
+exfat_name_filter_count_objects(struct shrinker *shrinker,
+ struct shrink_control *sc)
+{
+ struct exfat_sb_info *sbi = shrinker->private_data;
+ unsigned long pages;
+
+ spin_lock(&sbi->name_filter_lock);
+ pages = sbi->name_filter_pages;
+ spin_unlock(&sbi->name_filter_lock);
+
+ return pages ? pages : SHRINK_EMPTY;
+}
+
+static unsigned long
+exfat_name_filter_scan_objects(struct shrinker *shrinker,
+ struct shrink_control *sc)
+{
+ struct exfat_sb_info *sbi = shrinker->private_data;
+ unsigned long freed = 0;
+ bool empty = false;
+
+ /* Avoid reclaim recursion from a GFP_NOFS allocation under s_lock. */
+ if (!mutex_trylock(&sbi->s_lock)) {
+ sc->nr_scanned = 0;
+ return SHRINK_STOP;
+ }
+
+ while (freed < sc->nr_to_scan) {
+ struct exfat_inode_info *ei;
+ struct exfat_name_filter *filter;
+
+ spin_lock(&sbi->name_filter_lock);
+ if (list_empty(&sbi->name_filter_lru)) {
+ spin_unlock(&sbi->name_filter_lock);
+ empty = true;
+ break;
+ }
+
+ ei = list_first_entry(&sbi->name_filter_lru,
+ struct exfat_inode_info,
+ name_filter_lru);
+ filter = exfat_name_filter_detach_locked(sbi, ei);
+ spin_unlock(&sbi->name_filter_lock);
+
+ freed += exfat_name_filter_nr_pages(filter);
+ kvfree(filter);
+ cond_resched();
+ }
+
+ mutex_unlock(&sbi->s_lock);
+ if (empty && !freed) {
+ sc->nr_scanned = 0;
+ return SHRINK_STOP;
+ }
+ return freed;
+}
+
+void exfat_name_filter_shrinker_register(struct super_block *sb)
+{
+ struct exfat_sb_info *sbi = EXFAT_SB(sb);
+ struct shrinker *shrinker;
+
+ shrinker = shrinker_alloc(SHRINKER_NONSLAB,
+ "exfat-name-filter:%s", sb->s_id);
+ if (!shrinker) {
+ exfat_warn(sb, "failed to allocate name filter shrinker");
+ return;
+ }
+
+ shrinker->count_objects = exfat_name_filter_count_objects;
+ shrinker->scan_objects = exfat_name_filter_scan_objects;
+ shrinker->private_data = sbi;
+ shrinker_register(shrinker);
+ sbi->name_filter_shrinker = shrinker;
+}
+
+void exfat_name_filter_shrinker_unregister(struct super_block *sb)
+{
+ struct exfat_sb_info *sbi = EXFAT_SB(sb);
+ struct shrinker *shrinker = sbi->name_filter_shrinker;
+
+ sbi->name_filter_shrinker = NULL;
+ shrinker_free(shrinker);
+
+ for (;;) {
+ struct exfat_inode_info *ei;
+ struct exfat_name_filter *filter;
+
+ spin_lock(&sbi->name_filter_lock);
+ if (list_empty(&sbi->name_filter_lru)) {
+ spin_unlock(&sbi->name_filter_lock);
+ break;
+ }
+
+ ei = list_first_entry(&sbi->name_filter_lru,
+ struct exfat_inode_info,
+ name_filter_lru);
+ filter = exfat_name_filter_detach_locked(sbi, ei);
+ spin_unlock(&sbi->name_filter_lock);
+ kvfree(filter);
+ }
}

bool exfat_name_filter_maybe_contains(struct inode *inode,
@@ -145,6 +292,7 @@ bool exfat_name_filter_maybe_contains(struct inode *inode,
if (!filter)
return true;

+ exfat_name_filter_touch(ei);
exfat_name_filter_indexes(inode->i_sb, name, filter->order, indexes);
return test_bit(indexes[0], filter->bitmap) &&
test_bit(indexes[1], filter->bitmap) &&
@@ -161,6 +309,7 @@ void exfat_name_filter_add(struct inode *inode,
if (!filter)
return;

+ exfat_name_filter_touch(ei);
if (exfat_name_filter_order(inode) > filter->order) {
exfat_name_filter_free(inode);
return;
@@ -188,7 +337,7 @@ static void exfat_build_name_filter(struct super_block *sb,
u8 order;
int i;

- if (ei->name_filter ||
+ if (!sbi->name_filter_shrinker || ei->name_filter ||
exfat_bytes_to_dentries(i_size_read(inode)) <
EXFAT_NAME_FILTER_MIN_DENTRIES)
return;
@@ -240,7 +389,11 @@ static void exfat_build_name_filter(struct super_block *sb,
}

complete:
+ spin_lock(&sbi->name_filter_lock);
ei->name_filter = filter;
+ list_add_tail(&ei->name_filter_lru, &sbi->name_filter_lru);
+ sbi->name_filter_pages += exfat_name_filter_nr_pages(filter);
+ spin_unlock(&sbi->name_filter_lock);
return;
abort:
kvfree(filter);
diff --git a/fs/exfat/exfat_fs.h b/fs/exfat/exfat_fs.h
index 70f4e2d4ac81..159c1aa40372 100644
--- a/fs/exfat/exfat_fs.h
+++ b/fs/exfat/exfat_fs.h
@@ -14,6 +14,8 @@
#include <uapi/linux/exfat.h>
#include <linux/buffer_head.h>

+struct shrinker;
+
#define EXFAT_ROOT_INO 1

/*
@@ -265,6 +267,11 @@ struct exfat_sb_info {

spinlock_t inode_hash_lock;
struct hlist_head inode_hashtable[EXFAT_HASH_SIZE];
+ /* Protects name_filter_lru and name_filter_pages. */
+ spinlock_t name_filter_lock;
+ struct list_head name_filter_lru;
+ unsigned long name_filter_pages;
+ struct shrinker *name_filter_shrinker;
struct rcu_head rcu;
};

@@ -296,6 +303,7 @@ struct exfat_inode_info {
struct exfat_hint_femp hint_femp;
/* Complete, in-memory Bloom filter of directory names */
struct exfat_name_filter *name_filter;
+ struct list_head name_filter_lru;

spinlock_t cache_lru_lock;
struct list_head cache_lru;
@@ -637,6 +645,8 @@ bool exfat_name_filter_maybe_contains(struct inode *inode,
void exfat_name_filter_add(struct inode *inode,
const struct exfat_uni_name *name);
void exfat_name_filter_free(struct inode *inode);
+void exfat_name_filter_shrinker_register(struct super_block *sb);
+void exfat_name_filter_shrinker_unregister(struct super_block *sb);

static inline int exfat_chain_advance(struct super_block *sb,
struct exfat_chain *chain, unsigned int step)
diff --git a/fs/exfat/super.c b/fs/exfat/super.c
index 4924f0fad836..26b25cc86d1c 100644
--- a/fs/exfat/super.c
+++ b/fs/exfat/super.c
@@ -46,6 +46,7 @@ static void exfat_put_super(struct super_block *sb)
{
struct exfat_sb_info *sbi = EXFAT_SB(sb);

+ exfat_name_filter_shrinker_unregister(sb);
mutex_lock(&sbi->s_lock);
exfat_clear_volume_dirty(sb);
exfat_free_bitmap(sbi);
@@ -210,6 +211,7 @@ static struct inode *exfat_alloc_inode(struct super_block *sb)
return NULL;

ei->name_filter = NULL;
+ INIT_LIST_HEAD(&ei->name_filter_lru);
return &ei->vfs_inode;
}

@@ -746,6 +748,8 @@ static int exfat_fill_super(struct super_block *sb, struct fs_context *fc)
goto free_table;
}

+ exfat_name_filter_shrinker_register(sb);
+
return 0;

put_inode:
@@ -845,6 +849,10 @@ static int exfat_init_fs_context(struct fs_context *fc)

mutex_init(&sbi->s_lock);
mutex_init(&sbi->bitmap_lock);
+ spin_lock_init(&sbi->name_filter_lock);
+ INIT_LIST_HEAD(&sbi->name_filter_lru);
+ sbi->name_filter_pages = 0;
+ sbi->name_filter_shrinker = NULL;
ratelimit_state_init(&sbi->ratelimit, DEFAULT_RATELIMIT_INTERVAL,
DEFAULT_RATELIMIT_BURST);

--
2.34.1